前言
最近在开发公司的一个处于发展初期的项目,简单说就是一个monorepo的库,那么选择打包工具是必然要做的事情。
作为一个npm包,webapck
是必然不在考虑范围内了,另外一个选择是rollup
,但最后选择了esbuild
。
why esbuild
最直接的原因是快(在官网截了一张图)。仔细推演了之后,发现esbuild并没有什么不满足需求的地方,就直接上了。
实际使用过程中遇到的问题
前面有提到,我们的项目是一个monorepo,使用了yarn workspaces
和 lerna
,并且纯typescript
。
项目结构如下
1. 打包
每个子包都需要build一次,这里可以有两种做法。
1.1 做法a
yarn workspaces run build
这种做法我们需要先在每个子包的package.json
中写好build的脚本,个人比较讨厌这种方式,缺点很明显,当需要改动build脚本时,每个子包的package.json
都要修改一次。而且不是js,不够灵活。
1.2 做法b
在根目录下新增一个build.js
,在文件内完成build的整个流程,大概的代码如下。
const { build } = require('esbuild');
const path = require('path');
const fs = require('fs');
const packages = fs.readdirSync(path.join(__dirname, 'packages'));
const createBuildConfig = (targetPath) => {
const config = {
entry: path.join(targetPath, 'src/index.ts'),
outfile: path.join(targetPath, 'dist/index.js'),
bundle: true,
minify: true,
target: 'es2015',
format: 'esm',
};
return config;
};
packages.forEach(package => {
const targetPath = path.join(__dirname, 'packages', package);
const config = createBuildConfig(targetPath);
build(config);
});
2. d.ts怎么办
一个typescript
项目最终要提供d.ts
出来给外部用,但是esbuild
最终build出来的内容中并没有d.ts
,因此我们要单独运行tsc
,稍微修改一下上面的代码,build前先生成d.ts
// ...
const childProcess = require('childProcess');
// ...
packages.forEach(package => {
const targetPath = path.join(__dirname, 'packages', package);
// 因为只需要d.ts,因此加上--emitDeclarationOnly
childProcess.execSync('tsc --emitDeclarationOnly', {
cwd: targetPath,
})
const config = createBuildConfig(targetPath);
build(config);
});
3. jsx
默认情况下,jsx语法最终会编译成React.createElement(tag, props, child)
。
如果不使用React,如何将JSX遍历成自定义的createElement
?在build的选项中新增如下配置
build({
// ...
jsxFactory: 'selfCreateElement',
jsxFragment: 'Fragment',
})
如果使用了tsx,上面的配置可以添加到tsconfig.json
的compilerOptions
中。
即使如此,我们还需要在每个使用到jsx的文件顶部新增一行
import { selfCreateElement } from './selfCreateElement';
为了解放我们的双手,esbuild提供了inject
,这样我们就不需要每一个jsx都引入selfCreateElement
了
// jsx-shim.js
export { selfCreateElement } from 'xxx';
// build.js
build({
// ...
jsxFactory: 'selfCreateElement',
jsxFragment: 'Fragment',
inject: ['jsx-shim.js'],
})
如果不想用inject,也可以写一个插件去动态插入各种代码片段,举个例子(非可运行代码)。
const plugin = {
name: 'example',
setup(build) {
let svelte = require('svelte/compiler')
let path = require('path')
let fs = require('fs')
build.onLoad({ filter: /\.(jsx|tsx)$/ }, async (args) => {
// Load the file from the file system
let source = await fs.promises.readFile(args.path, 'utf8')
// Convert Svelte syntax to JavaScript
try {
const contents = 'import { selfCreateElement } from "xxx";' + source;
return { contents };
} catch (e) {
return { errors: [convertMessage(e)] }
}
})
}
}
require('esbuild').build({
// ...
plugins: [plugin],
});
4. 热更新
我们的项目虽然是一个库,但是最终还是要在web上用的,web上的打包还是使用的webpack
,热更新最终还是走的webapck-dev-server
。
esbuild本身有提供serve
选项,但看了一下,这个不太符合我们的需求,它会占用一个端口,而且生成的文件只存在于内存中。
因此,我们需要自己去watch
文件的改动,然后重新build。
watch的功能自己去实现一套成本略大
找到了一个相对比较适合的库 github.com/rsms/estrel… ,estrella
基于esbuild,自己封装了一层,并且用chokidar
自己实现了watch,此外还有type check。
最终代码
// build.js
const { build } = require('estrella');
const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');
const packages = fs.readdirSync(path.join(__dirname, '../packages'));
const CONFIG_FILE_NAME = 'esbuild.config.js';
const createBuildConfig = (targetPath) => {
let config = {
entry: path.join(targetPath, 'src/index.ts'),
outfile: path.join(targetPath, 'dist/index.js'),
bundle: true,
minify: false,
target: 'es2015',
format: 'esm',
tslint: true,
};
const configPath = path.join(targetPath, CONFIG_FILE_NAME);
if (process.env.mode === 'development') {
config.sourcemap = 'inline';
}
if (fs.existsSync(configPath)) {
// 读取子包esbuild.config.js 配置
const selfConfig = require(configPath);
config = {
...config,
...selfConfig,
};
}
return config;
};
packages.forEach(package => {
const targetPath = path.join(__dirname, '../packages', package);
const config = createBuildConfig(targetPath);
childProcess.execSync('tsc --emitDeclarationOnly', {
cwd: targetPath,
});
build(config);
});
esbuild的缺点
目前个人感受最深的就是,不够灵活,官方文档也提到,不支持ast操作,也就是开发者需要操作ast时,需要自己去处理ast。另外的就是生态一般,完全比不上webpack。