vite整合了rollup,所以vite的插件可以看成是受限制的rollup插件,它支持部分rollup的Hooks。vite也提供仅属于它自己的Hooks。
命名规范
- 插件希望在vite和rollup里使用: rollup-plugin-xxx
- 仅在vite使用:vite-plugin-xxx
兼容rollup部分Hooks(仅列举常用的)
服务启动时
会兼容rollup的options和buildStart钩子,只会执行一次,文件改变触发的打包不会再触发钩子与文件更新无关,也符合插件编码规范。
每个模块
为了实现代码编译能力,兼容resolveId、load、transform、buildEnd。
- resolveId:找到对应文件
- load:加载文件的源码
- transform:把文件源码转换成目标代码
- buildEnd:代码编译完成
服务关闭时
会触发closeBundle。
在vite里使用rollup插件
如果需要在vite里使用rollup的插件,vite会用自己的规范读取rollup插件的config,只会执行vite里兼容的Hooks。
常见的场景是,已经写了rollup的插件,不想重写一个插件来兼容vite了,有没有办法能使我们在vite按照rollup的规则来使用插件呢?
vite提供了rollupOptions
配置项,使我们可以在build中配置rollup 插件:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
// ...
build: {
rollupOptions: {
plugins: []
}
}
})
vite 在 build 的时候其实是执行了 rollup 的 build。
⚠️ 需要注意的是modulePased钩子不会被触发,vite为了对代码整体进行ast的解析,在开发环境代码编译是通过esbuild进行,在执行完rollup插件会传给esbuild来产出最终的代码,所以modulePased这部分会交给esbuild来完成。
总结:适合在vite使用的rollup插件需要满足三要素
- 没有使用 moduleParsed 钩子
- 打包和输出之间没有很强的耦合
- 代码输出不过分依赖Hooks的执行或使用到的Hooks是vite所兼容的
vite专属的Hooks
config
在config里返回的对象会被合并到vite的配置当中立即生效,可以根据vite当前配置(包含用户自定义配置)自动生成额外的配置或初始化用户漏传的配置,它的返回值有两种方式,返回对象或Promise。
export default () => {
return {
name: 'test',
config(userConfig) {
// 方式1直接返回对象
return {
resolve: {
alias: {
'@diy': '/src/diy'
}
}
}
// 方式2在Promise里的resolve返回对象,
return new Promise(resolve => {
resolve({
resolve: {
alias: {
'@diy': '/src/diy'
}
}
})
})
}
}
}
configResolved
所有插件的config执行完毕会触发这个hook,可以在这个钩子里拿到最终的配置,在这个钩子不能再修改配置了。
可以将配置存到插件的运行环境(闭包)中,方便后续在其它钩子中读到它。
export default () => {
let root = ''
return {
name: 'test',
config(userConfig) {
return { resolve: { alias: { '@diy': '/src/diy' } }
},
configResolved(config) {
root = config.root
console.log(config.resolve)
// { ..., alias: [
// { find: /^\/@vite\//, replacement: [Function: replacement] },
// { find: '@diy', replacement: '/src/diy' }
// ]
// }
}
}
}
configureServer
在这个钩子里可以获取server的实例,增加devServer中间件做一些处理。
// server属性
interface ViteDevServer {
/**
* 被解析的 Vite 配置对象
*/
config: ResolvedConfig
/**
* 一个 connect 应用实例
* - 可以用于将自定义中间件附加到开发服务器。
* - 还可以用作自定义http服务器的处理函数。
或作为中间件用于任何 connect 风格的 Node.js 框架。
*
* https://github.com/senchalabs/connect#use-middleware
*/
middlewares: Connect.Server
/**
* 本机 node http 服务器实例
*/
httpServer: http.Server | null
/**
* chokidar 监听器实例
* https://github.com/paulmillr/chokidar#api
*/
watcher: FSWatcher
/**
* web socket 服务器,带有 `send(payload)` 方法。
*/
ws: WebSocketServer
/**
* Rollup 插件容器,可以针对给定文件运行插件钩子。
*/
pluginContainer: PluginContainer
/**
* 跟踪导入关系、url 到文件映射和 hmr 状态的模块图。
*/
moduleGraph: ModuleGraph
/**
* 以代码方式解析、加载和转换 url 并获取结果
* 而不需要通过 http 请求管道。
*/
transformRequest(
url: string,
options?: TransformOptions
): Promise<TransformResult | null>
/**
* 应用 Vite 内建 HTML 转换和任意插件 HTML 转换
*/
transformIndexHtml(url: string, html: string): Promise<string>
/**
* 加载一个给定的 URL 作为 SSR 的实例化模块
*/
ssrLoadModule(
url: string,
options?: { isolated?: boolean }
): Promise<Record<string, any>>
/**
* 解决 ssr 错误堆栈信息
*/
ssrFixStacktrace(e: Error): void
/**
* 启动服务器
*/
listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
/**
* 重启服务器
*
* @param forceOptimize - 强制优化器重新大伯啊,和命令行内使用 --force 一致
*/
restart(forceOptimize?: boolean): Promise<void>
/**
* 停止服务器
*/
close(): Promise<void>
}
控制添加的中间件执行顺序:
export default () => {
return {
name: 'test',
configureServer(server) {
// 高优先级:直接添加中间件优先级很高,会在vite中间件执行之前执行
server.middlewares.use((req, res, next) => {
next()
})
// 低优先级:在Hook的返回值里添加,会在vite中间件执行之后执行
return () => {
server.middlewares.use((req, res, next) => {
next()
})
}
}
}
}
中间件的入参和express中间件的入参含义一样。
⚠️ 如果需要处理get请求的接口推荐使用高优先级的方式,vite的中间件会将页面发出的get请求映射到index html里,轮不到低优先级的中间件处理。
transformIndexHtml
处理入口html文件内容的钩子。
export default () => {
return {
name: 'test',
transformIndexHtml(html) {
console.log(html)
/**
* 控制台打印index.html文件的内容:
* <!DOCTYPE html>
* <html lang="en">
* <head>
* <meta charset="UTF-8" />
* <link rel="icon" href="/favicon.ico" />
* <meta name="viewport" content="width=device-width, initial-scale=1.0" />
* <title>Vite App</title>
* </head>
* <body>
* <div id="app"></div>
* <script type="module" src="/src/main.js"></script>
* </body>
* </html>
*/
}
}
}
handleHotUpdate
热更新的时候会触发的钩子,钩子能拿到热更新模块的信息。
export default () => {
return {
name: 'test',
handleHotUpdate(ctx) {
console.log(ctx)
/**
* {
* file: 更新的文件
* timestamp: 更新的时间
* modules: 更新的module [
* ModuleNode: modulePased处理的内容
* ]
* ...
* }
*/
}
}
}
热更新后,通知客户端处理热更新:
export default () => {
return {
name: 'test',
handleHotUpdate(ctx) {
ctx.server.ws.send({
type: 'custom',
event: 'handleHotUpdate',
data: {
msg: 'hello client, hot updated',
file: ctx.file,
timestamp: ctx.timestamp
}
})
}
}
}
// app.jsx
if (import.meta.hot) {
import.meta.hot.on('handleHotUpdate', val => {
console.log(val)
})
}
// 触发热更新,控制台打印
/**
* {
* msg: 'hello client, hot updated',
* file: '/.../src/App.jsx', (绝对路径)
* timestamp: 1660728266941
* }
*/
vite插件执行时机
相对于rollup插件是按照配置数组顺序执行vite的执行顺序更加灵活,按照数组执行的前提下支持3阶段配置。
pre
首批被执行的插件,会在@rollup/plugin-alias插件执行之后执行。normal(默认值)
第二批配执行的插件,会在vite的build阶段之前被执行,可以根据配置判断是否需要处理当前文件的代码。post
会在vite的build阶段之后被执行,进行代码构建方面的工作(minimize、代码分析...)。
// vite-plugin-test.js
export default (enforce: 'pre' | 'post') => {
return {
name: 'test',
enforce,
buildStart() {
console.log('buildStart:', enforce);
}
}
}
// vite.config.js
import { defineConfig } from 'vite'
import testPlugin from './plugins/test-plugin'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [testPlugin('post'), testPlugin(), testPlugin('pre')]
})
// 执行vite
#sh: vite
/**
* 终端打印
* buildStart: pre
* buildStart: undefined
* buildStart: post
*/
可以看到组件的执行机制是先按照优先级,再按顺序。