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

概述

  • 作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如 依赖预编译 、 TS 语法转译 、 代码压缩 ) 让 Vite 获得了相当优异的性能,是 Vite 高性能的得力助手
  • 无论是在 Vite 的配置项还是源码实现中,都包含了不少 Esbuild 本身的基本概念和高阶用法

高性能的Esbuild

  • Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。获得这样超高的构建性能,主要原因可以概括为 4 点

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 有 2 种方式,分别是 命令行调用和代码调用

1 )命令行调用

  • 命令行方式调用也是最简单的使用方式。我们先来写一些示例代码,新建 src/index.jsx 文件,内容如下

    // src/index.jsx
    import Server from "react-dom/server";
    
    let Greet = () => <h1>Hello, juejin!</h1>;
    console.log(Server.renderToString(<Greet />));
    
  • 注意安装一下所需的依赖,在终端执行如下的命令:

    • $ pnpm install react react-dom
  • 接着到 package.json 中添加 build 脚本:

    "scripts": {
         
         
    	 "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 方法。你可以在项目根目录新建 build.js 文
    件,内容如下:
const {
   
    build, buildSync, serve } = require("esbuild");
async function runBuild() {
   
   
 // 异步方法,返回一个 Promise
 const result = await build({
   
   
	 // ---- 如下是一些常见的配置 --- 
	 // 当前项目根目录
	 absWorkingDir: process.cwd(),
	 // 入口文件列表,为一个数组
	 entryPoints: ["./src/index.jsx"],
	 // 打包产物目录
	 outdir: "dist",
	 // 是否需要打包,一般设为 true
	 bundle: true,
	 // 模块格式,包括`esm`、`commonjs`和`iife`
	 format: "esm",
	 // 需要排除打包的依赖列表
	 external: [],
	 // 是否开启自动拆包
	 splitting: true,
	 // 是否生成 SourceMap 文件
	 sourcemap: true,
	 // 是否生成打包的元信息文件
	 metafile: true,
	 // 是否进行代码压缩
	 minify: false,
	 // 是否开启 watch 模式,在 watch 模式下代码变动则会触发重新打包
	 watch: false,
	 // 是否将产物写入磁盘
	 write: true,
	 // Esbuild 内置了一系列的 loader,包括 base64、binary、css、dataurl、file、js(x)、ts(x)、text
 	// 针对一些特殊的文件,调用不同的 loader 进行加载
	 loader: {
   
   
	 	'.png': 'base64',
	 }
 });
 console.log(result);
}
runBuild();
  • 随后,你在命令行执行 node build.js ,就能在控制台发现如下日志信息:
  • 以上就是 Esbuild 打包的元信息,这对我们编写插件扩展 Esbuild 能力非常有用。

  • 接着,我们再观察一下 dist 目录,发现打包产物和相应的 SourceMap 文件也已经成功写入磁盘:

  • 其实 buildSync 方法的使用几乎相同,如下代码所示:

    function runBuild() {
         
         
     // 同步方法
     const result = buildSync({
         
         
     // 省略一系列的配置
     });
     console.log(result);
    }
    runBuild();
    
  • 但我并不推荐大家使用 buildSync 这种同步的 API,它们会导致两方面不良后果。一方面容易使 Esbuild 在当前线程阻塞,丧失 并发任务处理 的优势。另一方面,Esbuild 所有插件中都不能使用任何异步操作,这给 插件开发 增加了限制

  • 因此更推荐使用 build 这个异步 API,它可以很好地避免上述问题。在项目打包方面,除了 build 和 buildSync ,Esbuild 还提供了另外一个比较强大的 API
    —— serve 。这个 API 有 3 个特点

    • 开启 serve 模式后,将在指定的端口和目录上搭建一个静态文件服务 ,这个服务器用原生 Go 语言实现,性能比 Nodejs 更高
    • 类似 webpack-dev-server,所有的产物文件都默认不会写到磁盘,而是放在内存中,通过请求服务来访问
    • 每次请求到来时,都会进行重新构建( rebuild ),永远返回新的产物
  • 值得注意的是,触发 rebuild 的条件并不是代码改动,而是新的请求到来

  • 现在,举一个例子

    // build.js
    const {
         
          build, buildSync, serve } = require("esbuild");
    function runBuild() {
         
         
     serve({
         
         
    	 port: 8000,
    	 // 静态资源目录
    	 servedir: './dist'
     }, {
         
         
    	 absWorkingDir: process.cwd(),
    	 entryPoints: ["./src/index.jsx"],
    	 bundle: true,
    	 format: "esm",
    	 splitting: true,
    	 sourcemap: true,
    	 ignoreAnnotations: true,
    	 metafile: true,
     }).then((server) => {
         
         
     	console.log("HTTP Server starts at port", server.port);
     });
    }
    runBuild();
    
  • 我们在浏览器访问 localhost:8000 可以看到 Esbuild 服务器返回的编译产物如下所示:

  • 后续每次在浏览器请求都会触发 Esbuild 重新构建,而每次重新构建都是一个增量构建的过程,耗时也会比首次构建少很多(一般能减少 70% 左右)。
  • Serve API 只适合在开发阶段使用,不适用于生产环境。

单文件转译——Transform API

  • 除了项目的打包功能之后,Esbuild 还专门提供了单文件编译的能力,即 Transform API ,与 Build API 类似,它也包含了同步和异步的两个方法,分别是 transformSync 和 transform 。下面,我们具体使用下这些方法。

  • 首先,在项目根目录新建 transform.js ,内容如下:

    // transform.js
    const {
         
          transform, transformSync } = require("esbuild");
    async function runTransform() {
         
         
     // 第一个参数是代码字符串,第二个参数为编译配置
     const content = await transform(
     	"const isNull = (str: string): boolean => str.length >
<think>我们面对的问题是Viteesbuild插件在编译Vue文件时出现“Duplicate key 'modelModifiers' in object literal”错误。这个错误通常发生在Vue组件的`v-model`使用中,特别是在使用带有修饰符的多个`v-model`绑定时。在Vue3中,当我们为同一个组件定义多个`v-model`绑定,并且每个绑定都使用了相同的修饰符(如`v-model:foo`和`v-model:bar`),但在编译过程中出现了重复的键(key)'modelModifiers'。 根据错误信息,问题出现在文件`src/views/microgrid/collection-analysis/components/FormDrawer/index.vue`中的`<script setup>`部分(lang.ts)。错误提示在对象字面量中重复的键“modelModifiers”。这通常是因为在同一个组件中,Vue为每个`v-model`绑定的修饰符生成一个对象,但生成的代码中出现了重复的属性名。 在Vue3中,当我们在组件上使用`v-model`时,实际上会展开为`modelValue`和`update:modelValue`。如果使用带参数的`v-model`(如`v-model:title`),则展开为`title`和`update:title`。同时,如果使用了修饰符,比如`v-model.trim`,Vue会自动将修饰符作为一个对象传递给子组件,这个对象的属性名为`modelModifiers`(对于默认的`v-model`)或`xxxModifiers`(对于带参数的`v-model:xxx`)。 但是,当我们在同一个组件上使用多个`v-model`并且都带有修饰符时,每个`v-model`都会生成一个对应的修饰符对象。然而,在生成的代码中,这些修饰符对象的属性名可能会重复,尤其是当我们在同一个组件上同时使用默认的`v-model`和带参数的`v-model`时,它们生成的修饰符对象都试图使用相同的属性名(`modelModifiers`)时,就会导致重复键的错误。 例如,考虑以下代码: ```vue <script setup> defineProps({ modelValue: String, modelModifiers: { default: () => ({}) }, title: String, titleModifiers: { default: () => ({}) } }) </script> ``` 但是,如果我们在同一个组件上同时使用: 1. 默认的`v-model`(生成`modelModifiers`) 2. 另一个带参数的`v-model`(例如`v-model:title`,生成`titleModifiers`) 那么就不会有重复。然而,错误信息提示的是重复键“modelModifiers”,这意味着在同一个对象字面量中,出现了两个名为`modelModifiers`的属性。 在Vue3的`<script setup>`中,我们通常使用`defineProps`来定义props。如果我们在`defineProps`中手动定义了两个同名的属性,就会导致这个错误。但通常,这个错误是由Vue编译器自动生成的代码引起的,而不是我们手动编写的。 根据引用[2]中提到的问题(vite-plugin-require-transform插件导致的打包错误)和引用[3](Duplicate keys detected)的类似性,我们可以推测可能是由于某些编译插件(如vite-plugin-require-transform)在处理过程中出现了问题,导致重复生成相同的属性。 因此,解决此问题的方法可能包括: 1. **检查组件中的`v-model`使用**:确保在同一个组件上,不要混用默认的`v-model`和另一个带参数的`v-model`但使用了相同的修饰符对象属性名。但按照Vue的设计,带参数的`v-model`应该生成不同的修饰符对象(如`titleModifiers`),所以正常情况下不会重复。 2. **检查使用插件**:根据引用[2],vite-plugin-require-transform插件可能会导致一些奇怪的错误。如果你的项目中使用了这个插件,尝试移除它,或者更新到最新版本,或者寻找替代方案。 3. **更新Vite和Vue相关依赖**:确保所有依赖都是最新版本,特别是Vite和`@vitejs/plugin-vue`,因为这个问题可能已经在更新版本中修复。 4. **检查`defineProps`中的重复键**:查看报错文件(FormDrawer/index.vue)中`defineProps`的定义,确保没有重复定义`modelModifiers`或其他修饰符对象。 5. **检查Vite配置**:在vite.config.js中,检查是否配置了多个Vue插件,或者是否有其他插件可能干扰了Vue文件的编译。 6. **临时解决方案**:如果以上方法都不行,可以尝试在vite.config.js中关闭esbuild对Vue文件的处理,使用Vite自带的Vue插件进行编译。具体做法是在`esbuild`配置中排除Vue文件: ```javascript // vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], optimizeDeps: { exclude: ['vue'], // 确保不排除Vue文件?这里可能需要调整 }, esbuild: { // 排除Vue文件,避免esbuild处理 exclude: ['*.vue'], } }) ``` 但是,这样配置后,Vue文件将由`@vitejs/plugin-vue`处理,而不是esbuild。这可能会降低开发服务器的启动速度,但可以避免这个错误。 7. **使用`vite:vue`插件替代esbuild处理Vue文件**:Vite默认使用esbuild来转换.ts文件,但Vue文件通常由`@vitejs/plugin-vue`处理。如果esbuild试图处理Vue文件中的TypeScript代码,可能会与Vue插件产生冲突。因此,确保Vue文件中的`<script lang="ts">`是由Vue插件处理的,而不是esbuild。 8. **检查重复的修饰符定义**:在组件的`defineProps`中,检查是否有重复的键。例如: ```vue <script setup lang="ts"> const props = defineProps({ modelValue: String, modelModifiers: { type: Object, default: () => ({}) }, // 第一个 // ...其他props modelModifiers: { type: Object, default: () => ({}) } // 重复! }) </script> ``` 如果有,删除重复的定义。 由于错误信息指向了文件`FormDrawer/index.vue`,我们应首先检查这个文件,看是否存在重复的`modelModifiers`的定义。 如果经过检查没有发现重复,那么问题可能出在编译插件上。建议移除或更新可能引起问题的插件(如vite-plugin-require-transform),或者调整Vite配置,避免esbuild处理Vue文件。 另外,引用[1]提到Vite集成eslint和prettier,虽然与问题没有直接关系,但确保代码规范也可能避免一些潜在错误。 综上所述,我们提供以下具体解决步骤: **步骤1:检查FormDrawer/index.vue文件** 打开该文件,检查`defineProps`中是否重复定义了`modelModifiers`。同时,检查是否在同一个组件中使用了多个`v-model`并且修饰符对象命名冲突。 **步骤2:移除或更新插件** 如果你使用了`vite-plugin-require-transform`或其他可能引起冲突的插件,尝试暂时移除它,然后重新构建,看是否解决问题。如果必须使用,尝试更新到最新版本,或者寻找替代插件。 **步骤3:更新依赖** 更新Vite、Vue以及相关插件到最新版本。 **步骤4:修改Vite配置** 在vite.config.js中,配置esbuild排除Vue文件,如下: ```js // vite.config.js export default defineConfig({ // ...其他配置 esbuild: { exclude: ['**/*.vue'] } }) ``` 这样,Vue文件就不会被esbuild处理,而是由Vue插件处理。 **步骤5:检查是否还有其他插件冲突** 暂时移除其他插件,只保留Vue插件,逐步添加以定位问题。 通常,经过以上步骤,问题应该可以解决。 如果问题仍然存在,请提供更多关于该组件的代码和vite.config.js的配置,以便进一步分析。 希望这些方法能够帮助你解决问题! §§相关问题§§ 1. 如何正确地在Vue3中使用多个`v-model`绑定? 2. Vite中如何配置esbuild排除特定文件? 3. 如何解决Vite插件之间的冲突? 4. 如何更新Vite和Vue的相关依赖? 5. Vue3中`v-model`的修饰符是如何工作的? [^1]: 引用来源: Vite集成eslint和prettier的配置。 [^2]: 引用来源: vite-plugin-require-transform插件导致的打包错误。 [^3]: 引用来源: Vue报错解决Duplicate keys detected。 </think>### 解决 Vite esbuild 插件编译 Vue 文件时出现的 "Duplicate key 'modelModifiers'" 错误 此错误通常发生在 Vue 3 的 `<script setup>` 语法中,当组件使用多个 `v-model` 绑定且存在修饰符冲突时。以下是详细解决步骤: --- #### 1. **理解错误原因** 错误 `Duplicate key "modelModifiers" in object literal` 表明: - Vue 编译器在生成代码时,为多个 `v-model` 绑定创建了相同的修饰符对象键名 `modelModifiers` - 常见于同时使用默认的 `v-model` 和带参数的 `v-model`(如 `v-model:title`) - 根本原因是 Vue 3 的响应式系统在编译时生成的代码键名冲突[^2] --- #### 2. **解决方案步骤** ##### (1) 检查并修改 `v-model` 绑定 在报错文件 (`src/views/microgrid/collection-analysis/components/FormDrawer/index.vue`) 中: ```vue <!-- 错误示例:多个 v-model 使用默认修饰符 --> <ChildComponent v-model="value" v-model:title="titleValue" /> <!-- 正确修改:显式命名修饰符 --> <ChildComponent v-model="value" v-model:title.modifiers="titleModifiers" <!-- 自定义修饰符变量 --> /> ``` 在子组件中接收修饰符: ```vue <script setup> // 子组件中明确接收不同修饰符对象 defineProps({ modelModifiers: { default: () => ({}) }, // 默认 v-model 修饰符 titleModifiers: { default: () => ({}) } // 自定义 v-model:title 修饰符 }) </script> ``` ##### (2) 升级 Vite 和 Vue 相关依赖 ```bash # 升级关键包解决已知编译问题 npm update vite @vitejs/plugin-vue vue typescript esbuild ``` ##### (3) 配置 Vite 排除冲突文件 在 `vite.config.js` 中添加: ```js import { defineConfig } from 'vite' export default defineConfig({ esbuild: { exclude: ['**/*.vue'], // 禁止 esbuild 处理 Vue 文件 }, plugins: [ vue() // 确保 @vitejs/plugin-vue 处理 Vue 文件 ] }) ``` ##### (4) 检查重复的 Props 定义 确保组件中没有重复的修饰符定义: ```vue <script setup> // ❌ 错误:重复定义 modelModifiers defineProps({ modelModifiers: Object, modelModifiers: Object }) // ✅ 正确:唯一键名 defineProps({ modelModifiers: { type: Object, default: () => ({}) }, titleModifiers: { type: Object, default: () => ({}) } }) </script> ``` --- #### 3. **验证修复** 1. 清理构建缓存: ```bash rm -rf node_modules/.vite && npm run build ``` 2. 如果使用 TS,检查类型定义: ```ts interface Props { modelModifiers?: Record<string, boolean> // 可选修饰符 titleModifiers?: Record<string, boolean> } ``` --- #### 4. **防止复现的最佳实践** - 为所有带参数的 `v-model` 显式命名修饰符: ```vue <Component v-model:search.trim="query" /> ``` - 统一使用 `defineProps` 类型声明: ```vue <script setup lang="ts"> interface Modifiers { trim?: boolean; lazy?: boolean } defineProps<{ modelValue: string modelModifiers?: Modifiers search?: string searchModifiers?: Modifiers }>() </script> ``` - 避免混用默认 `v-model` 和多参数 `v-model`(优先使用带命名参数的版本) > **关键点**:此错误通常源于 Vue 编译器在处理修饰符时的命名冲突策略[^2],通过显式命名修饰符可彻底解决。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值