阐述 Vite 源码中 `plugin-vue` 如何处理 SFC 的编译,并与 `Rollup` 兼容。

各位观众老爷,晚上好!我是今晚的特邀嘉宾,一位平平无奇的代码搬运工,今天要跟大家聊聊 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,就是这个翻译官。它的使命就是:

  1. 解析 SFC:.vue 文件拆解成 template、script、style 三个部分。
  2. 编译 SFC: 把 template 编译成 render 函数,把 script 编译成 JavaScript 代码,把 style 处理成 CSS。
  3. 集成 SFC: 把编译后的 template、script、style 组合在一起,形成一个 Vue 组件。
  4. 与 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-sfcparse 函数,把 .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 函数的流程大概是这样:

  1. 处理 script: 调用 transformScript 函数,把 script 块编译成 JavaScript 代码。
  2. 处理 template: 调用 transformTemplate 函数,把 template 块编译成 render 函数。
  3. 处理 style: 调用 transformStyle 函数,把 style 块处理成 CSS 代码。
  4. 拼接结果: 把 script、template、style 的编译结果拼接在一起,形成最终的 JavaScript 代码。
  5. 添加 HMR 代码: 如果是开发环境,并且 SFC 包含 script 或 template,则添加热更新代码。

咱们接下来分别看看 transformScripttransformTemplatetransformStyle 这三个函数是如何工作的。

第四幕: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 函数主要做了这些事情:

  1. 获取 script 块: 从 descriptor 中获取 script 或 scriptSetup 块。
  2. 调用 compileScript: 使用 @vue/compiler-sfccompileScript 函数,把 script 块编译成 JavaScript 代码。compileScript 函数会根据 script 块的类型(Options API 或 Composition API),生成不同的代码。
  3. 返回结果: 返回编译后的 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 函数主要做了这些事情:

  1. 获取 template 块: 从 descriptor 中获取 template 块。
  2. 调用 compileTemplate: 使用 @vue/compiler-sfccompileTemplate 函数,把 template 块编译成 render 函数。compileTemplate 函数会把 template 块解析成 AST(Abstract Syntax Tree),然后把 AST 转换成 render 函数的 JavaScript 代码。
  3. 返回结果: 返回编译后的 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 函数主要做了这些事情:

  1. 获取 style 块: 从 descriptor 中获取 style 块。
  2. 调用 compileStyleAsync: 使用 @vue/compiler-sfccompileStyleAsync 函数,把 style 块编译成 CSS 代码。compileStyleAsync 函数会根据 style 块的类型(普通 CSS、scoped CSS 或 CSS Modules),生成不同的代码。
  3. 处理 CSS Modules: 如果 style 块是 CSS Modules,则把 CSS 代码转换成 JavaScript 对象,并添加到组件的 data 选项中。
  4. 添加 CSS 代码: 把 CSS 代码插入到 HTML 中。

第七幕:与 Rollup 的和谐共处

plugin-vue 需要与 Rollup 兼容,才能让 Vite 构建出来的代码能够顺利地被 Rollup 处理。plugin-vue 通过以下方式与 Rollup 兼容:

  • 使用 Rollup 的 API: plugin-vue 使用 Rollup 提供的 API,例如 createFiltertransformhandleHotUpdate 等。
  • 返回标准的 Rollup 模块: plugin-vuetransform 函数返回的是标准的 Rollup 模块,包含了 JavaScript 代码、sourcemap 和依赖关系。
  • 处理 Rollup 的钩子函数: plugin-vue 处理 Rollup 的钩子函数,例如 configResolvedtransformhandleHotUpdate 等,以便在 Rollup 的构建过程中执行自定义的逻辑。

总结陈词

好了,各位观众老爷,今天的讲座就到这里。咱们一起回顾一下 plugin-vue 的核心要点:

  • plugin-vue 是一个 Vite 插件,负责把 Vue 的 SFC 编译成浏览器可以理解的代码。
  • plugin-vue 使用 @vue/compiler-sfc 这个核心库来完成 SFC 的编译工作。
  • plugin-vuetransform 函数是核心函数,负责把 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 的单文件组件编译器,提供 parsecompileScriptcompileTemplatecompileStyleAsync 等 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 的基本原理,理解这些细节就不是什么难事了。

感谢大家的观看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注