大家好,我是Fly, 最近研究的技术就是esbuild,一个新技术的 出现必然是解决是某些问题的, 这不我就打算用它来进行项目的编译速度进行操刀了, 前端对于tsx 的编译方式 无非是两种,
- 第一种就是 使用我们 很熟悉的babel 配合对应的presets 「@babel/preset-typescript」 然后进行 编译转换js
- 第二种 就是 由社区提供的一些loader, 对于tsx 的文件进行编译, 比如 「ts-loader」
- 还有一些比较新的技术, 「可以在开发环境玩玩, 生产环境慎用」, 比如使用swc 和 esbuild 对tsx 文件进行编译。还是看公司的业务场景,esbuild 没法编译成es5 ,没法做类型检查, 然后对于ts的一些语法不支持,等缺点。
esbuild
esbuild 号称下一代前端的打包工具,我们看一张图就是知道他的速度了
对于three.js 项目的打包 esbuild 的打包速度,竟然是webpack5 的100 倍都不止, 这速度看了谁不喜欢???多快哦, 看下esbuild支持的特性:
- 极速,无需缓存, 这里就是 在现阶段的打包的过程中, 对于AST 的分析, 反复去做, 每一个工具链, 可能都会利用AST 做一点事情,这就会导致大量的内存占用。而esbuild共用同一份AST 节点数据,提高内存利用率。
- 使用go 语言开发, 充分利用cpu 多核的优势, 使用多线程打包, 线程之间共享数据。
- esbuild的解析器可以同时处理ES6和CommonJS的模块。它不会去区分ES6和其他的模块,因此可以让你在同一个文件中同时使用ES6和CommonJS的语法。随着esm 的流行,未来可能是一种趋势, 但是老的包有的是基于commonJS 的, 要把common js 类型的包 转成esm, 或者esm 转换成 commonjs. esm的模式 打包出来的bundle, 更加高效, 运行时开销也小。
- 还有 作用域提升 已经 tree-shaking 这些特性
swc
swc 本身工具的定位有类似于 babel, 不过是基于rust 写的,esbuild 定位有点类似于webpack。我们看下图:
越高表示越好, swc 的性能是 babel、typescript、esbuild 在es2015 语法 6倍多, 由于这是官网的截图, 没有经过实际项目验证, 不过我说一个数据, 对于大型react 项目, 我使用esbuild-loader 和 swc-loader 构建的速度, 整体速度的差距是不大的。swc不是本篇文章的重点, 接下来继续分析esbuild
esbuild-loader
既然esbuild, 辣么快, 那么社区肯定有人就会想到, esbuild 如何与当前前端工程化项目去做结合, 提升开发体验。现阶段我所知道的有下面这些
- esbuild-jest 检测单元测试, 使用esbuild的能力 来缩短跑单侧的时间
- esbuild-eslint 我们项目由于eslint 的规则 十分多, 子路由大概10多个, 模块关系依赖复杂,跑一次lint, 时间都是非常慢, 如果借助esbuild 的能力,美滋滋。
- 还有一个就是编写一个loader 去解析tsx, 解析tsx这个loader, 在webpack 的编译时间, 往往都是十分耗时的,优化这段部分,整个webpack的启动时间就会大幅度加快。
目前所做的都是开发环境的优化, 没有在生产环境去操作过, 如果使用,请慎重,等vite 啥时候生产环境 不采用rollup ,保持开发环境 和生产环境的打包方式一致了, vite 的项目 还是具有代表性的, 唯一的担心, 就是esbuild 打包出来的bundle 和 webpack 打包出来的bundle 不一致, 出现问题就背大锅了, fly 建议 内部项目 或者个人学习项目, 在技术层面可以激进点,坏了, 可以修, 但是线上项目,服务很多用户的, 还是要慎重。所以现阶段 webpack 任然是最棒的工具, 繁荣的社区, 几乎问题都可以解决,项目没有升级webpack5的, wepack5 的持久化缓存, 模块联邦、lazyCompilation、 都是令人惊艳的重大更新。
好的下面我们就开始编写一个webpack 的loader, 本篇文章不会详细介绍loader 的编写不是本文的重点, 不会编写webpack loader的同学, 请自行学习。「esbuild-import-loader」 主要是有下面有两个功能
- 第一个 使用esbuild 进行编译tsx
- 实现组件库的按需加载格式
安装相关依赖
主要的依赖有
- esbuild 这个主要是核心依赖, 主要利用 transform 这个api esbuild 提供的AST 接口少之又少, 如果你的项目重度依赖 babel , 那么在技术选型的过程中, 不建议使用 esbuild, 因为esbuild, 提供的AST 接口很少, esbuild 的作者本身 追求极致性能, 你可以使用swc, swc 你可以理解为 更快一点的 babel, 不过是基于rust, 相对于js 有着天然的优势 。可以看下官网的移植指南,结合项目综合去考虑。
- 由于esbuild, 没有提供AST 的接口, 所以对于AST 的 处理 我们 还是使用babel 相关的包 去做处理。 「@babel/parser , @babel/traverse, @babel/type @babel/generator」
定义接口类型
关于接口的设计其实很简单, 在 esbuild Transform 接口的类型上, 进行增加
import { TransformOptions } from 'esbuild' export type EsbuildImportLoader = Omit<TransformOptions, 'sourcemap' | 'sourcefile'> & { libraryName: string customStyle?: (importName: string) => string customName: (importName: string) => string }
T除了 sourcemap 和 sourcefile 这两个属性
「sourcefile」 这个属性:
当使用没有文件名的输入时,此选项设置文件名。当使用转换 API 和使用带有标准输入的构建 API 时会发生这种情况。配置的文件名反映在错误消息和源映射中。如果未配置,则文件名默认为
let fs = require('fs') let js = fs.readFileSync('app.js', 'utf8') require('esbuild').transformSync(js, { sourcefile: 'example.js', sourcemap: 'inline', })
sourcemap 就是我们前端理解的 源码映射, 我们写loader 完全没必要 由外面传了,
直接通过loader 的上下文拿对应的map其实就可以了。
TransformOptions 到底有哪些 类型呢 ???
有很多类型, 感兴趣的同学 去 这个网址 进行 查看 https://esbuild.github.io/api/#sourcemap
我选几个有代表性的属性 进行讲解:
Format
定义转换的文件 格式: 有 iife、commonjs 、esm。我们一一进行试验。
IIFE
let js = 'alert("我是FLY")' let out = require('esbuild').transformSync(js, { format: 'iife', }) process.stdout.write(out.code)
我们执行 ts-node 看控制台的输出
CommonJS
继续实验commonjs 的
let js = 'export default "test"' let out = require('esbuild').transformSync(js, { format: 'cjs', }) process.stdout.write(out.code)
打包结果 是对 esm 用一个 「toCommonJS」 进行包裹
ESM
let js = 'module.exports = "test"' let out = require('esbuild').transformSync(js, { format: 'esm', }) process.stdout.write(out.code)
但是 同样一段代码 esm 打包出来的 代码更少,用了一个 commonJS 函数包裹 这也证明了 esbuild 在esm 和commonjs 之间的能力。