最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
手写 Rollup 插件实现解析 Vue 文件实战解析
时间:2026-07-30 14:13:50 编辑:袖梨 来源:一聚教程网
本文围绕手写 Rollup 插件实现解析 Vue 文件实战解析展开,先梳理核心概念,再结合实践场景说明步骤、代码思路和容易忽略的细节,方便后续直接参考。
前言 通过上篇文章我们知道 Vue 文件一般由 template、script、style 三部分组成,编译的时候在读取了 Vue 文件的内容之后会通过 @vue/compiler-sfc 把上述 Vue 文件的三部分内容解析出来,然后再去
前言
通过上篇文章我们知道 Vue 文件一般由 template、script、style 三部分组成,编译的时候在读取了 Vue 文件的内容之后会通过 @vue/compiler-sfc 把上述 Vue 文件的三部分内容解析出来,然后再去解析各自部分的内容,比如,template 部分会通过模板编译器编译成 render 函数,script 部分会判断是否是 script setup 类型,因为 script setup 类型还需要编译 defineProps、defineEmits、defineModel 等语法糖代码,即对所谓编译宏的部分进行编译,以及将 TS 编译成 JavaScript,style 部分则需要编译 sass、less、stylus 的 CSS 代码。处理完成后,插件将结果传递给 Rollup 或 Vite 进行后续的打包或开发服务器启动等操作。
本文将详细讲解 Rollup 插件是如何编译 vue 文件的。
准备配置文件
首先我们在 rollup-plugin 目录下来创建一个 App.vue 文件,内容如下:
复制代码<template> <div>i am Cobyte ~div> template> <script setup> console.log('我是掘金签约作者:Cobyte') script> <style> div { color: red; } style> 我们现在的目标就是把上述的 App.vue 文件的代码通过 Rollup 插件编译成 JavaScript 代码。接着我们创建一个 Rollup 插件,根据我们前一篇文件所学的 Rollup 插件知识,一个基础的 Rollup 插件结构如下:
复制代码// rollup-plugin-vue.js export default function pluginVue() { return { name: 'rollup-plugin-vue' } } 接着我们修改 rollup.config.js 配置文件:
复制代码import pluginVue from './rollup-plugin-vue.js'export default [ { input: './App.vue', output: { file: 'dist/output.js', format: 'esm', }, plugins: [ pluginVue() ], }, ] 那么根据我们前一篇文章所学到的知识,可以知道 Rollup 插件对本地相对目录的文件是所有的钩子函数都会执行的,所以我们可以直接在 transform 钩子中直接编译 App.vue 文件,同时因为 Rollup 插件对本地目录的文件都会进行访问,所以我们需要判断一下只有 Vue 文件的时候才进行处理,Rollup 插件修改如下:
复制代码import compiler from '@vue/compiler-sfc' export default function pluginVue() { return { name: 'rollup-plugin-vue', transform(code, id) { // 因为 Rollup 插件对本地目录的文件都会进行访问,所以我们要判断一下只有 vue 文件的时候才进行处理 if (id.endsWidth('.vue')) { const result = compiler.parse(code); console.log(result) } } } } 同时我们需要安装 @vue/compiler-sfc:
复制代码pnpm add @vue/compiler-sfc -D Vue 文件的初步编译
接下来我们先进行调试一下,看一下经过 @vue/compiler-sfc 的 parse 方法编译结果是什么。
我们先打个断点:

然后启动调试模式运行命令:

接着我们进到 rollup-plugin 目录运行 pnpm build,最终可以看到 @vue/compiler-sfc 的 parse 方法编译结果如下:

我们可以看到 @vue/compiler-sfc 的 parse 方法把一个 Vue 文件分成了:
- template 模块
- script 模块
- scriptSetup 模块 (script setup 模式)
- styles 模块(是一个数组,因为可能有多个)
我们知道 Vue 文件一般由 template、script、style 三部分组成,@vue/compiler-sfc 的 parse 方法先把 Vue 文件进行拆分,处理成方便我们进行下一步编译的数据结构。这一步并没有对代码进行编译,可以看到每个模块中的 content 字段的内容还是原来 Vue 文件的内容。
编译 script 模块
我们知道 script setup 类型还需要编译 defineProps、defineEmits、defineModel 等语法糖代码,同时还是需要处理 CSS 变量注入等,所以我们需要编译 script 部分的代码,而这个能力 @vue/compiler-sfc 也提供了 compileScript 方法进行处理。compileScript 方法的使用如下:
复制代码import compiler from '@vue/compiler-sfc' export default function pluginVue() { return { name: 'rollup-plugin-vue', transform(code, id) { if (id.endsWith('.vue')) { const result = compiler.parse(code); + // 创建组件的唯一标识符,用于样式作用域等 + const scopeId = `${Math.random().toString(36).substr(2, 9)}`; + const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId }); + console.log(scriptCode) } } } } 打印出来的结果如下:

也就是 content 字段中的内容就是编译后的 script 部分的代码,我们整理一下如下:
复制代码export default { setup(__props, { expose: __expose }) { __expose(); console.log('我是掘金签约作者:Cobyte') const __returned__ = { } Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true }) return __returned__ } } 接下来我们编译 template 部分的代码。
编译 template 模块
编译编译 template 部分的代码也很简单,只需要使用 compileTemplate 方法进行以下操作:
复制代码import compiler from '@vue/compiler-sfc' export default function pluginVue() { return { name: 'rollup-plugin-vue', transform(code, id) { if (id.endsWith('.vue')) { const result = compiler.parse(code); // 创建组件的唯一标识符,用于样式作用域等 const scopeId = `${Math.random().toString(36).substr(2, 9)}`; const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId }); + const templateCode = compiler.compileTemplate({ source: result.descriptor.template.content, id: scopeId }) + console.log(templateCode) } } } } 编译结果如下:

也就是 code 字段中的内容就是编译后的 template 部分的代码,也就是 render 函数,我们整理一下如下:
复制代码import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, "i am Cobyte ~")) } 那么我们现在得到了 script 、template 部分的编译后的代码,我们怎么整合它们呢?通过上一篇的文章我们知道可以通过虚拟模块的方式进行整合。
组装 script 和 render 函数
我们上面提到了可以通过虚拟模块的技术整合 script 、template 部分的编译后的代码,上一篇中代码是这样的:

现在我们依然可以这样:
复制代码import compiler from '@vue/compiler-sfc' export default function pluginVue() { return { name: 'rollup-plugin-vue', transform(code, id) { if (id.endsWith('.vue')) { const result = compiler.parse(code); // 创建组件的唯一标识符,用于样式作用域等 const scopeId = `${Math.random().toString(36).substr(2, 9)}`; const scriptCode = compiler.compileScript(result.descriptor, { id: scopeId }); const templateCode = compiler.compileTemplate({ source: result.descriptor.template.content, id: scopeId }) + // 脚本导入的虚拟模块 + const scriptImport = `import script from '${id}?vue&type=script'n` + // 模板导入的虚拟模块 + const templateImport = `import { render } from '${id}?vue&type=template'n`+ // 组装 + const renderReplace = `script.render = rendern` + const exportDefault = `export default script` + return { + code: ` + ${scriptImport} + ${templateImport} + ${renderReplace} + ${exportDefault} + ` + } } } } } 通过我们前面对 Rollup 插件的了解,我们知道经过 transform 钩子处理过的代码如果还存在 import 语句那么还会继续调用 resolveId 钩子处理模块 ID,而且只对第三方模块的 ID 进行处理。而我们上述构建的模块地址是这样的:import script from '${id}?vue&type=script'n,其中 id 是一个绝对路径地址,例如:'D:workspacegithubmini-vue-vaporrollup-pluginApp.vue',最终会变成:'D:workspacegithubmini-vue-vaporrollup-pluginApp.vue?vue&type=script'。那么这个导入地址很明显不是一个本地的相对路径,所以它将被当作一个第三方库的路径进行处理。所以我们需要在 resolveId 钩子处理在模块路径,让我们写的 Rollup 插件后续可以继续处理该文件。
我们的插件改动如下:
复制代码import compiler from '@vue/compiler-sfc' + import qs from 'querystring' + // 判断否是一个 Vue 单文件组件的请求 + function parseVuePartRequest(id) { + const [filename, query] = id.split('?', 2)+ if (!query) return { vue: false, filename } + // 使用 qs.parse 方法解析查询字符串,将其转换成一个对象 + const raw = qs.parse(query) + // 如果解析后的对象中包含 vue 键,说明这是一个 Vue 组件请求 + if ('vue' in raw) { + return { + ...raw, + filename, + vue: true + } + } + // 如果不包含 vue 键,返回表示这不是 Vue 组件请求的对象 + return { vue: false, filename } + }export default function pluginVue() { return { name: 'rollup-plugin-vue', + async resolveId(id) { + // 调用 parseVuePartRequest 函数解析模块 ID + const query = parseVuePartRequest(id); + // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块 + if (query.vue) { + return id + } + // 如果不是 Vue 组件请求,返回 null,表示这个模块 ID 不应该由这个插件处理 + return null + }, // 省略... } } 我们通过封装一个 parseVuePartRequest 函数来判断是否 Vue 组件请求,主要判断依据就是文件ID的 ? 后面部分的参数中是否存在 vue 键,这就是我们上述构建虚拟模块的时候为什么要设置 ?vue&type=script 部分的原因,就是为了后面可以进行区分。
同时我们需要安装 querystring 依赖模块: pnpm add querystring -D
这样我们再次运行测试的时候,结果如下:

当代码可以走到这一步的时候,说明我们的代码是没有问题的。
根据我们在前一篇文章中所学到的知识可以知道当在 resolveId 钩子中返回了一个虚拟模块的ID之后,那么接下来就轮到 load 钩子进行处理了,那么还是根据前面所学的知识知道可以在 load 钩子中返回虚拟模块的代码,所以我们就可以在 load 钩子中根据模块ID 中的 type 类型来判断是返回 script 部分的代码还是 template 部份的代码。
插件代码改动如下:
复制代码// 省略... export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... + load(id) { + // 调用 parseVuePartRequest 函数解析模块 ID + const query = parseVuePartRequest(id); + // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块 + if (query.vue) { + if (query.type === 'script') { + // 返回 script 部分的代码 + } else if (query.type === 'template') { + // 返回 template 部分的代码 + } + } + }, // 省略... } } 那么问题来了,我们在 load 钩子中返回 script 和 template 部分的代码呢?通过上文我们知道 script 和 template 部分的代码已经在 transform 钩子中进行了编译,怎么可以在 load 钩子中将 transform 钩子中编译好的代码获取到呢?我们可以在 transform 钩子中将 script 和 template 部分的编译结果缓存起来,然后在 load 钩子中就可以获取缓存中对应的编译结果了。
设置编译缓存
我们通过以下方式进行设置 script 和 template 部分的缓存。代码如下:
复制代码+ const cache = new Map(); // 省略... export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... load(id) { // 调用 parseVuePartRequest 函数解析模块 ID const query = parseVuePartRequest(id); // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块 if (query.vue) { if (query.type === 'script') { // 返回 script 部分的代码 + const code = cache.get(id).content + return { + code + } } else if (query.type === 'template') { // 返回 template 部分的代码 + const code = cache.get(id).code + return { + code + } } } }, transform(code, id) { if (/.vue$/.test(id)) { // 省略 ... - const scriptImport = `import script from '${id}?vue&type=script'n` + const scriptID = `${id}?vue&type=script`; + const scriptImport = `import script from '${scriptID}'n` - const templateImport = `import { render } from '${id}?vue&type=template'n` + const templateID = `${id}?vue&type=template`; + const templateImport = `import { render } from '${templateID}'n` + // 缓存编译结果 + cache.set(scriptID, scriptCode) + cache.set(templateID, templateCode) // 组装 const renderReplace = `script.render = rendern` const exportDefault = `export default script` return { // 省略... } } } } } 在上述代码中我们通过 Map 对象的实例 cache 来缓存编译后的结果,这个缓存是以我们自己构建的虚拟模块 ID 为键,然后设置对应的 script 或 template 部分的编译结果为键值。
我们运行上述代码的时候却报错了:

这个报错是说我们在读取缓存结果的时候读取不到,那么我们在相关的地方打断点进行测试,最后发现了以下问题。
我们初始设置缓存的键是下面这样的:

在 load 钩子中却变成了:

我们发现路径没有被正确地转义。这是 Rollup 内部处理的,我们可以通过 JSON.stringify 确保任何特殊字符(如路径中的空格、查询参数中的特殊符号等)都被正确地转义,从而生成一个合法的模块请求路径。所以我们修改如下:
复制代码// 省略... export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... transform(code, id) { if (/.vue$/.test(id)) { // 省略... // 脚本导入的虚拟模块 const scriptID = `${id}?vue&type=script`; + const scriptRequest = JSON.stringify(scriptID); - const scriptImport = `import script from '${scriptID}'n` + const scriptImport = `import script from ${scriptRequest}n` // 模板导入的虚拟模块 const templateID = `${id}?vue&type=template`; + const templateRequest = JSON.stringify(templateID); - const templateImport = `import { render } from '${templateID}'n` + const templateImport = `import { render } from ${templateRequest}n` // 缓存编译结果 cache.set(scriptID, scriptCode) cache.set(templateID, templateCode) // 省略... return { // 省略... } } } } } 我们再次运行代码,发现成功了。

编译结果如下:

运行编译结果代码
在前面一小节,我们实现了对 Vue 文件的编译,因为我们的打包输出格式是 ESM 的,所以我们可以通过以下的方式进行测试。
我们回到根目录的 src/main.js 中修改测试代码如下:
复制代码import { createApp } from 'vue' import App from '../rollup-plugin/dist/output'const app = createApp(App) app.mount('#app') 接着修改根目录的 vite.config.js 的文件内容:
复制代码import { defineConfig } from 'vite' import Vue from '@vitejs/plugin-vue'export default defineConfig({ plugins: [ Vue(), ] }) 如果你还没要安装 Vue3 的依赖包,那么先安装一下 Vue3 的依赖包。
我们在根目录启动 Vite 开发模式,最后我们可以看到 Vite 项目正常启动了。结果如下:

在浏览器的网络窗口,我们也可以看到打包的 Vue 文件也被加载了,结果如下:

日志打印窗口也打印了组件中 setup 方法的日志输出:

编译 style 模块
对于 style 模块,跟 script 和 template 模块一样,需要通过 @vue/compiler-sfc 包将 Vue 本身赋予 style 模块的能力代码进行编译,比如是否使用了 scoped 属性,然后需要对 scoped 属性的处理,总的来说需要处理以下的问题:
- 如果样式使用了
scoped属性的,编译器会生成唯一的 CSS 类名,确保样式不会影响其他组件; - 如果 style 中使用了
v-bind指令来绑定 script 模块中的响应式变量,那么需要将v-bind指令转换为 CSS 变量; - 同时使用了
scoped样式无法穿透到子组件,那么需要使用:deep()指令,所以当遇到:deep()指令时,编译器会特殊处理这些选择器,确保它们能够应用到子组件的 DOM 结构中。 - 如果使用了 CSS 模块化 (module),也需要进行编译处理
经过 @vue/compiler-sfc 包编译之后,可能还需要通过 sass、less、stylus 编译器处理成普通的 CSS 代码。这样处理后 Vue 文件中 style 部分的样式就会被转换成标准的 CSS,并插入到最终打包后的 JavaScript 代码中,或者在单独的 CSS文件中引用。
而我们这里暂时只讲插入打包后的 JavaScript 代码中的情况,那么怎么将编译后的普通 CSS 代码插入打包后的 JavaScript 代码中呢?
比如我们上述的 App.vue 文件的 style 部分的 CSS 代码最终会编译成:
复制代码div { color: red; } 很明显这段代码是字符串,是不能直接放到 JavaScript 文件中的,那么我们可以通过一个变量把它存起来,这样它就可以放到 JavaScript 文件中了。
复制代码const cssBlock = ` div { color: red; } ` 那么我们再通过 JavaScript 代码动态地添加到HTML页面上即可,实现也很简单,直接用 createElement 创建 style 标签元素,然后插入到页面 head 元素内即可,代码如下:
复制代码const cssBlock = `div {color: red;}` function styleInject(css) { // 获取页面 head 标签元素 const head = document.getElementsByTagName('head')[0] // 创建 style 元素 const style = document.createElement('style') style.type = 'text/css' // 插入到页面 `head` 元素内 head.appendChild(style) // 添加 style 元素的 CSS 内容 style.appendChild(document.createTextNode(css)) }styleInject(cssBlock) 这个就是实现编译后的 CSS 代码添加到页面上的实现原理。接下来我们来进行具体的实现。
跟 script 和 template 模块一样,style 部分我们也需要通过虚拟模块来处理,而 style 部分可能存在多个,所以我们的代码实现如下:
复制代码export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... transform(code, id) { if (/.vue$/.test(id)) { // 省略... + // 编译 style 模块 + let stylesImport = `` + if (result.descriptor.styles.length) { + result.descriptor.styles.forEach((style, i) => { + // 样式导入的虚拟模块 + const query = `?vue&type=style&index=${i}&lang.css` + const styleRequest = id + query + stylesImport += `nimport ${JSON.stringify(styleRequest)}` + // 编译 style 模块 + const styleCode = compiler.compileStyle({ + id: `data-v-${scopeId}`, + source: style.content, + scoped: style.scoped // 是否使用 scoped + }) + // 缓存 style 模块 + cache.set(styleRequest, styleCode) + }) + } // 省略... return { code: `+ ${stylesImport} ` } } } } } 值得注意的是我们在加载 style 文件路径参数后面加了 lang.css 的部分,以此来区分是哪种 style 预处理语言,如果是 scss 那么就以 lang.scss 结尾,以此类推,因为我们在 App.vue 文件中是只使用了普通的 CSS,所以我们是以 .css 结尾。
接着我们需要在 load 钩子里从缓存中加载 style 部分的代码,代码实现如下:
复制代码export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... load(id) { // 调用 parseVuePartRequest 函数解析模块 ID const query = parseVuePartRequest(id); // 解析结果如果 query.vue 为 true 表示这是一个 Vue 组件请求,则返回原始的模块 ID,这样该插件后续就会继续处理这个模块 if (query.vue) { if (query.type === 'script') { // 省略... + } else if (query.type === 'style') { + // 返回 style 部分的代码 + const code = cache.get(id).code + return { + code, + } + } } }, // 省略... } } 我们知道目前在 load 中加载的 CSS 代码是不能直接放到 JavaScript 文件中的,所以我们需要在 transform 中进行处理,当然也可以在 load 中处理好再进行返回,我们现在的处理方案是放到 transform 中进行处理,而方案就是我们在上面已经提到的动态添加 CSS 代码到页面的方案。
复制代码export default function pluginVue() { return { name: 'rollup-plugin-vue', // 省略... transform(code, id) { if (/.vue$/.test(id)) { // 省略... + } else if (/.css$/.test(id)) { + const styleCode = ` + const cssBlock = '${JSON.stringify(code)}';n + function styleInject(css) {n + const head = document.getElementsByTagName('head')[0]n + const style = document.createElement('style')n + style.type = 'text/css'n + head.appendChild(style)n + style.appendChild(document.createTextNode(css))n + }n + styleInject(cssBlock)n + ` + return { + code: styleCode + } + } } } } 这时我们在 ./rollup-plugin/ 目录下再执行 pnpm build,我们就可以看到 ./rollup-plugin/dist/output.js 文件,已经把我们的 CSS 代码编译进去了。

这时我们再回到根目录运行我们 ./src/main.js 中的代码,我们发现我们 CSS 代码已经成功运行了:

至此我们的 Vue 文件中的 CSS 代码也实现了编译。
总结
通过本文的逐步推导,我们完整实现了一个 Rollup 插件对 Vue 单文件组件的编译流程。从插件的基础结构出发,我们利用 @vue/compiler-sfc 提供的 parse、compileScript、compileTemplate 和 compileStyle 等核心 API,将 Vue 文件中的 template、script 和 style 三部分分别提取并编译成独立的中间产物。其中 template 被编译为 render 函数,script(尤其是 )中的编译宏和 TypeScript 被转换为标准 JavaScript,style 则经过作用域处理(scoped)和 CSS 变量绑定(v-bind)后生成普通 CSS 字符串。这个分步编译的过程,不仅展示了 Vue 官方编译工具链的威力,也揭示了现代前端构建工具处理组件化框架时的底层逻辑。
在整个实现中,虚拟模块技术和缓存机制的巧妙结合是保证 Rollup 插件正确工作的关键。我们为每个编译好的部分(script、template、style)构造了带有查询参数的模块 ID(如 App.vue?vue&type=script),并在 resolveId 和 load 钩子中拦截这些请求,返回缓存中的编译结果。这种设计让 Rollup 能够像处理普通 JavaScript 模块一样处理 Vue 的不同组成部分,同时避免了重复编译,提升了构建性能。特别是对于 style 模块,我们通过注入运行时的 styleInject 函数,将 CSS 字符串动态添加到页面中,实现了样式模块的按需加载,这一策略在开发环境和生产打包中都具有实用价值。
最终,我们成功将编译后的代码整合成一个完整的 Vue 组件对象,并能够在真实项目中运行验证。这个插件的实现不仅涵盖了 Vue 单文件组件编译的核心路径,也为后续扩展其他特性(如 CSS 预处理器支持、自定义块处理、热更新等)预留了清晰的接口。通过这篇实战,读者可以深入理解 Rollup 插件的钩子执行顺序、虚拟模块的用法以及 Vue 官方编译工具的内部工作原理,从而具备自主定制和优化 Vue 构建流程的能力,为复杂的工程化场景打下坚实基础。
实际使用时,建议结合项目规模、依赖环境和团队习惯做取舍;先保证流程清晰和结果可验证,再逐步优化细节。
相关文章
- 海域重启什么时候上线攻略 07-30
- 彩虹六号手游如何对枪攻略 07-30
- 罗马拓荒录新手如何建城 07-30
- 驯龙之旅莱克角色强度如何攻略 07-30
- 赛尔号萨斯洛克如何样 07-30
- 谍惊蛰舞厅惨案如何触发攻略 07-30