对于Vite来说,它是基于esbuild与rollup双引擎设计的,在开发阶段使用esbuild进行依赖预构建,然后基于浏览器原生支持的ESM完成开发预览,而在生产环境打包时,直接使用的rollup构建。那么在这种背景下,Vite的插件机制应该如何设计?
在源码中,我们能够经常看到PluginContainer的身影,Vite正是通过它来模拟rollup的行为
PluginContainer 的 实现 基于借鉴于 WMR 中的 rollup-plugin-container.js ,主要功能有两个:
在开发阶段,vite会模拟rollup的行为,所以插件的执行机制也与rollup相同
图片
❝
这里需要注意的是:在 vite 中由于 AST 分析是通过 esbuild 进行的,所有没有模拟 moduleParsed 钩子
上下文对象通过 Context 实现 PluginContext 接口定义,PluginContext 实际上是 Rollup 内部定义的类型,可以在源码中看到 vite 实现了 Rollup 上下文对象
class Context implements PluginContext { //... 具体实现}type PluginContext = Omit< RollupPluginContext, // Rollup 定义插件上下文接口 // not documented | 'cache' // deprecated | 'moduleIds'>
Context 上下文对象一共有 14 个核心方法,其中有 3 个方法是比较核心的方法
更多内容可以查看rollup文档
rollup构建流程主要分为两大类:build和output,build 阶段主要负责创建模块依赖图,初始化各个模块的 AST 以及模块之间的依赖关系。output阶段才是真正的打包构建过程
通过构建流程rollup的hook类型可以分为:build hook和output hook两大类
除了上面这种分类,rollup插件还可以根据各自的执行方式来进行分类:
除了根据构建阶段可以将 Rollup 插件进行分类,根据不同的 Hook 执行方式也会有不同的分类,主要包括Async、Sync、Parallel、Squential、First这五种。在后文中我们将接触各种各样的插件 Hook,但无论哪个 Hook 都离不开这五种执行方式。
首先是Async和Sync钩子函数,两者其实是相对的,分别代表异步和同步的钩子函数,这个很好理解。
这里指并行的钩子函数。如果有多个插件实现了这个钩子的逻辑,一旦有钩子函数是异步逻辑,则并发执行钩子函数,不会等待当前钩子完成(底层使用 Promise.all)。
比如对于Build阶段的buildStart钩子,它的执行时机其实是在构建刚开始的时候,各个插件可以在这个钩子当中做一些状态的初始化操作,但其实插件之间的操作并不是相互依赖的,也就是可以并发执行,从而提升构建性能。反之,对于需要依赖其他插件处理结果的情况就不适合用 Parallel 钩子了,比如 transform。
Sequential 指串行的钩子函数。这种 Hook 往往适用于插件间处理结果相互依赖的情况,前一个插件 Hook 的返回值作为后续插件的入参,这种情况就需要等待前一个插件执行完 Hook,获得其执行结果,然后才能进行下一个插件相应 Hook 的调用,如transform。
如果有多个插件实现了这个 Hook,那么 Hook 将依次运行,直到返回一个非 null 或非 undefined 的值为止。比较典型的 Hook 是 resolveId,一旦有插件的 resolveId 返回了一个路径,将停止执行后续插件的 resolveId 逻辑。
以下钩子在服务器启动时被调用:
以下钩子会在每个传入模块请求时被调用:
它们还有一个扩展的 options 参数,包含其他特定于 Vite 的属性。
以下钩子在服务器关闭时被调用:
请注意 moduleParsed 钩子在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。
output阶段的hook(除了 closeBundle) 在开发中是 不会 被调用的。你可以认为 Vite 的开发服务器只调用了 rollup.rollup() 而没有调用 bundle.generate()。
Vite 插件也可以提供钩子来服务于特定的 Vite 目标。当然这些钩子会被 Rollup 忽略。
在解析 Vite 配置前调用。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的 mode 和 command。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)。
// 返回部分配置(推荐)const partialConfigPlugin = () => ({ name: 'nanjiu-plugin', config(config, { command }) { console.log('config', config, command) }})
图片
需要注意的是:用户插件在运行这个钩子之前会被解析,因此在 config 钩子中注入其他插件不会有任何效果。
在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。
const examplePlugin = () => { let config return { name: 'read-config', configResolved(resolvedConfig) { // 存储最终解析的配置 config = resolvedConfig }, // 在其他钩子中使用存储的配置 transform(code, id) { if (config.command === 'serve') { // dev: 由开发服务器调用的插件 } else { // build: 由 Rollup 调用的插件 } }, }}
是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件
const myPlugin = () => ({ name: 'configure-server', configureServer(server) { server.middlewares.use((req, res, next) => { // 自定义请求处理... }) },})
与 configureServer 相同,但用于预览服务器。configurePreviewServer 这个钩子与 configureServer 类似,也是在其他中间件安装前被调用。如果你想要在其他中间件 之后 安装一个插件,你可以从 configurePreviewServer 返回一个函数,它将会在内部中间件被安装之后再调用
const myPlugin = () => ({ name: 'configure-preview-server', configurePreviewServer(server) { // 返回一个钩子,会在其他中间件安装完成后调用 return () => { server.middlewares.use((req, res, next) => { // 自定义处理请求 ... }) } },})
转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。
这个钩子可以是异步的,并且可以返回以下其中之一:
默认情况下 order 是 undefined,这个钩子会在 HTML 被转换后应用。为了注入一个应该通过 Vite 插件管道的脚本, order: 'pre' 指将在处理 HTML 之前应用。order: 'post' 是在所有未定义的 order 的钩子函数被应用后才应用。
const htmlPlugin = () => { return { name: 'nanjiu-plugin', transformIndexHtml(html) { return html.replace(/<title>(.*?)<//title>/, `<title> nanjiu plugin </title>`) }, }}
执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象
interface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer}
const hotPlugin = () => { return { name: 'nanjiu-plugin', handleHotUpdate({ server, modules, timestamp}) { console.log('handleHotUpdate', modules) }, }}
当我修改App.vue文件时,modules可以获取到如下信息:
图片
一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:
请注意,这与钩子的排序是分开的,钩子的顺序仍然会受到它们的 order 属性的影响,这一点 和 Rollup 钩子的表现一样
vite 在 开发环境中,会使用 createPluginContainer 方法创建插件容器,插件容器有两个核心功能:管理插件生命周期、传递插件上下文
图片
本文链接:http://www.28at.com/showinfo-26-101721-0.html浅析Vite插件机制,你学会了吗?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 软件版本号为什么那么奇怪?