◆

Vue 相关依赖包 compiler-sfc 与 plugin-vue 说明

2023-02-02 01:370

@vue/compiler-sfc

这个包是 Vue 核心仓库的一部分,始终和当前 vue 包保持相同的版本。 通常情况下,它将被列为项目中 vue 的同级依赖。为了确保不出问题, 应该始终和 vue 的版本保持同步:也就是说当你升级 vue 版本时候,应该同时升级 @vue/compiler-sfc 来保持一致。 这个包本身提供了处理 Vue 单文件的底层工具,只对库作者需要对单文件进行支持时候有意义。

@vitejs/plugin-vue

可以在 Vite 中提供对 Vue 单文件提供支持。

它自己不实现编译逻辑,编译全部转交给 @vue/compiler-sfc。它的职责是把 Vite 的插件生命周期和 compiler-sfc 的 API 接起来:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue({
      // 把哪些文件当 SFC 处理,默认只有 .vue
      include: [/\.vue$/],
      // 传给 compiler-sfc 的模板编译选项
      template: {
        compilerOptions: {
          // 把 ion- 开头的标签当自定义元素,不当组件解析
          isCustomElement: tag => tag.startsWith('ion-'),
        },
      },
    }),
  ],
});

Vite 里 SFC 的处理流程是这样的:resolveId / load 钩子拦下 .vue 请求,用 compiler-sfc 的 parse() 把文件拆成 <template> / <script> / <style> 描述符,再把每个块改写成一个带 query 参数的虚拟模块(比如 App.vue?vue&type=style&index=0&lang.scss),最后让 Vite 自己的 CSS / TS 处理链去消费这些虚拟模块。这个设计是它能天然复用 Vite 全部 loader 能力的原因——<style lang="scss"> 不需要插件做任何特殊处理,改写成 .scss 请求后 Vite 的内置 Sass 支持就接上了。

HMR 也在这一层:插件比对新旧描述符,只有 <template> 变了就走 rerender(组件状态保留),<script> 变了就走 reload(组件重新创建,状态丢失)。所以改模板不丢状态、改逻辑丢状态,是这个粒度决定的。

版本必须对齐的原因

原文提到 compiler-sfc 要和 vue 版本保持一致,这不是保守建议,是硬要求。SFC 编译产物里包含大量对运行时内部 API 的引用,编译器和运行时是配套演进的:

// 编译产物示意,这些下划线开头的都是运行时导出的内部 API
import {
  createElementVNode as _createElementVNode,
  openBlock as _openBlock,
} from 'vue';

编译器生成了新版运行时才有的 helper,配上旧运行时就会报 xxx is not a function;反过来旧编译器生成的产物在新运行时上也可能因为签名变更而行为异常。所以 Vue 3 从 3.2.13 开始把 @vue/compiler-sfc 作为 vue 包的依赖直接内置了,@vitejs/plugin-vue 也改成从 vue 包里间接取它。

实践中的结论是:Vue 3.2.13 及以上不需要单独装 @vue/compiler-sfc。如果 package.json 里还显式列着它,反而容易和 vue 的版本走岔,装出两份不同版本。真的装了两份,pnpm why @vue/compiler-sfc 一看就知道是谁拉进来的。

相关包一览

Vue 3 的编译体系被拆成了多个包,搞清各自定位能少走弯路:

  • @vue/compiler-core:与平台无关的编译核心,负责模板 AST 解析和 transform。
  • @vue/compiler-dom:在 core 之上加浏览器平台的处理(v-html、v-model、原生标签识别等)。
  • @vue/compiler-sfc:解析 .vue 文件、编译 <script setup> 语法糖、处理 scoped CSS 和 CSS Modules。
  • @vue/compiler-ssr:生成服务端渲染用的字符串拼接代码,产物形态和客户端完全不同。
  • @vue/runtime-core:与平台无关的运行时,包含虚拟 DOM diff、组件生命周期、setup 调度。
  • @vue/runtime-dom:在 runtime-core 之上加浏览器 DOM 操作和 Transition 等内置组件。
  • @vue/reactivity:响应式系统,可以脱离 Vue 独立使用。
  • vue:面向用户的入口包,聚合上面几个并附带浏览器构建产物。

对应到构建工具侧,Vite 用 @vitejs/plugin-vue(SFC)和 @vitejs/plugin-vue-jsx(JSX / TSX),Webpack 用 vue-loader。JSX 需要单独插件是因为它不走 SFC 那套模板编译,而是走 Babel 的 JSX transform。

运行时 + 编译器 vs 只有运行时

vue 包提供了多份构建产物,这里最容易搞混的是是否包含模板编译器:

  • vue/dist/vue.runtime.esm-bundler.js(默认):只有运行时,体积小约 14kb。要求模板在构建期就编译完,也就是所有模板都写在 .vue 文件里。
  • vue/dist/vue.esm-bundler.js:带完整编译器,可以在运行时编译字符串模板。

只有运行时的版本遇到运行时字符串模板会报错:Component provided template option but runtime compilation is not supported in this build of Vue。用到 template: '<div>{{ msg }}</div>' 这种写法(比如动态渲染后端返回的模板)就必须切到完整版:

// vite.config.js
export default defineConfig({
  resolve: {
    alias: {
      vue: 'vue/dist/vue.esm-bundler.js',
    },
  },
});

代价是包体积增加约 14kb(gzip 后),并且模板编译在客户端进行,首屏会多一点开销。能避免就避免——需要动态模板的场景通常可以用动态组件 <component :is="..."> 或者渲染函数替代。

另外 esm-bundler 系列产物里有 __VUE_OPTIONS_API__、__VUE_PROD_DEVTOOLS__ 这类编译期常量,是留给打包器做 tree shaking 的开关。Vite 已经预置了合理的默认值,但如果确定项目全用组合式 API,可以显式关掉 Options API 支持进一步减小体积:

export default defineConfig({
  define: {
    __VUE_OPTIONS_API__: 'false',
  },
});

注意值必须是字符串 'false' 而不是布尔 false,define 做的是源码级文本替换。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue 相关依赖包 compiler-sfc 与 plugin-vue 说明加载中