各位观众老爷,晚上好!我是今晚的特邀嘉宾,一位平平无奇的代码搬运工,今天要跟大家聊聊 Vite 里面 plugin-vue
这位老朋友,看看它如何把 Vue 的 SFC(Single-File Components,单文件组件)给安排得明明白白,还能跟 Rollup 这位老大哥处得来。
咱们先打个预防针,这玩意儿涉及的代码量不小,我会尽量深入浅出,但还是需要大家集中注意力,准备好瓜子饮料,咱们这就开讲!
第一幕:SFC 的前世今生和 plugin-vue
的使命
话说当年 Vue 横空出世,SFC 这种写法简直是惊艳四座。把 template、script、style 三位一体,写在一个 .vue
文件里,代码组织度直接拉满,开发体验蹭蹭往上涨。
但浏览器它不懂啊!浏览器只认识 HTML、JavaScript、CSS,.vue
文件对它来说就是天书。所以,我们需要一个翻译官,把 .vue
文件翻译成浏览器能理解的东西。
plugin-vue
,就是这个翻译官。它的使命就是:
- 解析 SFC: 把
.vue
文件拆解成 template、script、style 三个部分。 - 编译 SFC: 把 template 编译成 render 函数,把 script 编译成 JavaScript 代码,把 style 处理成 CSS。
- 集成 SFC: 把编译后的 template、script、style 组合在一起,形成一个 Vue 组件。
- 与 Rollup 兼容: 让 Vite 构建出来的代码,能够顺利地被 Rollup 处理。
第二幕:plugin-vue
的内部结构剖析
plugin-vue
本身是一个 Vite 插件,它基于 @vue/compiler-sfc
这个核心库来完成 SFC 的编译工作。咱们先看看 plugin-vue
的基本结构:
// 源码位置:packages/plugin-vue/src/index.ts
import { createFilter } from '@rollup/pluginutils'
import { parse } from '@vue/compiler-sfc'
import { transform } from './transform'
import { handleHotUpdate } from './handleHotUpdate'
import { resolveTemplateCompilerOptions } from './templateCompiler'
export function createVuePlugin(options: VuePluginOptions = {}): Plugin {
const {
include = /.vue$/,
exclude,
isProduction = process.env.NODE_ENV === 'production',
compiler,
compilerOptions,
template,
script,
style,
preprocessStyles,
customElement,
defineModel,
} = options
const filter = createFilter(include, exclude)
let resolvedCompiler: SFCCompiler
let customElementFilter: (tag: string) => boolean
return {
name: 'vite:vue',
// ...省略其他钩子函数
configResolved(config) {
// ...省略配置解析逻辑
},
transform(code, id) {
if (!filter(id)) {
return
}
const { descriptor, errors } = parse(code, {
filename: id,
sourceMap: true,
})
if (errors.length) {
errors.forEach((e) => {
this.error(e)
})
return null
}
// 调用 transform 函数进行转换
return transform(
code,
id,
descriptor,
options,
this,
resolvedCompiler,
config.root,
config.command === 'build',
)
},
handleHotUpdate(ctx) {
if (!filter(ctx.file)) {
return
}
return handleHotUpdate(ctx, this, resolvedCompiler)
},
}
}
export default createVuePlugin
简单来说,plugin-vue
主要做了这些事情:
createFilter
: 创建一个过滤器,用来判断哪些文件需要被处理(通常是.vue
文件)。parse
: 使用@vue/compiler-sfc
的parse
函数,把.vue
文件解析成一个描述符(descriptor),包含了 template、script、style 等信息。transform
: 这是核心函数,负责把 SFC 转换成 JavaScript 代码。handleHotUpdate
: 处理热更新,当 SFC 文件发生变化时,更新浏览器中的组件。
第三幕:transform
函数的魔法表演
transform
函数是 plugin-vue
的灵魂所在,它决定了 SFC 如何被编译成 JavaScript 代码。咱们深入看看 transform
函数的实现:
// 源码位置:packages/plugin-vue/src/transform.ts
import {
SFCDescriptor,
SFCTemplateBlock,
SFCStyleBlock,
ScriptCompilerOptions,
TemplateCompilerOptions,
SFCAsyncStyleCompileOptions,
} from '@vue/compiler-sfc'
import { PluginContext } from 'rollup'
import { ResolvedOptions, SFCCompiler } from '.'
import { transformTemplate } from './templateTransform'
import { transformStyle } from './styleTransform'
import { transformScript } from './scriptTransform'
import { StyleIntegrationMode } from './style/index'
export async function transform(
code: string,
id: string,
descriptor: SFCDescriptor,
options: ResolvedOptions,
pluginContext: PluginContext,
compiler: SFCCompiler,
root: string,
isProduction: boolean,
) {
const { filename, cssModules } = descriptor
const hasScoped = descriptor.styles.some((s) => s.scoped)
const hasCSSModules = Object.keys(cssModules).length > 0
// 1. 处理 script
const scriptResult = await transformScript(
descriptor,
id,
options,
compiler,
isProduction,
)
// 2. 处理 template
const templateResult = descriptor.template
? await transformTemplate(
descriptor,
id,
options,
compiler,
scriptResult?.bindings,
isProduction,
)
: null
// 3. 处理 style
const stylesResult = await Promise.all(
descriptor.styles.map((style, index) =>
transformStyle(
code,
id,
descriptor,
index,
options,
pluginContext,
compiler,
isProduction,
scriptResult?.bindings,
),
),
)
// 4. 拼接结果
let codeToReturn = `n${scriptResult?.code || ''}n`
if (templateResult) {
codeToReturn += `n${templateResult.code}n`
}
if (stylesResult.length) {
codeToReturn += `n${stylesResult
.map((styleResult) => styleResult.code)
.join('n')}n`
}
// 5. 添加 HMR 代码(如果需要)
if (
!isProduction &&
(descriptor.script || descriptor.scriptSetup || descriptor.template)
) {
codeToReturn += `n${genHotReloadCode(descriptor, id)}n`
}
return {
code: codeToReturn,
map: scriptResult?.map, // 暂时只返回 script 的 sourcemap
}
}
transform
函数的流程大概是这样:
- 处理 script: 调用
transformScript
函数,把 script 块编译成 JavaScript 代码。 - 处理 template: 调用
transformTemplate
函数,把 template 块编译成 render 函数。 - 处理 style: 调用
transformStyle
函数,把 style 块处理成 CSS 代码。 - 拼接结果: 把 script、template、style 的编译结果拼接在一起,形成最终的 JavaScript 代码。
- 添加 HMR 代码: 如果是开发环境,并且 SFC 包含 script 或 template,则添加热更新代码。
咱们接下来分别看看 transformScript
、transformTemplate
、transformStyle
这三个函数是如何工作的。
第四幕:transformScript
的剧本改编
transformScript
函数负责把 SFC 中的 script 块编译成 JavaScript 代码。它可以处理两种 script 块:
<script>
: 传统的 script 块,使用 Options API。<script setup>
: Vue 3 新增的 script 块,使用 Composition API。
// 源码位置:packages/plugin-vue/src/scriptTransform.ts
import {
SFCDescriptor,
SFCScriptBlock,
ScriptCompileOptions,
BindingMetadata,
CompilerOptions,
} from '@vue/compiler-sfc'
import { ResolvedOptions, SFCCompiler } from '.'
import { genHotReloadCode } from './hmr'
export async function transformScript(
descriptor: SFCDescriptor,
id: string,
options: ResolvedOptions,
compiler: SFCCompiler,
isProduction: boolean,
): Promise<{ code: string; map?: any; bindings?: BindingMetadata } | null> {
let script = descriptor.script || descriptor.scriptSetup
if (!script) {
return null
}
const { bindings, code, map } = compiler.compileScript(descriptor, {
id,
sourceMap: true,
inlineTemplate: false, // template 将会被单独处理
templateOptions: {
ssr: options.template?.ssr,
compilerOptions: options.template?.compilerOptions,
},
// ...省略其他选项
})
return {
code,
map,
bindings,
}
}
transformScript
函数主要做了这些事情:
- 获取 script 块: 从 descriptor 中获取 script 或 scriptSetup 块。
- 调用
compileScript
: 使用@vue/compiler-sfc
的compileScript
函数,把 script 块编译成 JavaScript 代码。compileScript
函数会根据 script 块的类型(Options API 或 Composition API),生成不同的代码。 - 返回结果: 返回编译后的 JavaScript 代码、sourcemap 和 bindings。
第五幕:transformTemplate
的渲染魔法
transformTemplate
函数负责把 SFC 中的 template 块编译成 render 函数。render 函数是一个 JavaScript 函数,它接收一个 h 函数(createElement 的别名),返回一个 VNode(Virtual DOM Node)。
// 源码位置:packages/plugin-vue/src/templateTransform.ts
import {
SFCDescriptor,
SFCTemplateBlock,
TemplateCompilerOptions,
} from '@vue/compiler-sfc'
import { ResolvedOptions, SFCCompiler } from '.'
export async function transformTemplate(
descriptor: SFCDescriptor,
id: string,
options: ResolvedOptions,
compiler: SFCCompiler,
bindingMetadata: Record<string, any> | undefined,
isProduction: boolean,
): Promise<{ code: string; map?: any } | null> {
const template = descriptor.template
if (!template) {
return null
}
const { code, map } = compiler.compileTemplate({
id,
filename: descriptor.filename,
source: template.content,
sourceMap: true,
transformAssetUrls: options.template?.transformAssetUrls,
transformAssetUrlsOptions: options.template?.compilerOptions?.transformAssetUrls,
compilerOptions: {
...options.template?.compilerOptions,
bindingMetadata,
},
ssr: options.template?.ssr,
})
return {
code,
map,
}
}
transformTemplate
函数主要做了这些事情:
- 获取 template 块: 从 descriptor 中获取 template 块。
- 调用
compileTemplate
: 使用@vue/compiler-sfc
的compileTemplate
函数,把 template 块编译成 render 函数。compileTemplate
函数会把 template 块解析成 AST(Abstract Syntax Tree),然后把 AST 转换成 render 函数的 JavaScript 代码。 - 返回结果: 返回编译后的 render 函数的 JavaScript 代码和 sourcemap。
第六幕:transformStyle
的造型艺术
transformStyle
函数负责把 SFC 中的 style 块处理成 CSS 代码。它可以处理多种 style 块:
- 普通的 CSS: 直接把 CSS 代码插入到 HTML 中。
- scoped CSS: 为 CSS 代码添加作用域,防止 CSS 样式污染。
- CSS Modules: 把 CSS 代码转换成 JavaScript 对象,方便在组件中使用。
// 源码位置:packages/plugin-vue/src/styleTransform.ts
import {
SFCDescriptor,
SFCStyleBlock,
StyleCompileOptions,
} from '@vue/compiler-sfc'
import { PluginContext } from 'rollup'
import { ResolvedOptions, SFCCompiler } from '.'
import { StyleIntegrationMode } from './style/index'
export async function transformStyle(
code: string,
id: string,
descriptor: SFCDescriptor,
index: number,
options: ResolvedOptions,
pluginContext: PluginContext,
compiler: SFCCompiler,
isProduction: boolean,
bindingMetadata: Record<string, any> | undefined,
): Promise<{ code: string; map?: any }> {
const style = descriptor.styles[index]
const result = await compiler.compileStyleAsync({
filename: descriptor.filename,
source: style.content,
id: `data-v-${descriptor.id}`,
scoped: style.scoped,
modules: style.module || false,
preprocessLang: style.lang as any,
preprocessOptions: options.style?.preprocessOptions,
// ...省略其他选项
})
let cssCode = result.code
if (style.module) {
// CSS Modules
const cssModules = result.modules
// ...
} else {
// 普通 CSS 或 scoped CSS
cssCode = result.code
}
let codeToReturn = `nconst css = ${JSON.stringify(cssCode)};n`
codeToReturn += `nimport { injectStyles } from 'vite-plugin-vue/dist/client';ninjectStyles(${JSON.stringify(id)}, css)n`
return {
code: codeToReturn,
}
}
transformStyle
函数主要做了这些事情:
- 获取 style 块: 从 descriptor 中获取 style 块。
- 调用
compileStyleAsync
: 使用@vue/compiler-sfc
的compileStyleAsync
函数,把 style 块编译成 CSS 代码。compileStyleAsync
函数会根据 style 块的类型(普通 CSS、scoped CSS 或 CSS Modules),生成不同的代码。 - 处理 CSS Modules: 如果 style 块是 CSS Modules,则把 CSS 代码转换成 JavaScript 对象,并添加到组件的
data
选项中。 - 添加 CSS 代码: 把 CSS 代码插入到 HTML 中。
第七幕:与 Rollup 的和谐共处
plugin-vue
需要与 Rollup 兼容,才能让 Vite 构建出来的代码能够顺利地被 Rollup 处理。plugin-vue
通过以下方式与 Rollup 兼容:
- 使用 Rollup 的 API:
plugin-vue
使用 Rollup 提供的 API,例如createFilter
、transform
、handleHotUpdate
等。 - 返回标准的 Rollup 模块:
plugin-vue
的transform
函数返回的是标准的 Rollup 模块,包含了 JavaScript 代码、sourcemap 和依赖关系。 - 处理 Rollup 的钩子函数:
plugin-vue
处理 Rollup 的钩子函数,例如configResolved
、transform
、handleHotUpdate
等,以便在 Rollup 的构建过程中执行自定义的逻辑。
总结陈词
好了,各位观众老爷,今天的讲座就到这里。咱们一起回顾一下 plugin-vue
的核心要点:
plugin-vue
是一个 Vite 插件,负责把 Vue 的 SFC 编译成浏览器可以理解的代码。plugin-vue
使用@vue/compiler-sfc
这个核心库来完成 SFC 的编译工作。plugin-vue
的transform
函数是核心函数,负责把 SFC 转换成 JavaScript 代码。plugin-vue
通过使用 Rollup 的 API、返回标准的 Rollup 模块和处理 Rollup 的钩子函数,与 Rollup 兼容。
希望今天的讲座对大家有所帮助。如果大家还有什么疑问,欢迎在评论区留言,我会尽力解答。谢谢大家!
表格总结
函数/组件 | 职责 |
---|---|
createVuePlugin |
Vite 插件入口,创建 plugin-vue 插件实例。 |
parse |
使用 @vue/compiler-sfc 解析 SFC 文件,生成 SFC 描述符(SFCDescriptor )。 |
transform |
核心转换函数,负责将 SFC 描述符中的 script、template、style 块分别进行编译,并将编译后的代码拼接成最终的 JavaScript 模块代码。 |
transformScript |
编译 SFC 中的 script 块,生成 JavaScript 代码。支持 Options API 和 Composition API。 |
transformTemplate |
编译 SFC 中的 template 块,生成 render 函数。 |
transformStyle |
处理 SFC 中的 style 块,生成 CSS 代码。支持普通 CSS、scoped CSS 和 CSS Modules。 |
handleHotUpdate |
处理 SFC 的热更新,当 SFC 文件发生变化时,更新浏览器中的组件。 |
@vue/compiler-sfc |
Vue 的单文件组件编译器,提供 parse 、compileScript 、compileTemplate 、compileStyleAsync 等 API,用于解析和编译 SFC。 |
injectStyles |
注入 CSS 到页面的客户端运行时函数(vite-plugin-vue/dist/client.js中定义)。 |
补充说明
虽然我没有直接列出所有代码,但是我已经把关键的流程和重要的函数都详细地解释了一遍。希望大家能够理解 plugin-vue
的基本原理和实现方式。如果大家想深入了解 plugin-vue
的源码,可以去 Vite 的 GitHub 仓库查看。
彩蛋
其实,plugin-vue
还有很多细节没有讲到,比如:
- 如何处理 SFC 中的自定义块(custom blocks)。
- 如何处理 SFC 中的 TypeScript 代码。
- 如何处理 SFC 中的 i18n 代码。
这些细节都比较复杂,需要大家自己去探索。但是,只要掌握了 plugin-vue
的基本原理,理解这些细节就不是什么难事了。
感谢大家的观看!