Open wbccb opened 1 year ago
Esbuild玩过几天,老实讲官网文档太烂,用起来很糟心,真正跑起来非常费劲:搞了太多promise, 一不小心没await,就会报xxx not a function错误。然后官方文档对于loader也没说清楚,配config文件的时候一头雾水,跑起来要么就是.module.css的丢失搞得页面排版混乱,要么就是还有其他莫名其妙的问题。 最要命的一点,不能支持代码拆分,那还玩个屁啊,不能拆分,意味着页面加载极度缓慢,要你这破工具有什么用?! 另外插件支持也很烂,不能自动渲染index.html,自动注入生成后的css,js标签,全靠手动填写! 总之一个字,烂! 开发环境用用可以,生产环境的话,它想都别想! 它唯一的价值,可能就是让别人能研究它的源码,关键看它怎么用golang解析AST语法树的,仅此而已。
Esbuild玩过几天,老实讲官网文档太烂,用起来很糟心,真正跑起来非常费劲:搞了太多promise, 一不小心没await,就会报xxx not a function错误。然后官方文档对于loader也没说清楚,配config文件的时候一头雾水,跑起来要么就是.module.css的丢失搞得页面排版混乱,要么就是还有其他莫名其妙的问题。 最要命的一点,不能支持代码拆分,那还玩个屁啊,不能拆分,意味着页面加载极度缓慢,要你这破工具有什么用?! 另外插件支持也很烂,不能自动渲染index.html,自动注入生成后的css,js标签,全靠手动填写! 总之一个字,烂! 开发环境用用可以,生产环境的话,它想都别想! 它唯一的价值,可能就是让别人能研究它的源码,关键看它怎么用golang解析AST语法树的,仅此而已。
我正在使用 esbuild 创建一个构建工具。esbuild 是目前最好的 bundle 打包器。理念很不错,速度也还可以。bundle 对于小程序,和移动端这种非标浏览器环境很方便。
目前我使用的使用,是支持分割(splitting)的,不过仅限于 esm 模式下,esm 是静态的,便于分析依赖,处理成分片。也支持动态 import 和 require。
唯一不足的就是,插件没有 onTransform 的hook,我看到有些插件是有的,但是我运行报错,可能是版本迭代移除了。这也和设计理念有关系,Transform 是比较消耗时间的,如果转到 js 层去处理,速度就不能保证。
如果需要得到内容,进行处理,只能通过 fs 去读取文件 io。
esbuild介绍
极速
JavaScript
打包器!esbuild
构建工具的核心目标是开创构建工具性能的新时代, 同时创建一个易于使用的现代构建工具。为何esbuild如此之快
若干原因:
大多数构建工具都是用 JavaScript 编写的, 但对于需要 JIT(即时)编译的语言来说,命令行程序的性能是他们的噩梦。 每次运行你的构建工具时,对于 JavaScript 虚拟机来说,都是第一次运行你的代码, 没有任何优化提示。 当 esbuild 忙着解析你代码的 JavaScript 时, Node 可能还忙着解析你构建工具的 JavaScript。 当 Node 解析完你构建工具的代码时,esbuild 可能已经退出了, 而你的构建工具还未开始构建。 此外,Go 在核心设计上就采用了并行性,而 JavaScript 却没有。 Go 在线程间共享内存, 而 JavaScript 必须在线程间对数据进行序列化。 尽管 Go 和 JavaScript 都有并行的垃圾收集器, 但 Go 的堆是所有线程之间共享的, 而 JavaScript 则是每个线程都拥有一个单独的堆。 根据我的测试, 这似乎将 JavaScript 工作线程可能的并行量减少了一半。 这大概是因为一半 CPU 的核在忙着帮另一半进行垃圾回收。
esbuild 内部的算法经过了精心设计,在可能的情况下, 使得所有可用的 CPU 核完全饱和。 这过程中大概分为三个阶段:解析(parse)、链接(link)和代码生成(code generation)。 解析和代码生成是占据了大部分的工作, 并且完全是可并行的(链接在大部分情况下是一个固有的串行任务)。 由于所有线程间共享内存, 因此当构建引入相同 JavaScript 库的不同入口点时,可以很容易地共享内存。 大多数现代计算机都有许多核,所以并行性是 esbuild 的最大优势之一。
完全自己编写而不使用第三方库, 会带来很多性能上的好处。 从 0 开始就考虑到性能, 可以确保所有东西都采用一致的数据结构以避免昂贵的转换过程, 在必要时进行完全地架构变更。 当然,最大缺点就是相当的耗时。 例如,许多构建工具均使用官方的 TypeScript 编译器作为解析器。 但它是为了服务于 TypeScript 编译器团队的目标而被建立, 他们并没有将性能作为首要指标。 他们的代码中大量使用了 megamorphic object shapes 以及不必要的动态属性访问 (这些都是众所周知的 JavaScript 性能杀手)。 而 TypeScript 解析器即便在类型检查被禁用的情况下, 仍会运行类型检查器。而使用 esbuild 自定义的 TypeScript 解析器,就不会遇到上述问题。
例如,esbuild 仅访问 JavaScript 的 AST 三次:
当 AST 的数据仍在 CPU 热缓存(术语,CPU 缓存策略分为热缓存和冷缓存)中时, 可以最大限度地重复使用 AST 的数据。 其他构建工具会将这些步骤分开执行,而不会交错进行。 他们还可能会在数据的表现形式间进行转换,将多个库一同使用 (例如 string→TS→JS→string,然后 string→JS→older JS→string, 再然后 string→JS→minified JS→string)这将使用大量内存并使得构建变慢。 而 Go 的另外一个好处是,它可以将内容紧密的存储在内存中, 这使得它可以使用更少的内存,更适合 CPU 缓存。 所有的对象字段的类型和字段都紧密的包裹在一起, 例如,几个布尔类型的标志每个只占一个字节。 Go 还具有值语义,可以把一个对象直接嵌入到另一个对象中, 而不需要额外分配空间。 JavaScript 则没有这些特性,而且还有其他的缺点, 比如 JIT 的开销(比如 hidden class slots) 和低效的表示方式(比如非整数使用指针进行堆分配) 这些因素中每一点都只是有显著的提速, 但综合起来, 它们可以使得构建工具的速度比目前其他常用的构建工具快好几个数量级。
esbuild主要特性
ES6
和CommonJS
模块ES6
模块进行tree shaking
JavaScript
和Go
安装esbuild
简单示例
npm install react react-dom
let Greet = () =>
Hello, world!
console.log(Server.renderToString(上述命令执行后会创建一个名为
out.js
的文件, 其中包含你的代码以及 React 库的代码。 代码完全独立,无需再依赖你的 node_modules构建JS脚本
如果需要向 esbuild 传递许多选项, 这会使得命令看起来非常笨重。如果将 esbuild 用于较为复杂的情况, 你可能会用到 esbuild 的 JavaScript API, 即在 JavaScript 中编写构建脚本。具体代码如下:
针对不同环境进行构建
浏览器
构建工具默认为浏览器输出代码, 所以无需额外配置就可以完成构建。 对于开发版本,你可能需要使用
--sourcemap
以启用source map
, 对于生产版本,你可能需要使用--minify
启用压缩。有时,你使用的包可能会引入另一个只能在 node 上运行的包, 例如 node 内置的 path 包。 当发生这种情况时,你可以通过在 package.json 中使用 browser 字段 来将此包替换成对浏览器友好的包,具体如下:
有些你想使用的 npm 包可能并不是为在浏览器中运行设计的。 有时你可以使用 esbuild 的配置项来解决这些问题, 并成功打包。 未定义的全局变量在简单情况下可以用 define 功能代替, 如遇到更复杂的情况,可以用 inject 功能代替。
Node.js
打包的原因
尽管在使用 Node 时,无需打包,但有时在 Node 代码运行前, 用 esbuild 处理下代码还是有好处的。 通过打包可以自动剥离 TypeScript 的类型, 将 ECMAScript 模块语法转换为 CommonJS 语法, 同时将 JavaScript 语法转换为特定版本 Node 的旧语法。 在包发布前打包也是有好处的, 它可以让包的下载体积更小,从而保证加载时文件系统读取它的时间更少。
配置
需要配置 platform 设置,将
--platform=node
传递给 esbuildAPI介绍
三种方式调用 API:
JavaScript
中调用Go
中调用打包模式
在 esbuild 的 API 中有两种主要的 API 调用:transform 与 build
transform
transform API 操作单个字符串,而不访问文件系统。 这使其能够比较理想地在没有文件系统的环境中使用(比如浏览器)或者作为另一个工具链的一部分
build
调用 build API 操作文件系统中的一个或多个文件。 它允许文件互相引用并且打包在一起
配置项
Bundle
一般不会打包输入文件,如果想要打包输入文件,必须显示声明
bundle:true
Define
该特性提供了一种用常量表达式替换全局标识符的方法。 它可以在不改变代码本身的情况下改变某些构建之间代码的行为:
Inject
Inject
允许使用从另一个文件导入的内容自动替换全局变量。上面
entry.js
中的process.cwd()
被配置的'./process-cwd-shim.js'
导出的内容所替换,形成下面的输出文件External
你可以标记一个文件或者包为外部(
external
),从而将其从你的打包结果中移除。 导入将被保留(对于iife
以及cjs
格式使用require
,对于esm
格式使用import
),而不是被打包, 并将在运行时进行计算。Format
为生成的 JavaScript 文件设置输出格式。有三个可能的值:
iife
、cjs
与esm
。Loader
该配置项改变了输入文件解析的方式。例如, js loader 将文件解析为 JavaScript, css loader 将文件解析为 CSS。 配置一个给定文件类型的 loader 可以让你使用 import 声明或者 require 调用来加载该文件类型。 例如,使用 data URL loader 配置 .png 文件拓展名, 这意味着导入 .png 文件会给你一个包含该图像内容的数据 URL:
可以参考Content Types官方文档找到对应的文档
Minify
启用该配置时,生成的代码会被压缩而不是格式化输出。 压缩后的代码与未压缩代码是相等的,但是会更小。这意味着下载更快但是更难调试。 一般情况下在生产环境而不是开发环境压缩代码,还可以进行移除空格、重写语法使其更体积更小、重命名变量为更短的名称
Platform
默认情况下,esbuild 的打包器为浏览器生成代码。 如果你打包好的代码想要在 node 环境中运行,你应该设置 platform 为 node:
Sourcemap
Source map 可以使调试代码更容易。 它们编码从生成的输出文件中的行/列偏移量转换回 对应的原始输入文件中的行/列偏移量所需的信息。 如果生成的代码与原始代码有很大的不同, 这是很有用的(例如 你的源代码为 Typescript 或者你启用了 压缩)。
如果你更希望在你的浏览器开发者工具中寻找单独的文件, 而不是一个大的打包好的文件, 这也很有帮助。
注意 source map 的输出支持 JavaScript 和 CSS, 而且二者的配置一致。下文中提及的 .js 文件 和 .css 文件的配置是类似的。
Target
此配置项设置生成 JavaScript 代码的目标环境。
Content Types
下面列出了所有内置内容类型。 每个内容类型都有一个关联的
"loader"
,它告诉 esbuild 如何解释文件内容。 一些文件扩展名已经默认配置了一个加载器,尽管默认值可以被覆盖js
.js
、.cjs
、.mjs
ts
ortsx
.ts
、.tsx
、.mts
json
.json
css
.css
text
.txt
binary
base64
dataurl
data:image/png;base64
开头file
empty
bundle
中删除内容的有用方法。 例如,您可以将 .css 文件配置为加载空文件,以防止 esbuild 捆绑导入到 JavaScript 文件中的 CSS 文件,即不破坏程序移除某些类型文件,手动在配置中指定文件后缀插件
插件 API 允许您将代码注入构建过程的各个部分。 与 API 的其余部分不同,它不能从命令行使用。 您必须编写 JavaScript 或 Go 代码才能使用插件 API。 插件也只能与
build
API 一起使用,而不能与transform
API 一起使用。目前存在的插件集合
https://github.com/esbuild/community-plugins
使用插件
一个
esbuild Plugin
是一个包含name
和setup()
的Object
,在plugins:[]
中进行注册,这个setup()
函数在每一个build API
调用时执行一次plugin
提供多个钩子函数,主要为onStart
、onResolve
、onLoad
、onEnd
onResolve
onResolve
和onLoad
的第1个参数为filter
(必填)和namespaces
(可选) 钩子函数必须提供过滤器filter
正则表达式,但也可以选择提供namespaces
以进一步限制匹配的路径。 为了提高性能,filter
是必须的,因为正则表达式是在 esbuild 内部计算的,跟Go
相关,不需要调用JavaScript
onResolve
和onLoad
的第2个参数为一个function(args: onResolveArgs)
onResolve
和onLoad
的第2个参数function(){}
的返回值为OnResolveResult
onLoad
每个未标记为
external:true
的唯一路径/命名空间的文件加载完成会触发onLoad()
回调,它的工作是返回模块的内容并告诉 esbuild 如何解释它。 这是一个将 .txt 文件转换为单词数组的示例插件:参数说明
onStart
注册一个开始回调,以便在新构建开始时得到通知。 这会触发所有构建,而不仅仅是初始构建,因此它对重建、监视模式和服务模式特别有用。
onEnd
注册一个
on-end
回调,以便在新构建结束时得到通知。这会触发所有构建,而不仅仅是初始构建,因此它对重建、监视模式和服务模式特别有用插件示例
落地场景
1. 代码压缩工具
Esbuild 的代码压缩功能非常优秀,可以甩开传统的压缩工具一个量级以上的性能差距。
2. 第三方库 Bundler
Vite 中在开发阶段使用 Esbuild 来进行依赖的预打包,将所有用到的第三方依赖转成 ESM 格式 Bundle 产物
参考