用esbuild实现前端构建速度飞跃的实战经验分享

Dev · 爱玲 优化 阅读 2,430
赞 4 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我最近接手了一个老项目,构建时间从原来的十几秒飙到了快一分钟。Webpack 那套配置已经复杂到没人敢动,每次改点东西都得祈祷别出问题。直到我试了 esbuild —— 你猜怎么着?构建时间直接干到了 800ms。不是优化后的 Webpack,就是原生 esbuild,啥插件都没加。当时我就一个念头:早用这玩意儿能少熬多少夜。

用esbuild实现前端构建速度飞跃的实战经验分享

下面这个是最基础的打包命令,我建议你直接复制跑一遍感受下速度:

npx esbuild src/index.js --bundle --format=esm --outdir=dist

就这么一行,搞定打包、模块转换、输出目录。不需要写配置文件,不用搞什么 webpack.config.js 写一页纸。esbuild 的 CLI 就是这么暴力直接。

如果你习惯写配置文件,也可以整一个 build.js

const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  format: 'esm',
  outdir: 'dist',
  sourcemap: true,
  minify: true,
}).catch(() => process.exit(1));

运行方式也很简单:

node build.js

亲测有效,而且比 CLI 更容易加逻辑。比如你想根据不同环境输出不同格式,直接在 JS 里判断就行,比写一堆 npm scripts 干净多了。

这个场景最好用

说实话,esbuild 最让我爽的地方不是打包应用,而是处理那些“小而碎”的构建需求。比如我现在有个需求:把一个工具函数单独打包成 UMD 格式,供别人通过 script 标签引入。

这种事用 Webpack 太重了,还得配 output.library、libraryTarget,一通操作下来半小时过去了。但 esbuild 几行代码搞定:

esbuild.build({
  entryPoints: ['src/utils.js'],
  bundle: true,
  format: 'umd',
  globalName: 'MyUtils',
  outfile: 'dist/utils.js',
  minify: true,
}).catch(() => process.exit(1));

打完之后生成的文件可以直接扔到 HTML 里用:

<script src="dist/utils.js"></script>
<script>
  console.log(MyUtils.formatDate(new Date()));
</script>

注意这里的 globalName,它会决定你在全局挂载的对象名。我之前没设这个,结果默认挂到 window.exports 上去了,调用的时候一脸懵,折腾了半天才发现是配置漏了。

处理 CSS 也能行,但别指望太多

esbuild 原生支持导入 .css 文件,但它不会帮你做 PostCSS、autoprefixer 这些事情。比如你写了:

import './styles/main.css';

esbuild 会把这个文件当成字符串塞进 bundle,然后在运行时动态插入 style 标签。听起来不错,对吧?但问题来了 —— 它不支持 CSS Modules,也不支持 Sass/Less。

所以我的做法是:只让 esbuild 处理 JS/TS,CSS 单独用别的工具走一遍。如果非要用 esbuild 处理样式,可以试试社区插件,比如 esbuild-plugin-less,但稳定性一般,小项目可以玩,生产环境慎用。

如果你只是想简单合并几个 CSS 文件,可以用下面这种方式:

esbuild.build({
  entryPoints: ['src/main.css'],
  outfile: 'dist/bundle.css',
  loader: { '.css': 'copy' },
});

不过 loader 的 copy 功能其实是实验性的,官方文档都没正式写进去。我踩过一次坑:路径别名 alias 在 copy 模式下不生效,最后只能手动 resolve 路径,挺麻烦的。

踩坑提醒:这三点一定注意

  • Node.js 内置模块兼容问题:你在代码里用了 process.env.NODE_ENV 或者 Buffer,esbuild 默认不会给你 polyfill。打包出来在浏览器跑直接报错。解决方案有两个:一是自己定义 define:

    define: {
      'process.env.NODE_ENV': '"production"',
      'global': 'window',
      'Buffer': 'buffer'
    }

    二是配合 external 把 node 内建模块排除,再手动引入 shim。我个人建议前者,简单粗暴。

  • 动态 import() 的 chunk 名控制不了:你用 import(./modules/${name}.js) 这种动态导入时,esbuild 会自动生成 chunk 文件名,比如 chunk.BXK23OAE.js。没法像 Webpack 那样用魔法注释指定名字。这不是大问题,但如果要做资源预加载或者监控上报,解析文件名就有点难受。目前没有解法,只能接受。
  • alias 别名必须写全,不然会找不到文件:比如你配置了:

    alias: {
      '@': './src'
    }

    那你引用的时候必须写 @/utils,不能写 @utils,连掉斜杠都不行。更坑的是,它不会报错,而是直接当成外部依赖处理,最后包里少了这个模块,运行时报 undefined。我在这里卡了将近两小时,以为是缓存问题,清了三四遍才意识到是 alias 写错了。

高级玩法:写个自己的构建脚本

我一直觉得,构建工具最怕的就是“配置即代码”。esbuild 允许你在 JS 里完全控制构建流程,这点特别适合写一些自动化任务。比如我现在每天要给内部系统打一个带版本号的包,我写了个简单的 build-with-version.js:

const esbuild = require('esbuild');
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

const version = pkg.version;

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  format: 'iife',
  outfile: 'dist/bundle.js',
  minify: true,
  define: {
    '__APP_VERSION__': &quot;${version}&quot;,
    'process.env.NODE_ENV': '"production"'
  },
  plugins: [{
    name: 'version-inject',
    setup(build) {
      build.onLoad({ filter: /.js$/ }, async (args) => {
        const contents = await fs.promises.readFile(args.path, 'utf8');
        return {
          contents: console.log(&#039;v${version}&#039;);n${contents},
          loader: 'js'
        };
      });
    }
  }]
}).catch(() => process.exit(1));

你看这个 plugin,我在每个入口文件前面自动插入一行版本打印。虽然简单,但在排查线上问题时特别有用。以前都是手动改,现在一键搞定。而且这种脚本还能接 CI,push 一下自动打个带 commit hash 的包,省心得很。

插件机制虽然不如 Webpack 强大,但对于大多数轻量需求完全够用。尤其是 onLoadonResolve 这两个钩子,能让你拦截文件读取和路径解析,做一些很骚的操作,比如 mock 接口数据:

plugins: [{
  name: 'mock-api',
  setup(build) {
    build.onLoad({ filter: //api// }, () => ({
      contents: export default { data: &#039;mocked&#039;, status: 200 },
      loader: 'js'
    }));
  }
}]

这样所有请求 https://jztheme.com/api/user 的 import 都会被替换成 mock 数据,本地开发调试神器。当然,上线前记得关掉。

总结一下

esbuild 不是万能的,它不适合那种超复杂的前端工程,比如有几十个微前端、各种定制 loader 的项目。但如果你是中小型项目,或者想快速搭个 demo、工具库、内部系统,那它真的香到爆。

我现在的新项目基本都用 esbuild 打底,搭配一些轻量插件解决特定问题。复杂功能交给后端或服务端渲染,前端就专注快和稳。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流 —— 尤其是怎么优雅处理 CSS 的,我现在这方案还是太糙了。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论