Esbuild概念,使用与自定义插件实现
2022-5-12
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 04:11 PM
type
Post
status
Published
date
May 12, 2022
slug
summary
Esbuild概念,使用与自定义插件实现
tags
Esbuild
category
工程化
icon

ESBuild的优势

Transform API

Build API

通过build可以对一个或多个文件进行构建, 同时, 也可以将相互之间有引用关系的文档打包到一起, 一个简单的构建如下
 require('fs').writeFileSync('in.ts', 'let x: number = 1')  require('esbuild').buildSync({    entryPoints: ['in.ts'],    outfile: 'out.js',  })  { errors: [], warnings: [] }  require('fs').readFileSync('out.js', 'utf8')  'let x = 1;\n'
esbuild默认情况下只会进行转换而非打包, 如果需要进行打包操作, 则需要显式指定--bundle
构建相关的API主要如下, 分为基础选项和进阶选项

基础选项

bundle

bundle意味着会将任何导入的依赖关系内联到文件本身中, 整个流程会被递归执行. 以将所有依赖都引入进一个文件中
 require('esbuild').buildSync({    entryPoints: ['in.js'],    bundle: true,    outfile: 'out.js',  })  { errors: [], warnings: [] }
需要注意的是, esbuild的依赖分析只会分析静态的依赖, 正常的依赖导入会被分析和打包, 但是动态的引入则会被忽略
 // 以下的打包均会被忽略  import(`pkg/${foo}`);  require(`pkg/${foo}`);  ['pkg'].map(require);

define

通过define定义的变量会在编译阶段被替换
需要注意的是, define的替换是遵循直接替换的逻辑的, 也就是说我们进行普通的字符串替换, 在替换成功之后, 会变成一个变量, 因此替换成字符串时需要加上引号
 let js = 'hooks = DEBUG && require("hooks")'  require('esbuild').transformSync(js, {    define: { DEBUG: 'true' },  })  {    code: 'hooks = require("hooks");\n',    map: '',    warnings: []  }  require('esbuild').transformSync(js, {    define: { DEBUG: 'false' },  })  {    code: 'hooks = false;\n',    map: '',    warnings: []  }

entryPoints

entryPoints用于声明构建的入口, 支持数组和对象格式, 如果是对象格式, 则将会使用key来作为输出文件名称
 require('esbuild').buildSync({    entryPoints: ['home.ts', 'settings.ts'],    bundle: true,    write: true,    outdir: 'out',  })  // output: out/home.js out/setting.js  require('esbuild').buildSync({    entryPoints: {      out1: 'home.js',      out2: 'settings.js',   },    bundle: true,    write: true,    outdir: 'out',  }) // output out/out1.js out/out2.js

external

用于对打包阶段进行依赖排除, 可传入字符串数组作为值, 支持正则匹配
 require('fs').writeFileSync('app.js', 'require("fsevents")')  require('esbuild').buildSync({    entryPoints: ['app.js'],    outfile: 'out.js',    bundle: true,    platform: 'node',    external: ['fsevents'],  })    require('esbuild').buildSync({    entryPoints: ['app.js'],    outfile: 'out.js',    bundle: true,    external: ['*.png', '/images/*'],  })

format

设置输出文件的输出格式, 如果未设置时, esbuild会帮我们设置一种输出格式, 输出格式一般有以下几种
  1. iife
  1. cjs
  1. esm

inject

inject可以为构建上下文注入一个文件的内容
 require('esbuild').buildSync({    entryPoints: ['entry.js'],    bundle: true,    inject: ['./process-shim.js'],    outfile: 'out.js',  })
输出内容为
 // out.js  let process = {cwd: () => ""};  console.log(process.cwd());
一般在以下几种场景下很有用
  1. 自动在jsx中引入React.creatElement
  1. 统一在文件中注入一段文件, 我们可以不用担心重复注入的问题, esm会自动帮助我们处理
  1. 条件性注入一个文件

loader

目前esbuild提供了官方的loader, 官方的loader能够完成对日常场景下99%的场景支持, 具体loader列表在下面文档
使用方式则类似于webpack的loader
require('esbuild').buildSync({ entryPoints: ['app.js'], bundle: true, loader: { '.png': 'dataurl', '.svg': 'text', }, outfile: 'out.js', })
因为esbuild中存在stdin的标准输入文件, 此时需要单独对其指定一个loader
require('esbuild').buildSync({ stdin: { contents: 'import pkg = require("./pkg")', loader: 'ts', resolveDir: __dirname, }, bundle: true, outfile: 'out.js', })

minify

是否开启压缩代码, 一般我们设置minify为true即可
但是特殊场景下, minify是可以指定具体压缩的内容的, 在esbuild中一般有三种类型, 可以对其分别设置
  1. 压缩空格
  1. 压缩变量
  1. 压缩语法
var js = 'fn = obj => { return obj.x }' require('esbuild').transformSync(js, { minifyWhitespace: true, }) { code: 'fn=obj=>{return obj.x};\n', map: '', warnings: [] } require('esbuild').transformSync(js, { minifyIdentifiers: true, }) { code: 'fn = (n) => {\n return n.x;\n};\n', map: '', warnings: [] } require('esbuild').transformSync(js, { minifySyntax: true, }){ code: 'fn = (obj) => obj.x;\n', map: '', warnings: [] }
需要注意的是:
  1. esbuild的minify的产物是针对现代浏览器的, 例如: a === undefined || a === null ? 1 : a将会被压缩为a ?? 1, 如果这不是期望行为, 需要设置target为es6 -target=es6
  1. 转义符\n将会被替换成为模板字符串的换行符, 以节省占用字节大小
  1. 默认情况下esbuild不会压缩顶级声明的变量, 但当我们设置了format或bundle为true的时候, esbuild会认为压缩顶级声明变量是安全的, 而启动定义声明变量的压缩
  1. 压缩不是百分之百安全的, 即使是其他受欢迎的压缩工具例如terser也一样, 特别是在对于.toString()的处理上, 如果不去处理这类的内容压缩带来的效果就变得很小了, 这里建议的是使用明确的注释替代
  1. 默认情况下, esbuild不会保存函数和类的.name属性, 如果确实需要 可以设置keepNames为true
  1. 使用eval或with等将会使得许多优化失效导致整个文件无法被压缩处理, 一般建议不直接使用eval, 而是通过let result = (0, eval)(something)以防止对应的副作用产生
  1. esbuild中的压缩算法没有进行高级代码优化, 如果需要进行高级代码优化 得使用terser等工具

outdir

声明输出文件路径
require('esbuild').buildSync({ entryPoints: ['app.js'], bundle: true, outdir: 'out', })

outfile

声明输出文件, 只在输入的entryPoint是单个文件才有效
require('esbuild').buildSync({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', })

platform

默认情况下 esbuild构建的产物是针对浏览器的, 如果需要针对node, 则需要显式声明platform为node
当 platform 设置为 browser(默认值)时:
  • 默认的输出 格式iife,将生成的 JavaScript 代码包裹在立即执行函数表达式中, 以阻止变量泄露到全局作用域中。
  • 如果一个包在 package.json 文件中 的browser 配置配置了一个 map,esbuild 将会使用该 map 替换指定的文件或模块为对浏览器友好的版本。 例如,例如,一个包可能会用 path-browserify 替换 path
  • main fields 设置为 browser,module,main, 但是会有一些额外的特殊行为。如果你个包支持 modulemain,但是不支持 browser, 那么当使用 require() 导入时,将使用 main 而不是 module。 此行为通过将函数赋值给 module.exports 来改善与导出函数的 CommonJS 模块的兼容性。
  • conditions 设置自动包含了 browser 情况。 这将改变 package.json 文件中 exports 字段如何被解释为偏好特定于浏览器代码的方式。
  • All process.env.NODE_ENV expressions are automatically defined to "production" if all minification options are enabled and "development" otherwise. This only happens if process, process.env, and process.env.NODE_ENV are not already defined. This substitution is necessary to avoid React-based code crashing instantly (since process is a node API, not a web API).
当 platform 设置为 node 时:
  • 默认输出 格式cjs,代表 CommonJS(node 使用的模块格式)。 ES6-风格的导出使用的 export 语句将会被转换为 CommonJS exports 对象中的 getters。
  • 所有诸如 fs内置 node 模块 会被自动标记为 external,因此在打包器尝试打包他们时不会导致错误。
  • main 字段 设置为 main,module。 这意味着 tree shaking 操作可能不会发生在同时提供 modulemain 的包中, 因为 tree shaking 操作只适用于 ECMAScript 模块,而不适用于 CommonJS 模块。
    • 不幸的是,一些包将 module 视为 "browser code" 而不是 "ECMAScript module code", 因此,这种默认行为是兼容性所必需的。如果你想要启用 tree shaking 并且知道这样做是安全的, 那么你可以手动将 main 字段 设置为 module,main
  • conditions 设置自动包含 node 情况。 这将改变 package.json 文件中 exports 字段如何被解释为偏好特定于 node 端代码的方式。
当 platform 设置为 neutral 时:
  • 默认输出 格式esm,使用 ECMAScript 2015 (即 ES6) 中引入的 export 语法。 如果默认值不合适的话你可以改变输出格式。
  • main 字段 默认设置为空。如果你想使用 npm 风格的包, 你可能需要将其配置为其他内容,比如将 node 使用的 main 字段配置为 main
  • conditions 设置不会自动包含任何平台特定值。

serve

interface ServeOptions { port?: number; host?: string; servedir?: string; onRequest?: (args: ServeOnRequestArgs) => void; } interface ServeOnRequestArgs { remoteAddress: string; method: string; path: string; status: number; timeInMS: number; }

sourcemap

source map 的输出支持 JavaScript 和 CSS, 而且二者的配置一致
sorcemap一般有几个配置选项
  1. linked: sourcemap会作为文件输出出来, 并且js文件中包含了//# sourceMappingURL=的注释, 指向这个文件
  1. external: 会输出成为一个独立的.js.map文件, 源文件中不包含//# sourceMappingURL=注释
  1. inline: 会输出到js输出文件的末尾作为一个base64
  1. both: both inlune和external的行为

spliting

code spliting正在实现中, 且值生效于输出为esm的情况下
不启用代码分割, import() 表达式会变成 Promise.resolve().then(() => require())
当期启用代码分割时,你必须使用 outdir 配置输出文件夹。

target

设置构建的目标环境, 传入一个数组, 支持以下环境
  • chrome
  • edge
  • firefox
  • hermes
  • ie
  • ios
  • node
  • opera
  • rhino
  • safari
可以在目标环境后加上版本以支持到目标的版本, 如es2020 node12.19.0等等

watch

write

可以通过设置write: false来阻止默认的写入到outfile的行为
let result = require('esbuild').buildSync({ entryPoints: ['app.js'], sourcemap: 'external', write: false, outdir: 'out', }) for (let out of result.outputFiles) { console.log(out.path, out.contents) }

进阶选项

allowOverwrite

设置为true, 会将构建结果直接覆写entryPoints

analyze

生成一个易读的bundle打包报告
(async () => { let esbuild = require('esbuild') let result = await esbuild.build({ entryPoints: ['example.jsx'], outfile: 'out.js', minify: true, metafile: true, }) let text = await esbuild.analyzeMetafile(result.metafile, { verbose: true, }) console.log(text) })()
如果需要进一步详细信息, 可以额外设置一个verbose: true来展示整个文件的依赖关系

assetNames

当loader设置为file时, 可以通过这个来设置静态产物的名称
require('esbuild').buildSync({ entryPoints: ['app.js'], assetNames: 'assets/[name]-[hash]', loader: { '.png': 'file' }, bundle: true, outdir: 'out', })
主要有如下几个参数
  • [dir]: 相对路径
  • [name]: 原本的文件名称
  • [hash]: 内容的contentHash, 避免文件冲突
  • [ext]: 文件后缀名称

banner

用来在开头插入注释
require('esbuild').buildSync({ entryPoints: ['app.js'], banner: { js: '//comment', css: '/*comment*/', }, outfile: 'out.js', })
对应的还有footer, 用于在尾部插入注释

charset

默认情况下esbuild只处理ASCII, 任何非ASCII的字符都会被转译, 同时这也是性能提升的手段
如果需要, 可以指定固定的charset
let js = 'let π = Math.PI' require('esbuild').transformSync(js) { code: 'let \\u03C0 = Math.PI;\n', map: '', warnings: [] } require('esbuild').transformSync(js, { charset: 'utf8', }) { code: 'let π = Math.PI;\n', map: '', warnings: [] }

chunkNames

同样的设置方式, 类似于assetNames, 支持三个
  • [name]
  • [hash]
  • [ext]

color

启用或关闭有颜色的stdout输出, 默认启用

conditions

条件判断
一般在构建入口处, 需要进行条件判断以加载对应内容
if (importPath === './foo') { if (conditions.has('import')) return './imported.mjs' if (conditions.has('require')) return './required.cjs' return './fallback.js' }
conditions支持五种判断: default import require browser node

drop

可以删除掉代码中的内容, 支持debugger console
require('esbuild').buildSync({ entryPoints: ['app.js'], drop: ['debugger', 'console'], })

entryNames

globalName

当输出为iife的时候, 设置其全局的名称
let js = 'module.exports = "test"' require('esbuild').transformSync(js, { format: 'iife', globalName: 'xyz', }) // 输出内容戴上了xyz的变量 var xyz = (() => { ... var require_stdin = __commonJS((exports, module) => { module.exports = "test"; }); return require_stdin(); })();

ignoreAnnotations

忽略注释

incremental

增量构建

jsx

告诉esbuild如何处理jsx语法

...

插件

插件目前是一个实验性功能
现存插件可以从: https://github.com/esbuild/community-plugins 这里找
一个esbuild的插件是一个包含name和setup函数的对象
一个基本的插件, 用于将node环境下的process.env注入到构建上下文中
let envPlugin = { name: 'env', setup(build) { // Intercept import paths called "env" so esbuild doesn't attempt // to map them to a file system location. Tag them with the "env-ns" // namespace to reserve them for this plugin. build.onResolve({ filter: /^env$/ }, args => ({ path: args.path, namespace: 'env-ns', })) // Load paths tagged with the "env-ns" namespace and behave as if // they point to a JSON file containing the environment variables. build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({ contents: JSON.stringify(process.env), loader: 'json', })) }, } require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', plugins: [envPlugin], }).catch(() => process.exit(1))

概念

namespace

每个module都有一个关联的namespace, 默认情况下esbuild在file namespace下运行, 对应文件系统中的文件.
但esbuild也可以处理virtual模块, 这时候此虚拟模块并没有存在于文件系统中, 例如当我们使用stdin来创建一个虚拟文件
插件可以用来创建虚拟模块, 虚拟模块通常使用一个file以外的namespace来使其对文件模块进行区分
通常一个namespace特定于创建他们的插件, 例如: http插件使用了http-url作为namespace来下载文件

filters

每个回调函数都需要挺高一个正则表达式作为filter, 当filter的正则与路径不匹配时, 可以使用它来跳过回调的调用, 用于提高性能
从esbuild的高度并行内部调用到单线程的js代码是昂贵的, 应该尽可能避免以达到最大速度
尽可能的使用filter的正则来进行匹配而不是js代码, 这样可以提升性能, 因为过滤的正则是在esbuild内部基于go执行的
因为go的正则表达式与js正则表达式的差异性, 正向前瞻和后向前瞻, 反向引用等是不支持的
namespace也可以用来进行过滤. 回调必须提供正则表达式, 但也可选的可以提供一个namespace来进一步缩小匹配范围.

生命周期回调

onResolve回调

参数:
  • filter: 过滤的正则
  • namespace: 可选的, 指定namespace
回调参数
interface OnResolveArgs { path: string; // 当前模块的路径 importer: string; // 导入了当前模块的路径(父路径) namespace: string; // 解析此导入模块的命名空间, 由加载此文件的加载回调(onLoad)设置 resolveDir: string; // 文件的真实文件系统地址 kind: ResolveKind; // 如何导入要解析的路径 pluginData: any; // 前一个插件(onLoad)传递的数据 } type ResolveKind = | 'entry-point' // 入口 | 'import-statement' // 由import导入 | 'require-call' // 使用import导入的 | 'dynamic-import' | 'require-resolve' | 'import-rule' // 使用@import导入的css | 'url-token'
回调返回参数
interface OnResolveResult { errors?: Message[]; external?: boolean; // 设置为true, 将会排除在打包流程外, 直接使用此模块 namespace?: string; // 如果为空会被设置为file, 如果是file, 那么path就必须是一个当前文件系统中的绝对路径 path?: string; // 如果有值, 后续的onResolve就不会再执行, 否则会继续执行后续的onResolve, 如果onResolve执行完了, esbuild会提供一个默认的解析路径 pluginData?: any; // 透传到下一个plugin的参数 pluginName?: string; // 返回其他的pluginName, 让后续用其他plugin来解析 sideEffects?: boolean; // 设置为false表示这个模块没有被使用到的import内容可以被删除 suffix?: string; warnings?: Message[]; watchDirs?: string[]; // 用于返回额外的文件系统路径供监听, 默认情况下esbuild知会箭筒namespace为file和onLoad插件提供的文件路径, 如果需要增加的话, 就需要返回这个内容了 watchFiles?: string[]; // 用于返回额外的文件路径供监听 } interface Message { text: string; location: Location | null; detail: any; // The original error from a JavaScript plugin, if applicable } interface Location { file: string; namespace: string; line: number; // 1-based column: number; // 0-based, in bytes length: number; // in bytes lineText: string; }
每次模块有使用import进行路径导入时都会执行
onResolve则可以自定义esbuild如何进行路径解析. 例如: 它可以拦截导入的路径path, 然后将其设置为其他的路径. 同时也可以标记路径为external
let exampleOnResolvePlugin = { name: 'example', setup(build) { let path = require('path') // Redirect all paths starting with "images/" to "./public/images/" build.onResolve({ filter: /^images\// }, args => { return { path: path.join(args.resolveDir, 'public', args.path) } }) // Mark all paths starting with "http://" or "https://" as external build.onResolve({ filter: /^https?:\/\// }, args => { return { path: args.path, external: true } }) }, } require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', plugins: [exampleOnResolvePlugin], loader: { '.png': 'binary' }, }).catch(() => process.exit(1))
回调可以返回而不提供将路径解析的责任传递给下一个回调的路径. 对于给定的import路径, 所有的onResolve回调将会按照他们注册的路径执行, 直到有人负责路径解析.
如果没有callback返回一个路径, 则esbuild将会运行内置的path解析逻辑
需要注意的是, 许多的callbacks会同时执行, 在js中, 我们可以使用async/await来使其变成同步的, 以保证其他代码能够同时运行. 在go中, 每个callback将会通过一个分开的goroutine来执行

onLoad回调

参数:
  • filter: 过滤的正则
  • namespace: 可选的, 指定namespace
回调参数
interface OnLoadArgs { path: string; // 如果namespace是file的时候, 是绝对路径, 也有可能是别的, 例如一个http地址 namespace: string; // onResolve环境设置的命名空间, 默认为file suffix: string; pluginData: any; }
回调返回参数
interface OnLoadResult { contents?: string | Uint8Array; // 如果有内容返回, 后续的onLoad回调就不会在执行, 否则会继续执行后续的onLoad回调, 直到使用默认的 errors?: Message[]; loader?: Loader; // 告诉esbuild如何进行解析, 默认是js loader pluginData?: any; pluginName?: string; resolveDir?: string; warnings?: Message[]; watchDirs?: string[]; watchFiles?: string[]; } interface Message { text: string; location: Location | null; detail: any; // The original error from a JavaScript plugin, if applicable } interface Location { file: string; namespace: string; line: number; // 1-based column: number; // 0-based, in bytes length: number; // in bytes lineText: string; }
每个唯一的path/namespace对如果没有标记为external, 都会调用它. 它的工作是返回模块的内容, 并告诉esbuild如何解释它
let exampleOnLoadPlugin = { name: 'example', setup(build) { let fs = require('fs') // Load ".txt" files and return an array of words build.onLoad({ filter: /\.txt$/ }, async (args) => { let text = await fs.promises.readFile(args.path, 'utf8') return { contents: JSON.stringify(text.split(/\s+/)), loader: 'json', } }) }, } require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', plugins: [exampleOnLoadPlugin], }).catch(() => process.exit(1))

在onLoad中使用缓存

let examplePlugin = { name: 'example', setup(build) { let fs = require('fs') let cache = new Map build.onLoad({ filter: /\.example$/ }, async (args) => { let input = await fs.promises.readFile(args.path, 'utf8') let key = args.path let value = cache.get(key) if (!value || value.input !== input) { let contents = slowTransform(input) value = { input, output: { contents } } cache.set(key, value) } return value.output }) } }

onStart回调

注册onStart回调可以在一个新的构建开始时获得通知. 每次构建都会触发一次, 而并非初始的构建
let examplePlugin = { name: 'example', setup(build) { build.onStart(() => { console.log('build started') }) }, }
onStart回调可以是异步的, 当所有onStart回调执行完成, 才会启动构建步骤
onStart阶段无法修改构建的参数与选项

onEnd回调

注册onEnd回调可以在每个构建结束之后获得通知
let examplePlugin = { name: 'example', setup(build) { build.onEnd(result => { console.log(`build ended with ${result.errors.length} errors`) }) }, }

访问构建参数

插件可以在setup函数中访问到构建参数, 在这里我们可以查看到构建的配置, 并按需来在启动时修改构建参数
let examplePlugin = { name: 'auto-node-env', setup(build) { const options = build.initialOptions options.define = options.define || {} options.define['process.env.NODE_ENV'] = options.minify ? '"production"' : '"development"' }, }

路径解析

当我们使用插件来修改onResolve的返回路径的时候, 后续的路径解析相关的内容就需要我们自己来实现了, 但是我们也可以调用esbuild的默认解析方法来使用esbuild的默认方式来进行路径解析, 以方便我们进行进一步操作
let examplePlugin = { name: 'example', setup(build) { build.onResolve({ filter: /^example$/ }, async () => { const result = await build.resolve('./foo', { resolveDir: './bar' }) if (result.errors.length > 0) { return { errors: result.errors } } return { path: result.path, external: true } }) }, } require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', plugins: [examplePlugin], }).catch(() => process.exit(1))

插件限制

esbuild不允许修改ast, 以便暴露更少的api和提升构建性能
尽可能少的使用插件, 插件越多, 插件影响范围越大, 构建速度越慢. 同时, js的插件会比go的插件更慢

总结

esbuild的插件整个流程到这里就很清晰了:
  1. 插件有name, setup方法, esbuild在初始化的时候会调用setup方法传入构建的options来初始化整个构建流程, 同时注册对应的生命周期回调
  1. 首先, 会按顺序调用onResolve回调来解析导入内容, 处理导入的路径关系, 当文件匹配到对应的正则关系时会触发对应回调, 直到有path的输出为止
  1. 接下来会走到onLoad回调, onLoad回调会获取路径path, 根据路径path来解析出内容, 输出content, 也就是这个路径对应的内容文本或buffer
  1. 接下来会执行onStart回调, 代表构建开始
  1. 最后会执行onEnd回调, 代表构建结束, 回调的参数为最终的构建结果

番外

1. 如何基于esbuild实现ddl的功能

基本实现思路是:
  1. 通过虚拟文件来引入和全局注册需要进行预打包的包依赖, 同时在window上注册对应包的变量
  1. 通过esbuild来对其进行预打包, 生成文件
  1. 使用esbuild插件来对预打包的内容进行排除, 并打包代码
  1. 提前引入ddl打包的内容至html文件中
const isProduction = process.env.NODE_ENV == 'production' const contents = `window.React = require('react') window.ReactDOM = require('react-dom') window.mobx = require('mobx') window.mobxReact = require('mobx-react') ` require('esbuild').buildSync({ stdin: { contents, sourcefile: 'react-build.js', loader: 'js', resolveDir: __dirname }, define: {'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')}, bundle: true, format: 'iife', minify: isProduction, sourcemap: false, target: ['chrome58'], outfile: './lib/react.js', })
const reactPlugin = { name: 'react', setup(build) { build.onResolve({filter: /^(react|react-dom|mobx|mobx-react)$/}, args => { return { path: args.path, namespace: 'react-ns' } }) build.onLoad({filter: /^react$/, namespace: 'react-ns'}, () => { return { contents: 'module.exports = React' } }) build.onLoad({filter: /^react-dom$/, namespace: 'react-ns'}, () => { return { contents: 'module.exports = ReactDOM' } }) build.onLoad({filter: /^mobx$/, namespace: 'react-ns'}, () => { return { contents: 'module.exports = mobx' } }) build.onLoad({filter: /^mobx-react$/, namespace: 'react-ns'}, () => { return { contents: 'module.exports = mobxReact' } }) } } // esbuild 同步方式不支持 plugin async function start() { let esbuild = require('esbuild') await esbuild.build({ entryPoints: ['index.js'], target: ['chrome58'], bundle: true, outdir: 'lib', define: {'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')}, plugins: [reactPlugin], tsconfig: 'tsconfig.json' }) } start()

2. 如何自定义loader

esbuild官方提供了各种loader, 但对于无法支持的loader, 则需要自己来实现, 而esbuild并没有提供loader的实现, 如果需要实现类似的功能, 则只能以比较tricky的方式, 使用plugin来实现, 例如
let etPlugin = { name: 'et', setup(build) { build.onResolve({filter: /.*\.et$/}, args => { let p = path.relative(__dirname, args.resolveDir) return { path: path.posix.join(p, args.path), namespace: 'et' } }) build.onLoad({filter: /.*/, namespace: 'et'}, args => { // 读取文件生成 js 函数 let fn = et.compileFile(args.path, {debug: !isProduction}) return { contents: `module.exports = ${fn.toString()}` } }) } }
这样就可以直接在 js 文件中 require 后缀为 et 的模板文件。

3. 参考

还有自定义入口, 自定义loader, 重编译等 , 可参考: https://zhuanlan.zhihu.com/p/342336095
工程化
  • Esbuild
  • Rollup概念与运行原理包管理工具的提速与演进
    目录