Esbuild Content Types

本文列出了 esbuild 所以内置支持的文件类型, 每种文件类型都有一个对应的"解析器", 解析器的作用是告诉 esbuild 如何解读文件内容. esbuild 已经对一些文件扩展名添加了默认的解析器, 这些默认值都是可以覆盖的

JavaScript

laoder: js

这个解析器用来解析.js,.cjs,.mjs文件, .cjs扩展名表示用于 Node 环境的 CommonJS 标准模块. .mjs扩展名表示符合 ECMAScript 标准的模块

默认情况下, 当启用Minify启用时, esbuild 将会使用"更现代"的 JavaScript 语法. 比如a!==void && a!== null ? a : b将被转换成a??b, 这种语法是ES2022版本中添加的新语法, 如果你不想使用这种新语法, 必须自行设置Target属性, 用来告诉 esbuild 你的项目需要在什么环境中工作, 然后 esbuild 将会避免使用一些"太过于现代的语法"

esbuild 支持 JavaScript 所有的新语法, 但是某些旧的浏览器可能不支持这些新语法, 处理这个问题时, 还是需要你去配置Target属性, 告诉 esbuild 适当的把新语法转换为旧语法.

以下两种语法始终都会被转换为旧语法:

Syntax transform Language version Example
Trailing commas in function parameter lists and calls es2017 foo(a, b, )
Numeric separators esnext 1_000_000

以下的语法特性会按照配置的目标浏览器环境有条件的进行转换

Syntax transform Language version Example
Exponentiation operator es2016 a ** b
Async functions es2017 async () => {}
Asynchronous iteration es2018 for await (let x of y) {}
Spread properties es2018 let x = {...y}
Rest properties es2018 let {...x} = y
Optional catch binding es2019 try {} catch {}
Optional chaining es2020 a?.b
Nullish coalescing es2020 a ?? b
import.meta es2020 import.meta
Logical assignment operators es2021 a ??= b
Class instance fields es2022 class { x }
Static class fields es2022 class { static x }
Private instance methods es2022 class { #x() {} }
Private instance fields es2022 class { #x }
Private static methods es2022 class { static #x() {} }
Private static fields es2022 class { static #x }
Ergonomic brand checks es2022 #x in y
Class static blocks es2022 class { static {} }
Import assertions esnext import "x" assert {}

以下的语法特性始终不会进行转换:

Syntax transform Language version Example
Async generators es2018 async function* foo() {}
BigInt es2020 123n
Top-level await es2022 await import(x)
Arbitrary module namespace identifiers es2022 export {foo as 'f o o'}
Hashbang grammar esnext #!/usr/bin/env node

你也可以参考已完成的 ECMA 提案列表正在进行的 ECMA 提案列表来了解更多新的语法特性.

请注意, 只有Format选项配置为esm时, esbuild 才支持顶部上下文内的await语法, 否则 esbuild 会报错

注意事项

在 esbuild 中使用 JavaScript 时, 你需要考虑以下几点:

对 ES5 的支持

目前, 尚不支持把 ES6+的语法转为 ES5 语法. 但是如果你希望使用 esbuild 来转换 ES5 代码, 仍然需要将Target选项配置为es5. 这是为了防止 esbuild 把 ES6+ 的语法放入打包文件中. 比如如果没有这个配置, {x:x}将会被转换为{x}, esbuild 执行这种转换, 是因为转换后的内容会更简介, 如果target设置为es5 就不会进行这种转换

对私有属性的处理

针对类似于#name的私有属性, esbuild 使用WeakMapWrakSet来保存这些私有属性. 这和Babel,Typescript编译器中的转换方式是一致的, 但是大多数现代 JS 引擎, 对于WeakMapWrakSet这两种数据类型的支持性能并不是很好.

使用这种转换方式来保存私有字段, 可能为 JS 引擎的垃圾收集器带来更大的开销. 这是因为现代 JS 引擎(ChakraCore 除外)会都把WeakMapWrakSet当做真实、独立的对象保存在浏览器中, 大量的对象可能会导致垃圾收集的性能问题, 此方面可以参考这个资料

遵循 ECMAScript 模块规范导入模块

有时候你可能希望在导入模块之前, 修改该模块它所以来的某个全局属性的状态, 但无论是 JavaScript 规范还是 esbuild 都会默认把import语句的执行顺序提升到首位, 所以下面这种方式是行不通的:

JavaScript 复制代码
window.foo = {}
import './something-that-needs-foo'

有一些工具(比如 Typescript 编译器)在这方面没有遵循 JavaScript 规范, 使用这类工具编译的代码可能会帮你解决此类问题, 但这样的代码不符合 ECMAScript 规范, 也不被那些遵循 ECMAScript 规范的环境或者工具(比如 Node, 浏览器, esbuild 等)所支持, 更是不可移植的, 所以不推荐使用这种反感.

如果你真的希望在导入模块之前, 修改它所依赖的某个全局属性的状态, 完全可以把这些操作放在另一个模块内, 然后将这个模块通过import放到依赖该全局属性的模块之前引入即可, 就像这样:

JavaScript 复制代码
import './assign-to-foo-on-window'
import './something-that-needs-foo'

eval执行问题

表达式eval(x)看起来像是一个函数调用, 但实际上它在 JavaScript 内有特殊的行为, 它意味着保存在变量x中的字符串信息, 会被当做 JavaScript 语句执行. 比如let y=123; return eval(y)将返回123;

这种方式执行语句被称为direct eval(eval字面量), 使用它的时候将面临很多问题:

  • 使用eval的文件, 会导致 esbuild 取消对所在文件的所有优化, 包括"移除死代码"与"压缩"等功能(下方为译者案例).
  • 流行的打包器, 通常会把所有的文件, 合并到一个文件中, 并重命名这些文件内的变量名, 来防止名称冲突, 这就意味着通过eval执行的代码可以在打包文件中读取任意的变量或者修改任意变量的值, 如果被它修改的值包含敏感数据, 将会引发很严重的安全问题

译者注, 下面的案例中, 就体现了eval中的语句, "意外的"修改了main.js中的age属性值.

  • 使用eval的文件, esbuild 会直接把文件放入一个 CommonJS 模式的闭包函数中, 来防止eval执行过程中对其他文件的影响. 这也会导致降低构建速度

幸运的是, 有两种方案可以避免上述某些问题:

  1. 不直接使用eval: 比如(0,eval)('x')window.eval('x')或者[eval][0]('x')这几种, 都属于非直接调用eval.
  1. 使用new Function('x')声明新的函数对象, 这种方式相当于在全局上下文中声明了一个新的函数, 并且可以向函数中添加任意参数(Function 构造函数)

The value of toString() is not preserved on functions (and classes)

It's somewhat common to call toString() on a JavaScript function object and then pass that string to some form of eval to get a new function object. This effectively "rips" the function out of the containing file and breaks links with all variables in that file. Doing this with esbuild is not supported and may not work. In particular, esbuild often uses helper methods to implement certain features and it assumes that JavaScript scope rules have not been tampered with. For example:

javascript 复制代码
let pow = (a, b) => a ** b;
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));

When this code is compiled for ES6, where the ** operator isn't available, the ** operator is replaced with a call to the **pow helper function:

javascript 复制代码
let **pow = Math.pow;
let pow = (a, b) => **pow(a, b);
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));

If you try to run this code, you'll get an error such as ReferenceError: **pow is not defined because the function (a, b) => **pow(a, b) depends on the locally-scoped symbol **pow which is not available in the global scope. This is the case for many JavaScript language features including async functions, as well as some esbuild-specific features such as the keep names setting.

This problem most often comes up when people get the source code of a function with .toString() and then try to use it as the body of a web worker. If you are doing this and you want to use esbuild, you should instead build the source code for the web worker in a separate build step and then insert the web worker source code as a string into the code that creates the web worker. The define feature is one way to insert the string at build time.

this 指向问题

在 Javascript 中, 函数中的 this 值通常指向调用者, 比如obj.fn()执行时, fn中的 this 会指向obj, 默认情况下, esbuild 也遵循这种规则. 但是当你从其他模块中调用函数时, this 的指向可能并不是你希望的样子. 比如下面的案例:

上面的例子中,通过ns.foo()调用this.bar()失败了, 这是因为 esbuild 会自动把导入代码重写为import { foo } from './lib.js. 这是 esbuild 的 tree-shaking 功能导致的. 但这不是问题, 因为使用模块化编程的开发人员通常不会这么做, 如果他们想使用 bar 函数, 通常会直接使用ns.bar()来调用而不是通过ns.foo()来间接调用

ESM 中的 default 问题

ESM 模块中有一个名为default的导出项, 当具有default导出项的 ESM 模块被转换为 CommonJS 格式, 再将这个 CommonJS 格式的代码导入另一个 ESM 模块时, 再不同的解释器中执行, 将会引起不同的结果.

esbuild 在处理这类代码时, 它必须决定使用哪种解释器来运行, esbuild 使用的方式与 webpack 是相同的.

比如你有下面的一份例子:

javascript 复制代码
//  somelib.js
Object.defineProperty(exports, "__esModule", {
  value: true,
});
exports["default"] = "foo";

// index.js
import foo from "./somelib.js";
console.log(foo);
  • babel 中的解释器

在 babel 中, 上面的案例会输出字符串foo, 这是因为 babel 发现了__esModule关键字, 从而知道somelib.js是从 ESM 转换到 CommonJS 语法的, 所以它把这份代码当做下面的样子来直接执行

javascript 复制代码
// somelib.js
export default "foo";
  • Node 中的解释器

在 node 中执行时, 将会输出{default:'foo'}, 这是因为他们认为 CommonJS 支持动态导出, 而 ESM 仅支持静态导出, 如果将 CommonJS 导入 ESM 模块, 必须使用某种方式导出 CommonJS 的导出对象本身.

如果你是某个库的开发人员, 请尽量避免使用default导出, 它确实会存在一些兼容性问题, 可能会在某些时候给用户带来不好的影响

默认情况下, esbuild 将会使用 babel 解释器来运行, 如果你希望 esbuild 使用 node 解释器, 可以把代码放在.mjs或者.mts结尾的文件中, 或者在packag.json中指定type字段为module

译者注: 关于 CommonJS 与 ESM 的导出与引用关系, 可以参考Node.js 如何处理 ES6 模块文章

Typescript

laoder: ts/tsc

遇到.ts,.tsx,.mts,.cts文件时, 默认使用ts或者tsx加载器, 这意味着 esbuild 内置了对 Typescript 语法的支持. 但是 esbuild 不会进行相关的类型检查, 如果需要你可以使用其他工具来配合 esbuild 进行类型检查的工作

下面的这些 Typescript 语法会被 esbuild 忽略(这里只是例子, 并不是全部)

语法 例子
Interface declarations interface Foo {}
type declarations type Foo = number
Function declarations function foo():void;
Ambiend declarations declare module 'foo' {}
Type-only imports import type {Type} from 'foo'
Type-only exports export type {Type} from 'foo'
Type-only import specifiers import {type Type} from 'foo'
Type-only export specifiers export {type Type} from 'foo'

下面的这些 Typescript 语法会被 esbuild 编译为 Javascript(这里只是例子, 并不是全部)

语法 例子 备注
Namespaces namespace Foo {}
Enums enum Foo { A, B }
Const enums const enum Foo { A, B }
Generic type parameters <T>(a: T): T => a Not available with the tsx loader
JSX with types <Element<T>/>
Type casts a as B and <B>a
Type imports import {Type} from 'foo' Handled by removing all unused imports
Type exports export {Type} from 'foo' Handled by ignoring missing exports in TypeScript files
Experimental decorators @sealed class Foo {} The emitDecoratorMetadata flag is not supported
相关推荐
吕彬-前端36 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱39 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb