Vite: Esbuild的使用与其插件开发

Vite: Esbuild的使用与其插件开发概述
作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如 依赖预编译 、 TS 语法转译 、 代码压缩 ) 让 Vite 获得了相当优异的性能,是

概述

作为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

(0)
CSDN's avatarCSDN
上一篇 2024年6月27日 下午10:06
下一篇 2024年6月27日 下午10:41

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注