概述
作为Vite 的双引擎之一,Esbuild 使Vite 在许多关键构建阶段(如依赖预编译、TS 语法翻译和代码压缩)都能实现卓越的性能,无论Vite 的配置如何。项目和源代码实现包括Esbuild本身的许多基本概念和高级用法。
高性能的Esbuild
Esbuild 是Figma CTO“Evan Wallace”基于Golang 开发的一款打包工具,与传统打包工具相比,它专注于性能优势,比传统工具快10-100 倍,您可以将其构建为.如此高的构建性能的主要原因可以概括为以下四点。
1)使用Golang开发
构造逻辑代码直接编译为原生机器码,而不是先将代码解析为字节码再转换为JS 等机器码,从而大大节省了程序执行时间。
2)多核并行处理
内部打包算法充分利用多核CPU,每一步都尽可能并行化。
利用Go 的多线程共享内存。
3)从头开始创建一个轮子
我很少使用第三方库,并且自己编写所有逻辑,从AST 解析到小块逻辑。
字符串操作确保最终的代码性能
4)高效的内存使用
Esbuild 尽可能地从头到尾重复使用单个AST 节点数据,而不是像JS 打包工具那样频繁地解析和传递AST 数据(例如String – TS – JS – String)。这浪费了大量的内存。
使用
运行pnpm init -y 创建新项目,并使用以下命令: 完成Esbuild 安装。
$ pnpm i esbuild Esbuild 有两种使用方式:命令行调用和代码调用。
1)命令行调用
命令行调用也是最简单的使用方法。我们首先编写一些示例代码并创建一个新的src/index.jsx 文件,其中包含以下内容:
//src/index.jsx
从“react-dom/server”导入服务器。
let Greet=()=h1你好,Jujin!/h1;
console.log(Server.renderToString(Greet /));
请注意安装所需的依赖项并在终端: 中运行以下命令:
$ pnpm installverseverse-dom 接下来,将构建脚本: 添加到package.json。
\’脚本\’: {
\’build\’: \’./node_modules/.bin/esbuild src/index.jsx –bundle –outfile=dist/out.js\’
},
现在,当您在终端中运行$ pnpm run build 时,您将看到以下日志信息:
这意味着Esbuild 打包已使用命令行成功完成。但命令行使用方式不够灵活,只能传递一些简单的命令行参数,所以一般情况下还是会使用代码调用。
2)代码调用
Esbuild 主要向外界公开两种类型的API: Build API 和Transform API。通过在Nodejs 代码中调用这些API,您可以使用Esbuild 的各种功能。
项目打包——Build API
Build API主要用于打包项目,包含三个方法:build、buildSync、serve。
首先,让我们使用Node.js 构建方法。您可以在项目的根目录中创建一个新的build.js 文件。
文件内容为:
const { 构建、buildSync、服务}=require(\’esbuild\’);
异步函数runBuild() {
//异步方法返回Promise
const 结果=等待构建({
//—- 下面是一些常用的配置—-
//当前项目根目录
absWorkingDir: process.cwd(),
//入口文件列表,是一个数组
EntryPoints: [\’./src/index.jsx\’],
//打包后的产品目录
outdir: \’距离\’,
//是否需要打包,一般设置为true
捆绑: 真实,
//模块化格式包括`esm`、`commonjs`、`iife`
格式:\’esm\’,
//必须排除打包的依赖列表
外部:[],
//是否启用自动提取
分割: 真实,
//是否生成SourceMap文件
源图: 真实,
//是否生成打包的元信息文件
图元文件: 正确,
//是否进行代码压缩
minify: 假,
//是否开启监听模式。在观察模式下对代码进行的任何更改都将触发重新打包。
手表:假货,
//是否将产品写入磁盘
写:真,
//Esbuild 有一组内置加载器,包括base64、binary、css、dataurl、file、js(x)、ts(x) 和text。
//对于一些特殊的文件,调用另一个加载器来加载它们。
装载机: {
\’.png\’: \’base64\’,
}
});
控制台.log(结果);
}
运行构建();
然后,当我在命令行上运行node build.js时,我在控制台中得到以下日志信息:
以上是Esbuild打包的元信息,对于创建扩展Esbuild功能的插件非常有用。
然后我检查了dist目录,发现打包后的产品和对应的SourceMap文件也成功写入到:磁盘了。
事实上,buildSync方法的使用几乎是相同的,如下面的代码:所示。
函数runBuild() {
//同步方法
常量结果=buildSync({
//跳过设置集
});
控制台.log(结果);
}
运行构建();
但是,我们不建议使用同步API,例如buildSync。这些有两个负面影响。另一方面,Esbuild很容易在当前线程上发生阻塞,失去任务并发的好处。另一方面,所有Esbuild 插件都不允许异步操作,这给插件开发增加了限制。
因此,我们建议使用异步API构建,这样可以更好地避免上述问题。在项目打包方面,除了Build和buildSync之外,Esbuild还提供了另一个比较强大的API。
—— 发球。这个API有3个功能
打开服务模式会在指定的端口和目录上构建静态文件服务。该服务器是用原生Go 语言实现的,所有产品文件也是如此。请注意,默认情况下它不会写入磁盘,但每次请求服务时都会访问它并始终返回新产品。导致重建的不是代码更改,而是新的请求。
让我们举个例子
//构建.js
const { 构建、buildSync、服务}=require(\’esbuild\’);
函数runBuild() {
服务({
端口: 8000,
//静态资源目录
servir: \’./dist\’
},{
absWorkingDir: process.cwd(),
EntryPoints: [\’./src/index.jsx\’],
捆绑: 真实,
格式:\’esm\’,
分裂: 真实,
源图: 真实,
ignoreAnnotations: 真,
图元文件: 正确,
}).then((服务器)={
console.log(\’HTTP 服务器在端口上启动\’,server.port);
});
}
运行构建();
如果将浏览器指向localhost:8000,您将看到Esbuild 服务器返回的编译结果为:
每个后续浏览器请求都会触发Esbuild 重建,并且每次重建都是增量构建过程,其完成时间比第一次构建要少得多(通常快70% 左右)。 Serve API仅适合在开发阶段使用,不适合生产环境。
单文件转译——Transform API
除了打包项目的能力外,Esbuild还提供了编译单个文件的能力,即Transform API。与Build API类似,它也包含两个同步和异步方法:transformSync和transform。我们将在下面详细解释这些方法。
首先,在项目根目录下新建一个transform.js,内容如下:
//转换.js
const { 转换,transformSync }=require(\’esbuild\’);
异步函数runTransform() {
//第一个参数是代码字符串,第二个参数是编译设置
常量内容=等待转换(
\’const isNull=(str: string): boolean=str.length 0;\’,
{
源图: 真实,
装载机:\’tsx\’,
}
);
控制台.log(内容);
}
运行变换();
使用transformSync类似,只是切换到同步调用方式。
函数运行变换{
const content=waittransformSync(/* 参数与transform相同*/)
控制台.log(内容);
}
但是,使用同步API消除了Esbuild并发任务处理的好处(Build API部分已经分析过),所以也不建议使用transformSync。出于性能考虑,Vite 底层实现也使用异步API 转换进行TS 和JSX 单文件转换。
Esbuild 插件开发
在使用Esbuild时,难免会遇到需要添加自定义插件的场景。 Vite 严重依赖带有预编译实现的Esbuild 插件的逻辑。因此,插件开发是Esbuild非常重要的一部分。接下来,您将完成Esbuild 的插件开发,以便您可以学习如何使用一些关键挂钩。
1)基本思想
插件开发其实就是根据自己的架构进行扩展和定制。 Esbuild 插件也不例外。通过Esbuild插件,您可以扩展Esbuild原有的路径解析、模块加载等功能,在Esbuild构建过程中执行一组自定义逻辑。
Esbuild插件结构被设计为具有两个属性的对象:名称和设置。 name 是插件的名称,setup 是函数,输入参数是构建对象。该对象用于自定义一些钩子函数逻辑。
2) 插件示例
下面是一个简单的Esbuild 插件的示例。
name: \’环境\’,
设置(构建){
build.onResolve({ filter: /^env$/}, args=({
path: args.path,
命名空间: \’env-ns\’,
}))
build.onLoad({ filter: /.*/, namespace: \’env-ns\’ }, ()=({
content: JSON.stringify(process.env),
loader: \’json\’,
}))
},
}
需要(\’esbuild\’).build({
EntryPoints: [\’src/index.jsx\’],
捆绑: 真实,
outfile: \’out.js\’,
//应用插件
plugins: [环境插件],
}).catch(()=process.exit(1))
2)使用插件后的效果为:
//应用env 插件将在构建期间将其替换为process.env 对象
从\’env\’ 导入{ PATH }
console.log(`路径是${PATH}`)
那么构建对象中的各个钩子函数是如何使用的呢?
3)使用钩子函数
3.1 onResolve 和onLoad 钩子
Esbuild插件中有两个非常重要的钩子:onResolve和onload,分别控制路径解析和模块内容加载的过程。
首先,我们来讨论一下如何使用上面插件示例中的两个钩子。
build.onResolve({ filter: /^env$/}, args=({
path: args.path,
命名空间: \’env-ns\’,
}));
build.onLoad({ filter: /.*/, namespace: \’env-ns\’ }, ()=({
content: JSON.stringify(process.env),
loader: \’json\’,
}));
可以看到两个钩子函数都需要传递两个参数:选项和回调。
我们先来谈谈选项。这是一个对象,对于onResolve 和onload 来说是一样的。包含两个属性:过滤器和命名空间。类型定义为:
界面选项{
过滤:正则表达式;
命名空间? 字符串;
}
filter 是必需参数,是一个正则表达式,用于确定过滤掉哪些特征文件。 请注意,插件的过滤器正则表达式是使用Go 的本机正则表达式实现的。规则应尽可能严格,以防止性能下降。同时,与JS 中通常的规则不同,它不支持预读(?)。
=)、后向引用(?=) 和反向引用(\\1)。命名空间是一个可选参数。通常,onResolve 钩子的回调参数可以通过onLoad 钩子的命名空间过滤模块。如上面的插件示例所示,onLoad 钩子过滤掉通过env-ns 命名空间标识符处理的env 模块。
除了Options参数之外,还有一个回调参数Callback,根据不同的hook,其类型也不同。相比于选项,回调函数参数和返回值的结构要复杂得多,涉及到的属性也很多。然而,没有必要了解每个属性的详细信息。首先了解它就足够了。
onResolve钩子的函数参数和返回值总结如下:
build.onResolve({ filter: /^env$/}, (args: onResolveArgs): onResolveResult={
//模块路径
console.log(args.path)
//父模块路径
console.log(args.importer)
//命名空间标识符
console.log(args.命名空间)
//基本路径
console.log(args.resolveDir)
//import include等导入方法
console.log(args.kind)
//附加绑定插件数据
console.log(args.pluginData)
返回{
//错误信息
错误:[],
//是否需要外部
external: 假;
//命名空间标识符
命名空间: \’env-ns\’;
//模块路径
path: args.path,
//附加绑定插件数据
pluginData: 空,
//插件名称
插件名称: \’xxx\’,
//如果设置为false,如果不使用模块,模块代码将从产品中删除。不然我就不会这么做
SideEffects:假的,
//添加路径后缀如`?xxx`
后缀:\’?xxx\’,
//警告信息
警告:[],
//仅当Esbuild 打开监督模式时启用
//告诉Esbuild 应该进一步监视哪些文件/目录的更改
watchDirs:[],
watchFiles:[]
}
}
onLoad 钩子的函数参数和返回值总结如下。
build.onLoad({ filter: /.*/, namespace: \’env-ns\’ }, (args: OnLoadArgs): OnLoadResult={
//模块路径
console.log(args.path);
//命名空间标识符
console.log(args.命名空间);
//后缀信息
console.log(args.suffix);
//附加插件数据
console.log(args.pluginData);
返回{
//模块具体内容
content: \’省略内容\’,
//错误信息
错误:[],
//指定`js`、`ts`、`jsx`、`tsx`、`json`等加载器
loader: \’json\’,
//附加插件数据
pluginData: 空,
//插件名称
插件名称: \’xxx\’,
//基本路径
solveDir: \’./dir\’,
//警告信息
警告:[],
//与上面相同
watchDirs:[],
watchFiles:[]
}
});
3.2 其他钩子
除了onResolve 和onLoad 之外,构建对象还有两个用于构建的钩子:onStart 和onEnd。
在构建的开始和结束时运行一些自定义逻辑,如下例所示。这个比较好用。
const 示例插件={
name: \’示例\’,
设置(构建){
构建.onStart(()={
console.log(\’构建开始\’)
});
build.onEnd((buildResult)={
if (buildResult.errors.length) {
返回;
}
//构建元信息
//执行自定义任务,例如在检索元信息后生成HTML。
console.log(buildResult.元文件)
})
},
}
使用此钩子时需要记住两件事。
onStart执行时间发生在每次构建时,包括触发monitor或serve模式时。
重建。如果要在onEnd 挂钩中检索元文件,则需要在Esbuild 的构建配置中设置元文件属性。
设置为真。 接下来,您将进入实际的插件实战,并通过创建几个具有特定功能的插件来熟悉Esbuild 插件开发流程。
和技能。
CDN 依赖拉取插件
Esbuild原生不支持通过HTTP从CDN服务中检索相应的第三方依赖资源,如以下代码://src/index.jsx所示。
//React-dom中的所有内容都是从CDN获取的
//该代码当前无法执行
从\’https://cdn.skypack.dev/react-dom\’导入{渲染};
从“https://cdn.skypack.dev/react”导入React
let Greet=()=h1你好,Jujin!/h1;
render(Greet /, document.getElementById(\’root\’));
示例代码使用Skypack,这是一种CDN服务,提供npm第三方打包的ESM产品。
可以通过URL访问第三方包中的资源,如下图:
然后,它通过Esbuild 插件识别此类URL 路径,从网络检索模块的内容,并
您也不再需要npm install 来安装依赖项,因为Esbuild 会为您加载它。这不是很棒吗?
顺便说一下,ESM CDN对于Vite作为面向未来的前端基础设施也有着重要影响。
由于其规模较大,可以显着提高Vite在生产环境中的构建性能。
我们从最简单的版本开始,创建:
//http-import-plugin.js
module.exports=()=({
name: \’esbuild:http\’,
设置(构建){
让https=require(\’https\’);
让http=require(\’http\’);
//1.拦截CDN请求
构建.onResolve({
过滤器: /^https?\\/\\//
}, (参数)=({
path: args.path,
命名空间: \’http-url\’,
}));
//2.通过fetch请求加载CDN资源
构建.onLoad({
过滤器: /.*/,
命名空间:\’http-url\’
}, 异步(参数)={
让内容=等待新的Promise((已解决,被拒绝)={
函数获取(网址){
console.log(`下载
ding: ${url}`);
let lib = url.startsWith(\”https\”) ? https : http;
let req = lib
.get(url, (res) => {
if ([301, 302, 307].includes(res.statusCode)) {
// 重定向
fetch(new URL(res.headers.location, url)
.toString());
req.abort();
} else if (res.statusCode === 200) {
// 响应成功
let chunks = [];
res.on(\”data\”, (chunk) => chunks.push(chunk));
res.on(\”end\”, () => resolve(Buffer.concat(chunks)));
} else {
reject(
new Error(`GET ${url} failed: status ${res.statusCode}`)
);
}
})
.on(\”error\”, reject);
}
fetch(args.path);
});
return {
contents
};
});
},
});
然后我们新建 build.js 文件,内容如下:
const { build } = require(\”esbuild\”);
const httpImport = require(\”./http-import-plugin\”);
async function runBuild() {
build({
absWorkingDir: process.cwd(),
entryPoints: [\”./src/index.jsx\”],
outdir: \”dist\”,
bundle: true,
format: \”esm\”,
splitting: true,
sourcemap: true,
metafile: true,
plugins: [httpImport()],
})
.then(() => {
console.log(\”🚀 Build Finished!\”);
});
}
runBuild();
通过 node build.js 执行打包脚本,发现插件不能 work,抛出了这样一个错误:
这是为什么呢?你可以回过头观察一下第三方包的响应内容:
export * from \’/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports/optimized/r
export {default} from \’/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports/opt
进一步查看还有更多的模块内容:
因此我们可以得出一个结论:除了要解析 react-dom 这种直接依赖的路径,还要解析它
依赖的路径,也就是间接依赖的路径。那如何来实现这个效果呢?我们不妨加入这样一段 onResolve 钩子逻辑:
// 拦截间接依赖的路径,并重写路径
// tip: 间接依赖同样会被自动带上 `http-url`的 namespace
build.onResolve({ filter: /.*/, namespace: \”http-url\” }, (args) => ({
// 重写路径
path: new URL(args.path, args.importer).toString(),
namespace: \”http-url\”,
}));
加了这段逻辑后,Esbuild 路径解析的流程如下:
现在我们再次执行 node build.js ,发现依赖已经成功下载并打包了。
实现 HTML 构建插件
Esbuild 作为一个前端打包工具,本身并不具备 HTML 的构建能力。也就是说,当它把
js/css 产物打包出来的时候,并不意味着前端的项目可以直接运行了,我们还需要一份对
应的入口 HTML 文件。而这份 HTML 文件当然可以手写一个,但手写显得比较麻烦,尤
其是产物名称带哈希值的时候,每次打包完都要替换路径。那么,我们能不能通过
Esbuild 插件的方式来自动化地生成 HTML 呢?
刚才我们说了,在 Esbuild 插件的 onEnd 钩子中可以拿到 metafile 对象的信息。那么,这个对象究竟什么样呢?
{
\”inputs\”: { /* 省略内容 */ },
\”output\”: {
\”dist/index.js\”: {
\”imports\”: [],
\”exports\”: [],
\”entryPoint\”: \”src/index.jsx\”,
\”inputs\”: {
\”http-url:https://cdn.skypack.dev/-/object-assign@v4.1.1-LbCnB3r2y2yFmhmiCfPn/dist=es\”: true, // 假设使用布尔值来表示引入
\”http-url:https://cdn.skypack.dev/-/react@v17.0.1-yH0aYV1FOvoIPeKBbHxg/dist=es2019,mode=imports-auto\”: true, // 添加了可能的mode参数
\”http-url:https://cdn.skypack.dev/-/scheduler@v0.20.2-PAU9F1YosUNPKr7V4s0j/dist=es2015\”: true, // 假设是es2015而不是es201(可能是个打字错误)
\”http-url:https://cdn.skypack.dev/-/react-dom@v17.0.1-oZ1BXZ5opQ1DbTh7nu9r/dist=es2019,mode=imports-auto\”: true, // 添加了可能的mode参数
// 注意:以下条目格式可能不正确,通常我们不会在这里使用对象,除非有特定的原因
// \”http-url:https://cdn.skypack.dev/react-dom\”: { \”bytesInOutput\”: 0 }, // 这可能不是有效的输入表示
\”src/index.jsx\”: { \”bytesInOutput\”: 178 }
},
\”bytes\”: 205284
},
\”dist/index.js.map\”: { /* 省略内容 */ }
}
}
从 outputs 属性中我们可以看到产物的路径,这意味着我们可以在插件中拿到所有 js 和
css 产物,然后自己组装、生成一个 HTML,实现自动化生成 HTML 的效果
我们接着来实现一下这个插件的逻辑,首先新建 html-plugin.js ,内容如下:
const fs = require(\”fs/promises\”);
const path = require(\”path\”);
const {
createScript,
createLink,
generateHTML
} = require(\’./util\’);
module.exports = () => {
return {
name: \”esbuild:html\”,
setup(build) {
build.onEnd(async (buildResult) => {
if (buildResult.errors.length) {
return;
}
const {
metafile
} = buildResult;
// 1. 拿到 metafile 后获取所有的 js 和 css 产物路径
const scripts = [];
const links = [];
if (metafile) {
const {
outputs
} = metafile;
const assets = Object.keys(outputs);
assets.forEach((asset) => {
if (asset.endsWith(\”.js\”)) {
scripts.push(createScript(asset));
} else if (asset.endsWith(\”.css\”)) {
links.push(createLink(asset));
}
});
}
// 2. 拼接 HTML 内容
const templateContent = generateHTML(scripts, links);
// 3. HTML 写入磁盘
const templatePath = path.join(process.cwd(), \”index.html\”);
await fs.writeFile(templatePath, templateContent);
});
},
};
// util.js
// 一些工具函数的实现
const createScript = (src) => `<script type=\”module\” src=\”${src}\”></script>`;
const createLink = (src) => `<link rel=\”stylesheet\” href=\”${src}\”></link>`;
const generateHTML = (scripts, links) => `
<!DOCTYPE html>
<html lang=\”en\”>
<head>
<meta charset=\”UTF-8\” />
<meta name=\”viewport\” content=\”width=device-width, initial-scale=1.0\” />
<title>Esbuild App</title>
${links.join(\”\\n\”)}
</head>
<body>
<div id=\”root\”></div>
${scripts.join(\”\\n\”)}
</body>
</html>
`;
module.exports = {
createLink,
createScript,
generateHTML
};
现在我们在 build.js 中引入 html 插件
const html = require(\”./html-plugin\”);
// esbuild 配置
plugins: [
// 省略其它插件
html()
],
然后执行 node build.js 对项目进行打包,你就可以看到 index.html 已经成功输出到根
目录。接着,我们通过 serve 起一个本地静态文件服务器:
// 1. 全局安装 serve
npm i -g serve
// 2. 在项目根目录执行
serve .
可以看到如下的界面:
再访问 localhost:3000 ,会默认访问到 index.html 的内容:
这样一来,应用的内容就成功显示了,也说明 HTML 插件正常生效了。当然,如果要做
一个足够通用的 HTML 插件,还需要考虑诸多的因素,比如 自定义 HTML 内容 、 自定义
公共前缀(publicPath) 、 自定义 script 标签类型 以及 多入口打包 等等,大家感兴趣的话
可以自行扩展。可以参考这个插件: esbuild-plugin-html
#以上关于Vite: Esbuild的使用与其插件开发的相关内容来源网络仅供参考,相关信息请以官方公告为准!
原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/92639.html