• Vue 文件是如何被转换并渲染到页面的?


    以前常常觉得,Vue 文件(单文件组件,Single File Component,SFC)的处理非常复杂,以至于很久一段时间,都不敢接触它,直到我看了 @vite/plugin-vue 的源码,才发现,这个过程并没有多复杂。因为 Vue 已经提供了 SFC 的编译能力,我们只需要站在巨人的肩膀上,简单地组合利用这些能力即可。

    本文会用一个极其简单的例子,来说明如何处理一个 Vue 文件,并将其展示到页面中。在这个过程中,介绍 Vue 提供的编译能力,以及如何组合利用这些能力。

    学完之后,你会明白 Vue 文件是如何一步一步被转换成 js 代码的,也能理解 viterollup 这些打包工具,是如何对 Vue 文件进行打包的。

    本文用到的项目,在该 Github 仓库中,喜欢自己动手的同学,可以下载下来玩玩

    一个简单的例子

    有一个 main.vue 文件如下:

    
    
    
    
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接下来,我会一步一步带大家手动处理这个 Vue 文件,并将其展示到页面中。

    我们首先来了解一下,如果不使用 Vue 文件,不进行编译,要如何使用 Vue

    在浏览器直接使用 Vue

    这是 Vue 官方文档提供的一个例子

    
    
    Title
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    利用 script 标签全局加载 Vue,通过全局变量 window.Vue 来获取 Vue 模块。然后定义组件,创建 Vue 实例,并挂载到对应的 DOM。

    页面效果如下:

    上面的例子,是使用 js 来定义组件的。

    那么如果我们用 Vue SFC 来定义组件,就需要将 Vue 文件,编译成 js 对象形式的 Vue 组件对象(像上述例子一样)

    Vue 文件主要由 3 部分组成:

    • script 脚本
    • template 模板,可选
    • style 样式,可选

    要分别将这三部分,转换成 js 并组合成一个 Vue 对象,浏览器才能正确的运行

    如何编译 Vue SFC?

    Vue 提供了 @vue/compiler-sfc专门用于 Vue 文件的预编译。下面我会一步一步演示 @vue/compiler-sfc 的使用方法。

    解析 Vue 文件

    在进行处理之前,首先要读取到代码的字符串

    import { readFile, writeFile } from "fs-extra";
    const file = await readFile("./src/main.vue", "utf8"); 
    
    • 1
    • 2

    然后用 @vue/compiler-sfc 提供的解析器,对代码进行解析

    import { parse } from "@vue/compiler-sfc";
    const { descriptor, error } = parse(file); 
    
    • 1
    • 2

    这个是 Vue 文件的内容

    
    
    
    
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    下图是 descriptor 的解析结果

    其实 parse 函数,就是把一个 Vue 文件,分成 3 个部分:

    • template
    • script 块和 scriptSetup
    • 多个style

    这一步做的是解析,其实并没有对代码进行编译,可以看到,每个块的 content 字段,都是跟 Vue 文件是相同的。

    值得注意的是,script 包括 script 块和 scriptSetup 块,scriptSetup 块在图中没有标注,是因为刚好我们的 Vue 文件,没有使用 script setup 的特性,因此它的值为空。

    style 块允许有多个,因为可以同时出现多个 style 标签,而其他标签只能有一个(scriptscript setup 能同时存在各一个)。

    解析的目的,是将一个 Vue 文件中的内容,拆分成不同的块,然后分别对它们进行编译

    编译 script

    编译 script 的目的有如下几个:

    • 处理 script setup 的代码, script setup 的代码是不能直接运行的,需要进行转换。
    • 合并 scriptscript setup 的代码。
    • 处理 CSS 变量注入
    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 }); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    compileScript 返回结果如下:

    import { ref } from "vue";
    
    export default {name: "Main",setup() {const message = ref("Main");return {message,};},
    }; 
    
    • 1
    • 2
    • 3
    • 4

    可以看出编译后的 script没有变化,因为这里的确不需要任何处理

    如果有 script setup 或者 css 变量注入,编译后的代码就会有变化,感兴趣的可以看看 main-with-css-inject.vuemain-with-script-setup.vue 这两个文件的编译结果。

    编译 template

    编译 template,目的是template 转成 render 函数

    import { compileTemplate } from "@vue/compiler-sfc";
    
    // 编译模板,转换成 render 函数
    const template = compileTemplate({source: descriptor.template.content,filename: "main.vue", // 用于错误提示id: scopeId,
    }); 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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 */))
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这段代码,看起来好像一个函数都不认识。但其实,你只要把 _createElementBlock 当成 Vue.h 渲染函数来看,你就觉得非常熟悉了。

    现在有了 scriptrender 函数,其实已经是可以把一个组件显示到页面上了,样式可以先不管,我们先把组件渲染出来,然后再加上样式

    组合 script 和 render 函数

    目前 scriptrender 函数,它们都是各自一个模块,而我们需要的是一个完整的 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 之后就直接创建