Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【基础】esbuild使用详解 #8

Open
wbccb opened this issue Mar 25, 2023 · 2 comments
Open

【基础】esbuild使用详解 #8

wbccb opened this issue Mar 25, 2023 · 2 comments
Labels

Comments

@wbccb
Copy link
Owner

wbccb commented Mar 25, 2023

注:esbuild 的版本尚未达到 1.0.0,并且目前只有一个主要开发者evanw
作者的规划中,这些特性已在进行中,处于第一优先级:

  • 代码分割(Code splitting)(#16docs)
  • CSS content type (#20docs)
  • Plugin API (#111)

下面这些是未来可能会开发的特性,但也可能不会, 亦或是会进行开发,但开发的程度有限:

  • HTML content type (#31)
  • 降级至 ES5 (#297)
  • 支持构建 top-level await (#253)

之后作者就会停止增加更多的特性。 因为作者不认为 esbuild 应该成为满足一切前端需求的一体化解决方案。 特别是,作者希望避免"webpack config"模式的麻烦和问题, 因为该模式的底层过于灵活, 其易用性会受到影响。
作者也不会为esbuild增加目前很多构建库具备的功能,比如作者并不打算在 esbuild 中加入如下特性:

  • 支持其他前端语言(例如 ElmSvelteVue 以及 Angular 等)
  • TypeScript 的类型检查(单独运行 tsc 即可)
  • 用于自定义 AST 操作的 API
  • 热更新
  • 模块联邦(module federation)

简单点说,作者就是想做一个非常非常快的打包器!不是做一个类似webpack/rollup的全能打包器!作者是有追求的人!不是重复造轮子!

同时作者认为:虽然esbuild 的 API 已被其他开发者工具使用。例如,ViteSnowpack 使用了 esbuild 的 transform API,用于将 TypeScript 转换为 JavaScript。而 Hugo 则在构建过程中使用 esbuild 的构建工具来构建 JavaScript 代码。 但目前来说,还不足以有足够的功能将 esbuild 投入生产中使用。

esbuild介绍

极速 JavaScript 打包器! esbuild 构建工具的核心目标是开创构建工具性能的新时代, 同时创建一个易于使用的现代构建工具。
截屏2023-03-25 21.58.37.png

为何esbuild如此之快

若干原因:

  • 它由 Go 编写,并被编译成原生代码。

大多数构建工具都是用 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 的最大优势之一。

  • esbuild 中的所有内容都是从 0 开始编写的。

完全自己编写而不使用第三方库, 会带来很多性能上的好处。 从 0 开始就考虑到性能, 可以确保所有东西都采用一致的数据结构以避免昂贵的转换过程, 在必要时进行完全地架构变更。 当然,最大缺点就是相当的耗时。
例如,许多构建工具均使用官方的 TypeScript 编译器作为解析器。 但它是为了服务于 TypeScript 编译器团队的目标而被建立, 他们并没有将性能作为首要指标。 他们的代码中大量使用了 megamorphic object shapes 以及不必要的动态属性访问 (这些都是众所周知的 JavaScript 性能杀手)。 而 TypeScript 解析器即便在类型检查被禁用的情况下, 仍会运行类型检查器。而使用 esbuild 自定义的 TypeScript 解析器,就不会遇到上述问题。

  • 内存得到有效的利用。理想情况下的编译器,输入内容的长度大多为 O(n) 的复杂度。 所以如果要处理大量的数据, 内存访问速度很可能会严重影响性能。 在数据上进行的访问次数越少(同时数据转化成的不同表现形式也要越少), 这样你的编译器就会越快。

例如,esbuild 仅访问 JavaScript 的 AST 三次:

  1. 第一次用于词法、解析、作用域设置以及声明符号;
  2. 第二次用于绑定符号、压缩语法、将 JSX/TS 转为 JS 以及将 ESNext 转为 ES2015;
  3. 最后一次则用于对标识符进行压缩、压缩空格、生成代码以及生成 source map。

当 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) 和低效的表示方式(比如非整数使用指针进行堆分配)
这些因素中每一点都只是有显著的提速, 但综合起来, 它们可以使得构建工具的速度比目前其他常用的构建工具快好几个数量级。

综上所述,

  • Go语言的并行性、线程间共享性能、本身语言设计的运行性能
  • esbuild内部精心设计的并行算法,充分利用CPU
  • esbuild完全自己编写所有库,比如不使用官方的ts编译器,自己写一个,虽然很费时间,但是能够自己优化里面大部分不合理的地方,提升性能
  • Go语言在利用内存使用上的优越性(与JS比较),比如存储内容更加紧密,可以使用更少内容,具有值语义,不需要额外分配空间,使得内存得到更加有效的利用,提升内存访问速度

esbuild主要特性

安装esbuild

npm install esbuild

简单示例

npm install react react-dom

// app.jsx
import * as React from 'react'
import * as Server from 'react-dom/server'

let Greet = () => <h1>Hello, world!</h1>
console.log(Server.renderToString(<Greet />))

最后,运行 esbuild 打包此文件:

{
  "scripts": {
    "build": "esbuild app.jsx --bundle --outfile=out.js"
  }
}

上述命令执行后会创建一个名为 out.js 的文件, 其中包含你的代码以及 React 库的代码。 代码完全独立,无需再依赖你的 node_modules

注意,esbuild 除了 jsx 扩展名, 无需任何配置就能够将 JSX 语法转换为 JavaScript。 尽管 esbuild 可以进行配置, 但它对常见的情况进行了默认的配置,让你不用配置就可以进行自动打包

构建JS脚本

如果需要向 esbuild 传递许多选项, 这会使得命令看起来非常笨重。如果将 esbuild 用于较为复杂的情况, 你可能会用到 esbuild 的 JavaScript API, 即在 JavaScript 中编写构建脚本。具体代码如下:

require('esbuild').build({
  entryPoints: ['app.jsx'],
  bundle: true,
  outfile: 'out.js',
}).catch(() => process.exit(1))

build 函数会在子进程中运行 esbuild 的可执行文件,并返回一个 Promise, 当构建完成后,该 Promise 将被 resolve。 上述代码并未打印捕获的异常, 因为异常中的任何错误信息默认会被打印到控制台
尽管有个同步的 buildSync API, 但异步 API 对于构建脚本来说更为合适, 因为插件只与异步 API 协同工作。

针对不同环境进行构建

浏览器

构建工具默认为浏览器输出代码, 所以无需额外配置就可以完成构建。 对于开发版本,你可能需要使用 --sourcemap 以启用 source map, 对于生产版本,你可能需要使用 --minify 启用压缩。

esbuild app.jsx --bundle --minify --sourcemap --target=chrome58,firefox57,safari11,edge16

有时,你使用的包可能会引入另一个只能在 node 上运行的包, 例如 node 内置的 path 包。 当发生这种情况时,你可以通过在 package.json 中使用 browser 字段 来将此包替换成对浏览器友好的包,具体如下:

{
  "browser": {
    "path": "path-browserify"
  }
}

有些你想使用的 npm 包可能并不是为在浏览器中运行设计的。 有时你可以使用 esbuild 的配置项来解决这些问题, 并成功打包。 未定义的全局变量在简单情况下可以用 define 功能代替, 如遇到更复杂的情况,可以用 inject 功能代替。

Node.js

打包的原因

尽管在使用 Node 时,无需打包,但有时在 Node 代码运行前, 用 esbuild 处理下代码还是有好处的。 通过打包可以自动剥离 TypeScript 的类型, 将 ECMAScript 模块语法转换为 CommonJS 语法, 同时将 JavaScript 语法转换为特定版本 Node 的旧语法。 在包发布前打包也是有好处的, 它可以让包的下载体积更小,从而保证加载时文件系统读取它的时间更少。

配置

需要配置 platform 设置,将 --platform=node 传递给 esbuild

这会将几个配置同时改为对 node 友好的默认值。 例如,所有 node 的内置包,如 fs,都会自动标记为外部(external)包,这样 esbuild 就不会尝试对它们打包。 此设置也会禁用 package.json 中的 browser 字段。

esbuild app.js --bundle --platform=node --target=node10.4

API介绍

三种方式调用 API:

  • 在命令行中调用
  • JavaScript 中调用
  • Go 中调用

概念和参数在这三种方式中基本相同

打包模式

在 esbuild 的 API 中有两种主要的 API 调用:transformbuild

transform

transform API 操作单个字符串,而不访问文件系统。 这使其能够比较理想地在没有文件系统的环境中使用(比如浏览器)或者作为另一个工具链的一部分
截屏2023-03-26 00.06.02.png

build

调用 build API 操作文件系统中的一个或多个文件。 它允许文件互相引用并且打包在一起
截屏2023-03-26 00.06.31.png

配置项

挑选部分配置进行分析,其它配置可以查看官网文档

Bundle

一般不会打包输入文件,如果想要打包输入文件,必须显示声明bundle:true

注意:esbuild只会打包import的依赖,不会管require运行时的依赖,如果你想要管require运行时依赖,就不应该使用esbuild

import * as esbuild from 'esbuild'

console.log(await esbuild.build({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
}))

Define

该特性提供了一种用常量表达式替换全局标识符的方法。 它可以在不改变代码本身的情况下改变某些构建之间代码的行为:

替换表达式必须是一个 JSON 对象或者一个标识符

let js = 'DEBUG && require("hooks")'

// 这里的js由于DEBUG=true,因此
// js=require("hooks")
require('esbuild').transformSync(js, {
  define: { DEBUG: 'true' },
})
// 这里的js由于DEBUG=false,因此
// js=false
require('esbuild').transformSync(js, {
  define: { DEBUG: 'false' },
})
// id和str会被替换为 text, "tex3t";
// 因为text3t是双引号的字符串!!
require('esbuild').transformSync('id, str', {
  define: { id: 'text', str: '"tex3t"' },
})

Inject

Inject允许使用从另一个文件导入的内容自动替换全局变量。

// process-cwd-shim.js
let processCwdShim = () => ''
export { processCwdShim as 'process.cwd' }

// entry.js
console.log(process.cwd())
import * as esbuild from 'esbuild'

await esbuild.build({
  entryPoints: ['entry.js'],
  inject: ['./process-cwd-shim.js'],
  outfile: 'out.js',
})

上面entry.js中的process.cwd()被配置的'./process-cwd-shim.js'导出的内容所替换,形成下面的输出文件

您可以认为Inject功能类似于Define功能,除了它用导入文件而不是常量替换表达式,并且要替换的表达式是使用文件中的导出名称而不是使用内联来指定的 esbuild 的 API 中的字符串。

// out.js
var processCwdShim = () => "";
console.log(processCwdShim());

External

你可以标记一个文件或者包为外部(external),从而将其从你的打包结果中移除。 导入将被保留(对于 iife 以及 cjs 格式使用 require,对于 esm 格式使用 import),而不是被打包, 并将在运行时进行计算。

你也可以在外部(external)路径中使用 * 通配符标记所有符合该模式的为外部(external)。 例如,你可以使用 .png 移除所有的 .png 文件或者使用 /images/ 移除所有路径以 /images/ 开头的路径。当在 外部(external)路径中使用 * 通配符时, 该模式将应用于源代码中的原始路径,而不是解析为实际文件系统路径后的路径。 这允许你匹配不是真实文件系统路径的路径。

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  platform: 'node',
  external: ['fsevents'],
})

Format

为生成的 JavaScript 文件设置输出格式。有三个可能的值:iifecjsesm

Loader

该配置项改变了输入文件解析的方式。例如, js loader 将文件解析为 JavaScript, css loader 将文件解析为 CSS。
配置一个给定文件类型的 loader 可以让你使用 import 声明或者 require 调用来加载该文件类型。 例如,使用 data URL loader 配置 .png 文件拓展名, 这意味着导入 .png 文件会给你一个包含该图像内容的数据 URL:

跟webpack的loader作用一致

import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)

import svg from './example.svg'
let doc = new DOMParser().parseFromString(svg, 'application/xml')
let node = document.importNode(doc.documentElement, true)
document.body.appendChild(node)

可以参考Content Types官方文档找到对应的文档

这里的loader就是textdataurl,至于常见的javascript文件类型的loader是js,名字是非常简短的!

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: {
    '.png': 'dataurl',
    '.svg': 'text',
  },
  outfile: 'out.js',
})

Minify

启用该配置时,生成的代码会被压缩而不是格式化输出。 压缩后的代码与未压缩代码是相等的,但是会更小。这意味着下载更快但是更难调试。 一般情况下在生产环境而不是开发环境压缩代码,还可以进行移除空格、重写语法使其更体积更小、重命名变量为更短的名称

这些概念同样适用于 CSS,而不仅仅是 JavaScript

var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {
  minify: true,
  minifyWhitespace: true,
  minifyIdentifiers: true,
  minifySyntax: true,
})

require('esbuild').transformSync(css, {
  loader: 'css',
  minify: true,
})

Platform

默认情况下,esbuild 的打包器为浏览器生成代码。 如果你打包好的代码想要在 node 环境中运行,你应该设置 platform 为 node:

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  platform: 'node',
  outfile: 'out.js',
})

Sourcemap

Source map 可以使调试代码更容易。 它们编码从生成的输出文件中的行/列偏移量转换回 对应的原始输入文件中的行/列偏移量所需的信息。 如果生成的代码与原始代码有很大的不同, 这是很有用的(例如 你的源代码为 Typescript 或者你启用了 压缩)。

如果你更希望在你的浏览器开发者工具中寻找单独的文件, 而不是一个大的打包好的文件, 这也很有帮助。

注意 source map 的输出支持 JavaScript 和 CSS, 而且二者的配置一致。下文中提及的 .js 文件 和 .css 文件的配置是类似的。

require('esbuild').buildSync({
  entryPoints: ['app.ts'],
  //会生成.js.map,有sourceMappingURL
  sourcemap: true,
  //会生成.js.map,但是没有sourceMappingURL
  sourcemap: 'external',
  //插入整个 source map 到 .js 文件中而不是单独生成一个 .js.map 文件
  sourcemap: 'inline',
  //同时设置 inline 与 external,上面两种模式都有
  sourcemap: 'both',
  outfile: 'out.js',
})

Target

此配置项设置生成 JavaScript 代码的目标环境。

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'firefox57',
    'safari11',
    'edge16',
    'node12',
  ],
  outfile: 'out.js',
})

Content Types

下面列出了所有内置内容类型。 每个内容类型都有一个关联的"loader",它告诉 esbuild 如何解释文件内容。 一些文件扩展名已经默认配置了一个加载器,尽管默认值可以被覆盖

Loader 处理文件类型(默认指定)
JavaScript js .js.cjs.mjs
TypeScript tsor tsx .ts.tsx.mts
JSON json .json
CSS css .css
Text text .txt
Binary binary 此加载器将在构建时将文件加载为二进制缓冲区,并使用 Base64 编码将其嵌入到包中,手动在配置中指定文件后缀
Base64 base64 此加载程序将在构建时将文件加载为二进制缓冲区,并使用 Base64 编码将其作为字符串嵌入到包中,手动在配置中指定文件后缀
Data URL dataurl 无法指定,必须手动在配置中指定文件后缀,文件中数据需要data:image/png;base64开头
External file file 此加载程序会将文件复制到输出目录并将文件名作为字符串嵌入到包中,手动在配置中指定文件后缀
Empty file empty 此加载程序告知 esbuild 假装文件为空。 在某些情况下,它可能是一种从bundle中删除内容的有用方法。 例如,您可以将 .css 文件配置为加载空文件,以防止 esbuild 捆绑导入到 JavaScript 文件中的 CSS 文件,即不破坏程序移除某些类型文件,手动在配置中指定文件后缀

插件

插件 API 允许您将代码注入构建过程的各个部分。 与 API 的其余部分不同,它不能从命令行使用。 您必须编写 JavaScript 或 Go 代码才能使用插件 API。 插件也只能与build API 一起使用,而不能与transform API 一起使用。

  1. Esbuild 插件机制只可作用于 build API,而不适用于 transformAPI,这意味着 webpack 当中的 esbuild-loader 这种只使用 Esbuild transform 功能的地方无法利用 Esbuild 的插件机制。
  2. 插件中的 filter 正则是使用 go 原生的正则实现的,用来过滤文件,为了不使性能过于劣化,规则应该尽可能严格。同时它本身和 JS 的正则也有所区别,比如前瞻(?<=)、后顾(?=)和反向引用(\1)就不支持。
  3. 实际的插件应该考虑到自定义缓存(减少 load 的重复开销)、sourcemap 合并(源代码正确映射)和错误处理。

目前存在的插件集合

https://github.com/esbuild/community-plugins

使用插件

一个esbuild Plugin是一个包含namesetup()Object,在plugins:[]中进行注册,这个setup()函数在每一个build API调用时执行一次

import * as esbuild from 'esbuild'

let envPlugin = {
  name: 'env',
  setup(build) {
    //...
  }
}

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
  plugins: [envPlugin],
})

plugin提供多个钩子函数,主要为onStartonResolveonLoadonEnd

// 测试插件
let envPlugin = {
  name: 'env',
  setup({ onStart, onResolve, onLoad, onEnd }) {
    onStart((args) => {
    });
    onResolve({ filter: /.*/ }, (args) => {
    });
    onLoad({ filter: /.*/ }, (args) => {
    });
    onEnd((args) => {
    });
  }
}

onResolve

onResolveonLoad的第1个参数为filter(必填)和namespaces(可选)
钩子函数必须提供过滤器filter正则表达式,但也可以选择提供namespaces以进一步限制匹配的路径。
为了提高性能,filter是必须的,因为正则表达式是在 esbuild 内部计算的,跟Go相关,不需要调用 JavaScript

interface OnResolveOptions {
  filter: RegExp;
  namespace?: string;
}

let exampleOnResolvePlugin = {
  name: 'example',
  setup(build) {
    build.onResolve({ 
      filter: /^images\//,
      namespace: 'xxxx'
    }, args => {
      //...
      return {...}
    })
  },
}

onResolveonLoad的第2个参数为一个function(args: onResolveArgs)

interface OnResolveArgs {
  path: string; //导入文件路径,和代码中导入路径一致
  importer: string;
  namespace: string; //导入文件的命名空间 默认值 'file'
  resolveDir: string; // path的dir部分的路径
  kind: ResolveKind; // ResolveKind
  pluginData: any; //上一个插件传递的值
}

type ResolveKind =
  | 'entry-point' //入口文件
  | 'import-statement' //ESM 导入
  | 'require-call'
  | 'dynamic-import' //动态导入 import ('')
  | 'require-resolve'
  | 'import-rule'//css @import 导入
  | 'url-token'

onResolveonLoad的第2个参数function(){}的返回值为OnResolveResult

interface OnResolveResult {
  errors?: Message[];
  external?: boolean;//将此设置为 true,将该模块标记为外部模块,这意味着它将不会包含在包中,而是在运行时被导入
  namespace?: string;//文件命名空间,默认为 'file',表示 esbuild 会走默认处理
  path?: string; //插件解析后的文件路径
  pluginData?: any;//传递给下一个插件的数据
  pluginName?: string;
  sideEffects?: boolean;
  suffix?: string;
  warnings?: Message[];
  watchDirs?: string[];
  watchFiles?: string[];
}

interface Message {
  text: string;
  location: Location | null;
  detail: any; // The original error from a JavaScript plugin, if applicable
}

interface Location {
  file: string;
  namespace: string;
  line: number; // 1-based
  column: number; // 0-based, in bytes
  length: number; // in bytes
  lineText: string;
}

onLoad

每个未标记为external:true的唯一路径/命名空间的文件加载完成会触发onLoad()回调,它的工作是返回模块的内容并告诉 esbuild 如何解释它。
这是一个将 .txt 文件转换为单词数组的示例插件:

import * as esbuild from 'esbuild'
import fs from 'node:fs'

let exampleOnLoadPlugin = {
  name: 'example',
  setup(build) {
    // Load ".txt" files and return an array of words
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      let text = await fs.promises.readFile(args.path, 'utf8')
      return {
        contents: JSON.stringify(text.split(/\s+/)),
        loader: 'json',
      }
    })
  },
}

await esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
  plugins: [exampleOnLoadPlugin],
})

参数说明

// 跟onResolve一样,回调的第1个参数
interface OnLoadOptions {
  filter: RegExp;
  namespace?: string;
}
// 回调的第2个参数function传入的参数
interface OnLoadArgs {
  path: string;
  namespace: string;
  suffix: string;
  pluginData: any;
}
// 回调的第2个参数function的返回值
interface OnLoadResult {
  contents?: string | Uint8Array;
  errors?: Message[];
  loader?: Loader;
  pluginData?: any;
  pluginName?: string;
  resolveDir?: string;
  warnings?: Message[];
  watchDirs?: string[];
  watchFiles?: string[];
}

interface Message {
  text: string;
  location: Location | null;
  detail: any; // The original error from a JavaScript plugin, if applicable
}

interface Location {
  file: string;
  namespace: string;
  line: number; // 1-based
  column: number; // 0-based, in bytes
  length: number; // in bytes
  lineText: string;
}

onStart

注册一个开始回调,以便在新构建开始时得到通知。 这会触发所有构建,而不仅仅是初始构建,因此它对重建、监视模式和服务模式特别有用。

onEnd

注册一个on-end回调,以便在新构建结束时得到通知。这会触发所有构建,而不仅仅是初始构建,因此它对重建、监视模式和服务模式特别有用

插件示例

let envPlugin = {
  name: 'env',
  setup(build) {
     // 文件解析时触发
    // 将插件作用域限定于env文件,并为其标识命名空间"env-ns"
    build.onResolve({ filter: /^env$/ }, args => ({
      path: args.path,
      namespace: 'env-ns',
    }))

    // 加载文件时触发
    // 只有命名空间为"env-ns"的文件才会被处理
    // 将process.env对象反序列化为字符串并交由json-loader处理
    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))

落地场景

1. 代码压缩工具

Esbuild 的代码压缩功能非常优秀,可以甩开传统的压缩工具一个量级以上的性能差距。

2. 第三方库 Bundler

Vite 中在开发阶段使用 Esbuild 来进行依赖的预打包,将所有用到的第三方依赖转成 ESM 格式 Bundle 产物

参考

  1. https://esbuild.docschina.org 中文文档
  2. https://esbuild.github.io 英文文档
  3. https://juejin.cn/post/7043777969051058183
  4. https://juejin.cn/post/7049147751866564621
@wbccb wbccb added the 基础 label Mar 25, 2023
@mebest100
Copy link

Esbuild玩过几天,老实讲官网文档太烂,用起来很糟心,真正跑起来非常费劲:搞了太多promise, 一不小心没await,就会报xxx not a function错误。然后官方文档对于loader也没说清楚,配config文件的时候一头雾水,跑起来要么就是.module.css的丢失搞得页面排版混乱,要么就是还有其他莫名其妙的问题。
最要命的一点,不能支持代码拆分,那还玩个屁啊,不能拆分,意味着页面加载极度缓慢,要你这破工具有什么用?!
另外插件支持也很烂,不能自动渲染index.html,自动注入生成后的css,js标签,全靠手动填写!
总之一个字,烂! 开发环境用用可以,生产环境的话,它想都别想!
它唯一的价值,可能就是让别人能研究它的源码,关键看它怎么用golang解析AST语法树的,仅此而已。

@YangYongAn
Copy link

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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants