我们知道,Vite 开发时,用的是 esbuild 进行构建,而在生产环境,则是使用 Rollup 进行打包。
为什么生产环境仍需要打包?为什么不用 esbuild 打包?
Vite 官方文档已经做出解析:尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)
虽然 esbuild
快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建应用的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild
作为生产构建器的可能。
由于生产环境的打包,使用的是 Rollup,Vite 需要保证,同一套 Vite 配置文件和源码,在开发环境和生产环境下的表现是一致的。
想要达到这个效果,只能是 Vite 在开发环境模拟 Rollup 的行为 ,在生产环境打包时,将这部分替换成 Rollup 打包
Vite 兼容了什么
要讲 Vite 如何进行兼容之前,首先要搞清楚,兼容了什么?
我们用一个例子来类比一下:
我们可以得到一下信息:
- 洗烘一体机可以替代洗衣机,它们能做到一样的效果
- 洗烘一体机,可以使用洗衣机的生态
这时候我们可以说,洗烘一体机,兼容洗衣机的生态,洗烘一体机能完全替代洗衣机
兼容关系,是不同层级的东西进行兼容。
替代关系,是同一层级的东西进行替代
那回到 vite,我们根据 Rollup 和 Vite 的关系,可以推出:
- Vite 不是兼容 rollup,说兼容 Rollup 其实是不严谨的
- Vite 是部分兼容 Rollup 的插件生态
- Vite 可以做到部分替代 Rollup
这里强调一下,是部分兼容、部分替代,不是完全的,因为 Vite 的部分实现是与 Rollup 不同的
如何兼容 Rollup 的插件生态
想要兼容 Rollup 生态,就必须要实现 Rollup 的插件机制
Rollup 插件是什么?
Rollup 插件是一个对象,对象具有一个或多个属性、build
构建钩子和 output generation
输出生成钩子。
插件应该作为一个包分发,它导出一个可以传入特定选项对象的函数,并返回一个对象。
下面是一个简单的例子:
// rollup-plugin-my-example.js export default function myExample () { return { name: 'my-example', resolveId ( source ) { if (source === 'virtual-module') { return source; // 这表明 Rollup 不应该检查文件系统来找到这个模块的 id } return null; // 其他模块照常处理 }, load ( id ) { if (id === 'virtual-module') { return 'export default "This is virtual!"'; // 返回 "virtual-module" 的代码 } return null; // 其他模块照常处理 } }; } // rollup.config.js import myExample from './rollup-plugin-my-example.js'; export default ({ input: 'virtual-module', plugins: [myExample()], // 使用插件 output: [{ file: 'bundle.js', format: 'es' }] }); // bundle.js import text from "virtual-module" console.log(text) // 输出:This is virtual! 当 import text from "virtual-module" 时,相当于引入了这段代码:export default "This is virtual!"
宏观层面的兼容架构
Vite 需要兼容 Rollup 插件生态,就需要 Vite 能够像 Rollup 一样,能够解析插件对象,并对插件的钩子进行正确的执行和处理
这需要 Vite 在其内部,实现一个模拟的 Rollup 插件机制,实现跟 Rollup 一样的对外的插件行为,才能兼容 Rollup 的插件生态
Vite 里面包含的一个模拟 rollup,由于只模拟插件部分,因此在 Vite 源码中,它被称为 PluginContainer
(插件容器)
微观层面的实现
实现 Rollup 的插件行为,实际上是实现相同的插件钩子行为。
插件钩子是在构建的不同阶段调用的函数。钩子可以影响构建的运行方式、提供有关构建的信息或在构建完成后修改构建。
钩子行为,主要包括以下内容:
- 实现 Rollup 插件钩子的调度
- 提供 Rollup 钩子的 Context 上下文对象
- 对钩子的返回值进行相应处理
- 实现钩子的类型
什么是钩子的调度?
按照一定的规则,在构建对应的阶段,执行对应的钩子。
例如:当 Rollup 开始运行时,会先调用 options
钩子,然后是 buildStart
下图为 Rollup 的 build
构建钩子(output generation
输出生成钩子不在下图)
什么是钩子的 Context 上下文对象?
在 Rollup 的钩子函数中,可以调用 this.xxx
来使用一些 Rollup 提供的实用工具函数,Context 提供属性/方法可以参考 Rollup 官方文档
而这个 this
就是钩子的 Context 上下文对象。
Vite 需要在运行时,实现一套相同的 Context 上下文对象,才能保证插件能够正确地执行 Context 上下文对象的属性/方法。
什么是对钩子的返回值做相应的处理?
部分钩子的返回值,是会影响到 Rollup 的行为。
例如:
export default function myExample () { return { name: 'my-example', options(options) { // 修改 options return options } }; }
options
钩子的返回值,会覆盖当前 Rollup 当前的运行配置,从而影响到 Rollup 的行为。
Vite 同样需要实现这个行为 —— 根据返回值做相应的处理。每个钩子的返回值(如果有),对应的处理是不同的,都需要实现
什么是钩子类型?
钩子分为 4 种类型:
async
:钩子函数可以是 async 异步的,返回 Promisefirst
:如果多个插件都实现了这个钩子,那么这些钩子会依次运行,直到一个钩子返回的不是 null 或 undefined的值为止。sequential
:如果有几个插件实现了这个钩子,串行执行这些钩子parallel
:如果多个插件都实现了这个钩子,并行执行这些钩子
例如: options
钩子,是 async
和 sequential
类型,options
钩子可以是异步的,且是串行执行的,因为配置会按顺序依次被覆盖修改,如果是并行执行 options
,那么最终的配置就会不可控
Vite 同样需要实现这些钩子类型
插件容器
前面小节已经说过,插件容器,是一个小的 Rollup,实现了 Rollup 的插件机制。
插件容器实现的功能如下:
- 提供 Rollup 钩子的 Context 上下文对象
- 对钩子的返回值进行相应处理
- 实现钩子的类型
注意:插件容器的实现,不包含调度。调度是 Vite 在其运行过程中,使用插件容器的方法实现的
插件容器的简化实现如下:
const container = { // 钩子类型:异步、串行 options: await (async () => { let options = rollupOptions for (const plugin of plugins) { if (!plugin.options) continue // 实现钩子类型:await 实现和异步和串行,下一个 options 钩子,需要等待当前钩子执行完成 // 实现对返回值进行处理:options 钩子返回值,覆盖当前 options options = (await plugin.options.call(minimalContext, options)) || options } return options; })(), // 钩子类型:异步、并行 async buildStart() { // 实现并行的钩子类型:用 Promise.all 执行 await Promise.all( plugins.map((plugin) => { if (plugin.buildStart) { return plugin.buildStart.call( new Context(plugin) as any, container.options as NormalizedInputOptions ) } }) ) }, // 钩子类型:异步、first 优先 async resolveId(rawId, importer) { // 上下文对象,后文介绍 const ctx = new Context() let id: string | null = null const partial: Partial<PartialResolvedId> = {} for (const plugin of plugins) { const result = await plugin.resolveId.call( ctx as any, rawId, importer, { ssr } ) // 如果有函数返回值 result,就直接 return,不执行后面钩子了 if (!result) continue; return result; } } // 钩子类型:异步、优先 async load(id, options) { const ctx = new Context() for (const plugin of plugins) { const result = await plugin.load.call(ctx as any, id, { ssr }) if (result != null) { return result } } return null }, // 钩子类型:异步、串行 async transform(code, id, options) { // transform 钩子的上下文对象,不太一样,因为多了一些需要处理的工具函数。不需要深究 const ctx = new TransformContext(id, code, map as SourceMap) for (const plugin of plugins) { let result: TransformResult | string | undefined try { result = await plugin.transform.call(ctx, code, id) } catch (e) { ctx.error(e) } if (!result) continue; code = result; } return { code, map: ctx._getCombinedSourcemap() } }, // ...省略 buildEnd 和 closeBundle }
上面代码,已经是实现了下面的两个内容:
- 对钩子的返回值进行相应处理
- 实现钩子的类型
Context 上下文对象,提供了很多实用工具函数:
class Context implements PluginContext { parse(code: string, opts: any = {}) { // 省略实现 } async resolve( id: string, importer?: string, options?: { skipSelf?: boolean } ) { // 省略实现 } // ...省略 }
我们大概知道有这么个东西就行了,不需要知道具体的实现工具函数是怎么实现的。感兴趣的可以查看 Rollup 文档
插件的调度是如何实现的?
插件容器要怎么使用?
这两个问题,其实是同一个问题,当需要调度时,就要使用插件容器了。
例如:当 Server 启动时,会调用 listen
函数进行端口监听,这时候就会调用 container
的 buildStart
函数,执行插件的 buildStart
钩子
httpServer.listen = (async (port: number, ...args: any[]) => { if (!isOptimized) { try { await container.buildStart({}) // 其他逻辑 } catch (e) { httpServer.emit('error', e) return } } return listen(port, ...args) })
这就是在构建对应的阶段,执行对应的钩子。
而在哪些阶段,分别调用了什么钩子,本篇文章则不过多介绍了
总结
至此,Vite 兼容 Rollup 的方式已经讲完了~
我们先介绍了兼容的概念, Vite 兼容的是 Rollup 插件生态,而不是 Rollup 这个工具。从而得出,Vite 需要实现 Rollup 插件生态的结论
然后围绕 Rollup 插件生态,我们介绍了什么是 Rollup 插件钩子,并从宏观和微观,分别介绍了兼容的架构(PluginContainer)和需要实现的细节:
- 实现 Rollup 插件钩子的调度
- 提供 Rollup 钩子的 Context 上下文对象
- 对钩子的返回值进行相应处理
- 实现钩子的类型
最后用简单的代码,实现了一个 PluginContainer,并介绍了,如何实现插件钩子的调度。
学完本篇内容,大概也就知道了 Rollup 钩子的相关生态了,如果我们需要实现一套插件生态,也可以对 Rollup 进行模仿。另外也学会了,如何用一个工具,去兼容另外一套工具的生态 —— 实现其对外的 API 能力
最后
如果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力。