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会帮我们设置一种输出格式, 输出格式一般有以下几种
- iife
- cjs
- 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());
一般在以下几种场景下很有用
- 自动在jsx中引入React.creatElement
- 统一在文件中注入一段文件, 我们可以不用担心重复注入的问题, esm会自动帮助我们处理
- 条件性注入一个文件
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中一般有三种类型, 可以对其分别设置
- 压缩空格
- 压缩变量
- 压缩语法
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: [] }
需要注意的是:
- esbuild的minify的产物是针对现代浏览器的, 例如:
a === undefined || a === null ? 1 : a
将会被压缩为a ?? 1
, 如果这不是期望行为, 需要设置target为es6-target=es6
- 转义符
\n
将会被替换成为模板字符串的换行符, 以节省占用字节大小
- 默认情况下esbuild不会压缩顶级声明的变量, 但当我们设置了format或bundle为true的时候, esbuild会认为压缩顶级声明变量是安全的, 而启动定义声明变量的压缩
- 压缩不是百分之百安全的, 即使是其他受欢迎的压缩工具例如terser也一样, 特别是在对于
.toString()
的处理上, 如果不去处理这类的内容压缩带来的效果就变得很小了, 这里建议的是使用明确的注释替代
- 默认情况下, esbuild不会保存函数和类的
.name
属性, 如果确实需要 可以设置keepNames为true
- 使用eval或with等将会使得许多优化失效导致整个文件无法被压缩处理, 一般建议不直接使用eval, 而是通过
let result = (0, eval)(something)
以防止对应的副作用产生
- 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
, 但是会有一些额外的特殊行为。如果你个包支持module
与main
,但是不支持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 ifprocess
,process.env
, andprocess.env.NODE_ENV
are not already defined. This substitution is necessary to avoid React-based code crashing instantly (sinceprocess
is a node API, not a web API).
当 platform 设置为
node
时:- 默认输出 格式 为
cjs
,代表 CommonJS(node 使用的模块格式)。 ES6-风格的导出使用的export
语句将会被转换为 CommonJSexports
对象中的 getters。
- 所有诸如
fs
的 内置 node 模块 会被自动标记为 external,因此在打包器尝试打包他们时不会导致错误。
- main 字段 设置为
main,module
。 这意味着 tree shaking 操作可能不会发生在同时提供module
和main
的包中, 因为 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一般有几个配置选项
- linked: sourcemap会作为文件输出出来, 并且js文件中包含了
//# sourceMappingURL=
的注释, 指向这个文件
- external: 会输出成为一个独立的
.js.map
文件, 源文件中不包含//# sourceMappingURL=
注释
- inline: 会输出到js输出文件的末尾作为一个base64
- 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的插件整个流程到这里就很清晰了:
- 插件有name, setup方法, esbuild在初始化的时候会调用setup方法传入构建的options来初始化整个构建流程, 同时注册对应的生命周期回调
- 首先, 会按顺序调用onResolve回调来解析导入内容, 处理导入的路径关系, 当文件匹配到对应的正则关系时会触发对应回调, 直到有path的输出为止
- 接下来会走到onLoad回调, onLoad回调会获取路径path, 根据路径path来解析出内容, 输出content, 也就是这个路径对应的内容文本或buffer
- 接下来会执行onStart回调, 代表构建开始
- 最后会执行onEnd回调, 代表构建结束, 回调的参数为最终的构建结果
番外
1. 如何基于esbuild实现ddl的功能
基本实现思路是:
- 通过虚拟文件来引入和全局注册需要进行预打包的包依赖, 同时在window上注册对应包的变量
- 通过esbuild来对其进行预打包, 生成文件
- 使用esbuild插件来对预打包的内容进行排除, 并打包代码
- 提前引入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