一、插件命名
一般如果你只是给vite用的插件, 命名采用vite-plugin-xxx, 如果需要兼容rollup插件, 则需要采用rollup-plugin-xxx, 如果你的插件只作用于特定的框架,比如vue, 则可以采用vite-plugin-vue-xxx.
二、插件执行顺序
一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:
- alias;
- 带有 enforce: 'pre' 的用户插件;
- Vite 核心插件, 包括
vite:modulepreload-polyfill, vite:resolve, vite:html-inline-proxy, vite:css, vite:esbuild, vite:json, vite:wasm, vite:worker, vite:worker-import-meta-url, 'vite:asset; - 没有 enforce 值的用户插件;
- Vite 构建用的插件, 包括
vite:vue, vite:define, vite:css-post, vite:watch-package-data, vite:build-html, commonjs, vite:data-uri, rollup-plugin-dynamic-import-variables, vite:asset-import-meta-url; - 带有 enforce: 'post' 的用户插件;
- Vite 后置构建插件, 包括
vite:build-import-analysis, vite:esbuild-transpile, vite:terser, vite:reporter, vite:load-fallback.
有兴趣的同学可以去查看对应插件的源码.
三、插件钩子函数
vite插件开发我认为主要是需要了解到其提供的几个生命周期钩子, 在相应的钩子内做对应的事情, 实现你的需求, 接下来就介绍一下vite提供的钩子函数.
3.1.options 钩子
用于获取vite 对于rollup的配置信息, 因为vite开发环境用的是esbulid打包, 所以获取到的配置是空对象, 而生产环境则是用的rollup, 这时候我们是可以获取到的, 同时也可以改变这个vite对于rollup的基础配置, 如果返回null, 则是保留原配置. 需要注意的是, 该钩子整个vite构建过程中只会触发一次.
比如我想改变项目的入口文件为test.html:
options(options) {
options.input = [normalizePath(path.resolve(process.cwd(), 'test.html'))]
return options
}
3.2.buildStart 钩子
用于获取options钩子处理之后的rollup配置,同时也会返回rollup所有配置选项的默认值. 该钩子整个vite构建过程中只会触发一次.
3.3.resolveId钩子
用于处理模块路径, 在vite中, 我们可以在代码中, 引入一个不存在的模块路径, 而在resolveId 钩子可以获得改路径, 然后对于该路径进行处理, 举个例子, 我们可以在这个钩子中自己实现vite resolve.alias的功能.
resolveId(id) {
if (id.startsWith('@/')) {
const base = path.resolve(process.cwd(), 'src')
return id.replace(/@/,base)
}
},
3.4.load钩子
常用于加载虚拟模块, 如何理解这个虚拟模块呢, 也就是, 我们在代码中引入一个不存在的模块路径, 比如import msg from 'virtual:test-module', 按vite文档所说, 我们需要先在上面的resolveId钩子中, 在该路径加上一个\0为前缀.
这是一个来自 Rollup 生态系统的惯例。这可以防止其他插件试图处理这个 ID(如节点解析),而像 sourcemap 这样的核心功能可以使用这些信息来区分虚拟模块和普通文件。\0 在导入的 URL 中不是一个允许的字符,所以我们必须在导入分析中替换它们。在浏览器中,一个 0{id} 的虚拟 ID 最终被编码为 /@id/x00{id}。在进入插件处理管道之前,这个 ID 会被解码回来。所以这个过程在插件钩子代码中将是不可见的.
然后在load钩子中, 针对virtual:test-module, 我们可以返回一个变量, 例如
// plugin.ts
resolveId(id) {
if (id === 'virtual:test-module') {
return '\0' + id
}
},
load(id) {
if (id === '\0' + 'virtual:test-module') {
return {
code: `const msg = '我来自load hook.'; \n export default msg; `,
map: '' // sourcemap
}
}
}
// main.ts
import msg from 'virtual:test-module';
console.log(msg) // 我来自load hook.
然后我们就可以在代码中读取到msg这个变量, 也就是说load提供了一种在源码中插入特定代码的能力.需要注意的是, 这个钩子会在解析每个模块的时候都执行一次.
3.5.transform钩子
这个钩子比较简单, 就是转化模块的代码, 该钩子的参数是code和id, 根据模块id来选择对改模块的code进行转换. 如果转换的过程比较简单, 可以直接操作code这个字符串, 如果需求较为复杂, 则需要借助babel/parse把code转为ast, 然后通过babel/tranverse中操作ast, 操作结束后, 再通过babel/generator生成code并返回.需要注意的是, 这个钩子会在解析每个模块的时候都执行一次.
3.6.config(vite独有钩子)
这个钩子的参数第一个userConfig是用户在vite.config.ts配置的内容, 第二是env是{mode: 'production', command: 'build'}, 所以我们可以基于mode/command来更改用户配置, 在config钩子中你可以返回一个对象, vite会帮你自动合并, 如何合并成功, 你可以直接修改userConfig.该钩子整个vite构建过程中只会触发一次.
config(userConfig, {mode, command}) {
if(command === 'serve') {
return {
server: { open: true },
}
}
}
3.7.configResolved(vite独有钩子)
在这个钩子中, 我们可以读取到对vite的最终配置, 也就是说, 某些配置我们并没有在vite.config.ts配置, 但是有会有默认值, 而在这个钩子中我们都可以拿到. 当插件需要根据运行的命令做一些不同的事情时, 一般在这个钩子函数中处理.
3.8.configureServer(vite独有钩子)
这个钩子的参数是开发服务器的实例, 就像我们开发koa服务的一样, 我们可以往该实例插入中间件. 这个钩子我没想到很好的应用场景.
3.9.handleHotUpdate(vite独有钩子)
这个钩子有四个参数:
- file: 变动文件的地址
- timestamp: 变动时间
- modules: 受到影响的模块数组
- read: 读取变动文件的方法
- server: 当前开发服务器的实例
其中read参数主要是解决, 某些编辑器保存时, 已经会触发文件的变动事件, 但是编辑器还未写入内容, 这read函数内部处理了这种情况, 确保我们能通过这个函数拿到最新的文件内容.
而这个钩子的返回值有三种情况, 如果返回空, 则不会推送热更新消息, 如果不返回值, 则vite会自动对该文件相关的模块推送热更新消息, 而当我们返回一个模块数组之后, vite会对这些模块推送热更新消息, 但是, 即使推送了热更新, 我们也需要在对应的模块接受更新.
比如, 我在main.ts中引入test.json;
import { createApp } from 'vue'
import App from './App.vue'
import json from './test.json'
createApp(App).mount('#app')
然后我在插件的handleHotUpdate中处理*.json文件的变动;
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.json')) {
// 这里是获取main.ts模块, importers表示引入了当前json的模块.
let mainModule = [...ctx.modules[0].importers]
return mainModule
}
}
这时,如果我们改变test.json, 浏览器会发生reload, 并不是热更新, 此时
我们需要在main.ts中接收热更新动作;
import { createApp } from 'vue'
import App from './App.vue'
import json from './test.json'
createApp(App).mount('#app')
if (import.meta.hot) {
import.meta.hot.accept((newModule)=> {
console.log('update')
})
}
也就是说, vite帮我们实现了 文件变动 -> 通知模块更新 -> 模块更新 的前两步工作, 第三步工作, 模块更新, 我们需要在accept函数的回调函数中处理, 而模块更新这一步一般工作都是重复的, 所以可以通过插件注入代码的方式去实现.
另外需要说明的是, 热更新这种处理一般不需要业务开发者来实现, 比如vue文件的热更新, @vitejs/plugin-vue 已经完整实现了整个过程.
3.10. buildEnd钩子
这个钩子只会在生产环境被调用, 调用时机是生成代码之前.
3.11. buildEnd钩子
这个钩子只会在生产环境被调用, 调用时机构建完成之后.
四、总结
当你业务中总有一些重复的内容需要CV, 不妨考虑一下用vite插件来做这些重复工作, 而前提就是你需要好好理解并实践每个钩子函数可以做的事情, 希望这篇文章对你有所帮助.