以前常常觉得,Vue 文件(单文件组件,Single File Component,SFC)的处理非常复杂,以至于很久一段时间,都不敢接触它,直到我看了 @vite/plugin-vue
的源码,才发现,这个过程并没有多复杂。因为 Vue 已经提供了 SFC 的编译能力,我们只需要站在巨人的肩膀上,简单地组合利用这些能力即可。
本文会用一个极其简单的例子,来说明如何处理一个 Vue 文件,并将其展示到页面中。在这个过程中,介绍 Vue 提供的编译能力,以及如何组合利用这些能力。
学完之后,你会明白 Vue 文件是如何一步一步被转换成 js 代码的,也能理解 vite
、rollup
这些打包工具,是如何对 Vue 文件进行打包的。
本文用到的项目,在该 Github 仓库中,喜欢自己动手的同学,可以下载下来玩玩
有一个 main.vue 文件如下:
接下来,我会一步一步带大家手动处理这个 Vue 文件,并将其展示到页面中。
我们首先来了解一下,如果不使用 Vue 文件,不进行编译,要如何使用 Vue
这是 Vue 官方文档提供的一个例子
Title
利用 script
标签全局加载 Vue,通过全局变量 window.Vue
来获取 Vue 模块。然后定义组件,创建 Vue 实例,并挂载到对应的 DOM。
页面效果如下:
上面的例子,是使用 js
来定义组件的。
那么如果我们用 Vue SFC 来定义组件,就需要将 Vue 文件,编译成 js 对象形式的 Vue 组件对象(像上述例子一样)
Vue 文件主要由 3 部分组成:
script
脚本template
模板,可选style
样式,可选要分别将这三部分,转换成 js
并组合成一个 Vue 对象,浏览器才能正确的运行
Vue 提供了 @vue/compiler-sfc
,专门用于 Vue 文件的预编译。下面我会一步一步演示 @vue/compiler-sfc
的使用方法。
在进行处理之前,首先要读取到代码的字符串
import { readFile, writeFile } from "fs-extra";
const file = await readFile("./src/main.vue", "utf8");
然后用 @vue/compiler-sfc
提供的解析器,对代码进行解析
import { parse } from "@vue/compiler-sfc";
const { descriptor, error } = parse(file);
这个是 Vue 文件的内容
下图是 descriptor
的解析结果
其实 parse
函数,就是把一个 Vue 文件,分成 3 个部分:
template
块script
块和 scriptSetup
块style
块这一步做的是解析,其实并没有对代码进行编译,可以看到,每个块的 content
字段,都是跟 Vue 文件是相同的。
值得注意的是,script
包括 script
块和 scriptSetup
块,scriptSetup
块在图中没有标注,是因为刚好我们的 Vue 文件,没有使用 script setup
的特性,因此它的值为空。
style
块允许有多个,因为可以同时出现多个 style
标签,而其他标签只能有一个(script
和 script setup
能同时存在各一个)。
解析的目的,是将一个 Vue 文件中的内容,拆分成不同的块,然后分别对它们进行编译
编译 script
的目的有如下几个:
script setup
的代码, script setup
的代码是不能直接运行的,需要进行转换。script
和 script setup
的代码。import { compileScript } from "@vue/compiler-sfc";
// 这个 id 是 scopeId,用于 css scope,保证唯一即可
const id = Date.now().toString();
const scopeId = `data-v-${id}`;
// 编译 script,因为可能有 script setup,还要进行 css 变量注入
const script = compileScript(descriptor, { id: scopeId });
compileScript
返回结果如下:
import { ref } from "vue";
export default {name: "Main",setup() {const message = ref("Main");return {message,};},
};
可以看出编译后的 script
没有变化,因为这里的确不需要任何处理。
如果有 script setup
或者 css
变量注入,编译后的代码就会有变化,感兴趣的可以看看 main-with-css-inject.vue
或 main-with-script-setup.vue
这两个文件的编译结果。
编译 template
,目的是将 template
转成 render
函数
import { compileTemplate } from "@vue/compiler-sfc";
// 编译模板,转换成 render 函数
const template = compileTemplate({source: descriptor.template.content,filename: "main.vue", // 用于错误提示id: scopeId,
});
compileTemplate
函数返回值如下:
编译后的 render 函数如下:
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { class: "message" }
export function render(_ctx, _cache) {return (_openBlock(), _createElementBlock("div", _hoisted_1, _toDisplayString(_ctx.message), 1 /* TEXT */))
}
这段代码,看起来好像一个函数都不认识。但其实,你只要把 _createElementBlock
当成 Vue.h
渲染函数来看,你就觉得非常熟悉了。
现在有了 script
和 render
函数,其实已经是可以把一个组件显示到页面上了,样式可以先不管,我们先把组件渲染出来,然后再加上样式
目前 script
和 render
函数,它们都是各自一个模块,而我们需要的是一个完整的 Vue 对象,即 render
函数需要作为 Vue 对象的一个属性。
可以采用以下这种方案:
// 将 script 保存到 main.vue.script.js,拿到的是 Vue 对象
import script from '/src/main.vue.script.js'
// 将 render 函数保存到 main.vue.template.js,拿到的是 render 函数
import { render } from '/src/main.vue.template.js'
// 将 style 函数保存到 main.vue.style.js,import 之后就直接创建