WebPack 的替代品 EsBuild

WebPack 是我一直不是很喜欢但又不得不用的一个工具。主要原因:

  1. 配置很奇怪 : 尤其是前几年升到5后,还要引一些 plugin 去 licence,经常看到一些几百行的 webpack.config。
  2. 性能也很慢 : 虽然有一些非主流的工具比它快一点,但基实提升并不明显,没有成为主流之前,风险大于收益。
  3. 生态太完善 : 这个应该是优点才对,但对于一个工具,不干本质工作,什么都想搞,导致团队里面很多人过度滥用,引一大堆的 loader,plugin,进一步放慢打包速度,且严重降低代码可读性。

最近发现了一个新的工具:esbuild ,主要特性一个字

esbuild 的速度

EsBuild : https://esbuild.github.io/

当然,眼见为虚,Run了才为实

速度对比

手头有两个项目,参考 esbuild 的文档,花不了一个小时,项目成功从 webpack 改造成 esbuild。

  • 一个比较重,用 webpack 启动就要半分钟,watch 每改一下1秒多。
  • 一个比较轻,用 webpack 启动也要等5到8秒,watch 改一下 0.3秒左右,很快,但也有肉眼可见的延时。

由于差距实在太大,就没有太大必要去看具体多少毫秒了,但实际上,这要的对比对于二者都有些不公平。

webpack 的 watch 我用的是 development 模式,速度快于 noneproduction 但实际上是通过 eval 方法执行,webpack 仅做了简单的拼接操作。

而 esbuild 默认的模式就已经相当于 webpack 里面的 none ,还有一个 minify 模式相当于 production , 从这点上来看, esbuild 又超出了 webpack 一档。

但是,esbuild 没有了 lint 的检查功能,如果一段 typesciprt 格式有错(VSCode 里面出现了红线), webpack 会报错,而 esbuild 会支持编译成功。

当然也不是完全没有检查,对于基本的 es语法错误,包引用错误,esbuild 还是会报错的。

有这个工具替换后,每天打开电脑,心情都会美丽一些。

安装

npm install --save-dev esbuild

虽然 esbuild 有自带的命令行,但不想增加认知压力。鉴于项目使用了 gulp ,就直接给 gulpfile 加 task 就可以了。

在 gulpfile 里面这样写,这样可以用 gulp watch 代替 webpack -w 了,还省了一个 webpack config 文件。

import * as gulp from 'gulp'
import * as esbuild from 'esbuild';
import * as _ from 'lodash';

const js = cb => {
    let watch = cb ? false : {
        onRebuild: (error, result) => {
            if (error) {
                console.error('build error', error)
            } else {
                console.log('build success')
            }
        }
    }
    esbuild.build({
        entryPoints: ["./src/index.ts"],
        outfile: './public/js/out.js',
        bundle: true,
        minify: cb ? true : false,
        platform: 'browser',
        watch,
    }).then(result => {
        if (watch) {
            console.log('Start Watching...')
        } else {
            console.log(result)
            cb();
        }
    }).catch(err => {
        console.error(err);
        throw (err);
    })
}

export default gulp.parallel(js, ...);

export const watch = () => js(false);

特性

除了快,还有很多不错的特性

  1. 支持 es6 和 CommonJS 模式
  2. 支持编程与命令行接口,编程支持 nodejs 和 golang
  3. 原生支持 typescript, tsx,jsx等,不再需要 ts-loader,babel-loader 等了。
  4. 可以生成 Source maps文件
  5. 支持压缩,相当于 production 模式
  6. 支持插件扩展

直接支持的类型

  1. js 、 jsx、 mjs 、 cjs
  2. ts 、 tsx、 mts 、 cts
  3. json
  4. css
  5. 文本 默认后缀为 .txt
  6. 二进制 默认不开启,需要指定后缀,如 –loader:.data=binary
  7. base64 默认不开启,需要指定后缀,如 –loader:.data=base64
  8. 转base64数据链接,默认不开启,需要指定后缀,如 –loader:.png=dataurl
  9. 文件地址链接,默认不开启,需要指定后缀,如 –loader:.png=file

依赖 external

在之前 webpack 的项目中,由于页面多,于是把一此通用的依赖抽了出来,像这样:

externals: {
  "lodash": "_",
  "dat.gui": "dat",
  "three": "THREE"
}

esbuild 也提供了类似的参数

esbuild.build({
    ...,
    external: ['three', 'lodash', 'dat.gui'],
})

编译后,发现不灵了。

目前我的解决方案是 : 自己写个 require 方法去兼容, 不清楚有没更好的方法。

<script src="js/three.min.js"></script>
<script src="js/lodash.min.js"></script>
<script src="js/dat.gui.min.js"></script>
<script>
    const require = function (mod) {
        switch (mod) {
            case 'dat.gui':
                return dat;
            case 'three':
                return THREE;
            case 'lodash':
                return _;
        }
    }
</script>

小结

  1. esbuild 速度上优势明显,但功能上不能完全取代 webpack,特别是在 webpack 生态耕耘了的项目来说更是难以迁移。
  2. 另外不支持 typescript 的lint 感觉有些小麻烦(只是简单使用,我也不知道,可能可以通过扩展支持说不定。)
  3. 由于我用 react 可以原生支持,但 vue 的项目(.vue后缀)不知道如何解决,应该也有方案,没细研究。
  4. 还有,现在的插件也比较少,需要自己写,例如一个 envPlugin 插件就是一个方法,也不是很困难。
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))