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

本文详细讲解了Vue单文件组件(SFC)如何被编译和渲染到页面,从一个简单的Vue文件出发,解析Vue文件结构,介绍如何使用@vue/compiler-sfc进行编译,包括script、template和style的处理,以及如何通过打包工具将编译后的代码转化为可在浏览器中运行的JavaScript。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

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

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

一个简单的例子

有一个 main.vue 文件如下:

<template><div class="message">{{ message }}</div>
</template>

<script> import { ref } from "vue";

export default {name: "Main",setup() {const message = ref("Main");return {message,};},
}; </script>

<style scoped> .message {font-size: 60px;font-weight: 900;
} </style> 

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

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

在浏览器直接使用 Vue

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

<!DOCTYPE html>
<html lang="en">
<head><script src="https://unpkg.com/vue@next"></script><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div id="app"></div>
</body>
<script> const Counter = {data() {return {counter: 0}},render(){return Vue.h('h1','hello-world')}}Vue.createApp(Counter).mount('#app') </script>
</html> 

利用 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"); 

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

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

这个是 Vue 文件的内容

<template><div class="message">{{ message }}</div>
</template>

<script> import { ref } from "vue";

export default {name: "Main",setup() {const message = ref("Main");return {message,};},
}; </script>

<style scoped> .message {font-size: 60px;font-weight: 900;
} </style> 

下图是 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 }); 

compileScript 返回结果如下:

import { ref } from "vue";

export default {name: "Main",setup() {const message = ref("Main");return {message,};},
}; 

可以看出编译后的 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,
}); 

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 渲染函数来看,你就觉得非常熟悉了。

现在有了 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 之后就直接创建 <style> 标签了
// 这个先不加,style 还没编译
// import '/src/main.style.template.js'

// 给 Vue 对象设置 render 函数
script.render = render

// 设置一些组件的信息,用于开发环境
script.__file = 'example.vue'
script.__scopeId = 'xxxxxx'

// 这里可以加入其它代码,例如热更新

export default script 

但我们其实有更简洁的方式,就是直接将 scripttemplate 这两个模块内联到代码中,这样就只有一个文件了。

于是我们可以这样做:

// 用于存放代码,最后 join('\n') 合并成一份完整代码
const codeList = [];
codeList.push(script.content);
codeList.push(template.code);

const code = codeList.join('\n') 

但这样做,其实是不行的,因为你会得到以下内容:

import { ref } from "vue";

export default {setup() { // setup 实现},
};

// ----- 上面是 script 的内容,下面是 template 的内容

import { xxx } from "vue"

export function render(_ctx, _cache) {// render 函数实现
} 

因为用的是 export default,组件没有存储到变量中,我们没法给 Vue 组件设置 render 函数

因此,@vue/compiler-sfc 贴心地给我们提供了一个工具函数 rewriteDefault,它的作用如图:

export default 改成 const 定义的变量

那我们现在就可以合成代码了:

import { compileScript, compileTemplate, rewriteDefault } 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 });

// 用于存放代码,最后 join('\n') 合并成一份完整代码
const codeList = [];

// 重写 default
codeList.push(rewriteDefault(script.content, "__sfc_main__"));
codeList.push(`__sfc_main__.__scopeId='${scopeId}'`);

// 编译模板,转换成 render 函数
const template = compileTemplate({source: descriptor.template!.content,filename: "main.vue", // 用于错误提示id: scopeId,
});

codeList.push(template.code);
codeList.push(`__sfc_main__.render=render`);
codeList.push(`export default __sfc_main__`);

// 将合成的代码写到本地
await writeFile("build.temp.js", code); 

得到的代码如下:

import { ref } from "vue";

// vue 组件
const __sfc_main__ = {name: "Main",setup() {const message = ref("Main");return {message,};},
};

__sfc_main__.__scopeId='data-v-1656415804393'
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "message" }

// render 函数
export function render(_ctx, _cache) {return (_openBlock(), _createElementBlock("div", _hoisted_1, _toDisplayString(_ctx.message), 1 /* TEXT */))
}
// 设置 render 函数到组件
__sfc_main__.render=render
export default __sfc_main__ 

虽然代码有点丑,但还是能看出来,它的是个 Vue 组件。

那么这个代码是不是能直接给浏览器用呢?

答案还是不能,因为浏览器无法导入裸模块,即 import "vue",浏览器是无法识别的,不知道从哪里获取 Vue 模块。

我们可以手动将 import { ref } from "vue"; 改成 Vue.ref,但是我们有更自动的方法 —— 用打包工具打包一遍

打包代码

直接使用 esbuild 进行打包。

import { build } from "esbuild";
import { externalGlobalPlugin } from "esbuild-plugin-external-global";

await build({entryPoints: ["build.temp.js"],	// 入口文件format: "esm",			// 打包成 esmoutfile: "bundle.js",		// 设置打包文件的名字bundle: true,				// bundle 为 true 才是打包模式external: ["vue"],plugins: [externalGlobalPlugin({vue: "window.Vue",	// 将 import vue 模块,替换成 window.Vue}),],
}); 

1.将 vue 模块 external,即不参与打包(因为我们在 index.html 已经全局引入了 Vue,如果不全局引入 Vue,则需要将 vue 也打包到代码中)2.使用 externalGlobalPlugin 插件,让 externalVue 模块从 window.Vue 中获取。打包完成的代码,就可以直接给浏览器使用了

<!DOCTYPE html>
<html lang="en">
<head><script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app"></div>
</body>
<script type="module"> // 引入刚刚打包好的代码import Comp from './bundle.js'Vue.createApp(Comp).mount('#app') </script>
</html> 

现在组件已经渲染到界面中了:

编译 style

编译 style,编译产物还是 style不是 js,目的是编译 vue 的一些特殊的能力,例如 style scopev-bind():deep()

import { compileStyle } from "@vue/compiler-sfc";

// 一个 Vue 文件,可能有多个 style 标签
for (const styleBlock of descriptor.styles) {const styleCode = compileStyle({source: styleBlock.content,id,		// style 的 scope id,filename: "main.vue",scoped: styleBlock.scoped,});
} 

编译后的对象如下:

编译后的 style 代码:

.message[data-v-1656417674368] {font-size: 60px;font-weight: 900;
} 

这里加上了传入的 scopeId

为什么编译产物不是 js?

因为 style 使用的不一定是 css,还可能是 lesssass 等语法,还需要交给其他预处理器以及后处理器,进行处理

css 最后如何转成 js?

直接用 createElement 创建 style 标签,然后拼接到页面 body 即可

const styleDOM = `var el = document.createElement('style')el.innerHTML =\`${styleCode.code}\`document.body.append(el);
`; 

css 其实都是全局的,在这段样式代码被加载时,style 标签就已经被创建,然后插入到页面了。因此 css 需要使用 scope 的方式用做样式的隔离,需要提供 scopeIdcompileStyle 函数,用来生成 [data-v-1656417674368] 这种选择器,以免影响到全局样式。

style 完整的代码如下(放在 esbuild 编译前):

import { compileStyle } from "@vue/compiler-sfc";

for (const styleBlock of descriptor.styles) {const styleCode = compileStyle({source: styleBlock.content,id,filename: "main.vue",scoped: styleBlock.scoped,});const styleDOM = `var el = document.createElement('style')el.innerHTML =\`${styleCode.code}\`document.body.append(el);`;codeList.push(styleDOM);
} 

编译后的代码,加入到 codeList 中,最终生成一份完整的代码,然后将这份代码进行打包即可。

最终的渲染结果:

总结

我们从一个非常简单的 Vue 文件,使用 @vue/compiler-sfc,一步一步地将 Vue 文件进行编译处理,分别编译 scripttemplatestyle,并将这三部分组装到一起,最后将其进行打包,打包后的文件就能够在浏览器中正确运行,并渲染出界面。

其实@vite/plugin-vue 的处理过程,与我们手动处理的过程,大致相同,不过还加上了热更新、编译缓存、拆分成虚拟模块等能力。

明白这个过程之后,回头看,其实 Vue 的处理,真的没有多复杂,很多时候,一个技术看起来很复杂,但当你潜下心往里面钻研时,其实想要理解它,也并非那么困难

### 实现 Vue3 中读取 Excel 文件将内容显示在页面上的方法 为了实现在 Vue3 项目中读取 Excel 文件将其内容展示在网上,可以采用前端解析的方式。这种方式不需要依赖后端服务来处理文件上传和解析。 #### 安装必要的库 首先,在项目中安装 `xlsx` 库用于解析 Excel 文件: ```bash npm install xlsx ``` #### 创建组件结构 创建一个新的 Vue 组件用来管理文件输入框以及数据显示区域。 #### 编写 HTML 和 JavaScript 部分 下面是一个完整的示例代码片段展示了如何实现这一功能: ```html <template> <div id="app"> <!-- 提供一个按钮让用户选择要导入的Excel文件 --> <input type="file" accept=".xls,.xlsx" @change="handleFileUpload"/> <!-- 显示已加载的工作表名称列表 --> <ul v-if="sheets.length > 0"> <li v-for="(sheet, index) in sheets" :key="index">{{ sheet.sheetName }}</li> </ul> <!-- 动态切换当前查看的工作表数据 --> <table border="1px solid black" cellpadding="5" cellspacing="0" width="80%" v-if="currentSheetData && currentSheetData.length > 0"> <thead> <tr> <th v-for="(item, key) in currentSheetData[0]" :key="key">{{ key }}</th> </tr> </thead> <tbody> <tr v-for="(row, rowIndex) in currentSheetData" :key="rowIndex"> <td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td> </tr> </tbody> </table> </div> </template> <script setup> import { ref } from 'vue'; import * as XLSX from 'xlsx'; // 存储所有工作表的信息 const sheets = ref([]); // 当前选中的工作表索引,默认为第一个 let currentIndex = 0; // 当前所选工作表的具体数据 const currentSheetData = ref(null); function handleFileUpload(event){ const files = event.target.files; if (files.length === 0 || !/\.(xls|xlsx)$/.test(files[0].name)) { alert('请选择有效的Excel文件'); return false; } // 使用 FileReader API 来异步读取文件的内容 let reader = new FileReader(); reader.onload = function(e){ try{ /* 将二进制字符串转换成ArrayBuffer */ var data = new Uint8Array(e.target.result); // 利用 XLSX.parse 方法把 ArrayBuffer 转换成 workbook 对象 var workbook = XLSX.read(data, {type:'array'}); // 获取所有的 Sheet 名字数组 var allSheetsNames = workbook.SheetNames; // 初始化存储每张表的数据对象 sheets.value = []; // 循环遍历每一个 Sheet Name ,依次获取对应的 JSON 数据 for(let i=0;i<allSheetsNames.length;i++){ let tempJson = XLSX.utils.sheet_to_json(workbook.Sheets[allSheetsNames[i]]); // 如果该 Sheet 下有数据,则保存起来 if(tempJson.length>0){ sheets.value.push({ "sheet":tempJson, "sheetName":allSheetsNames[i] }); // 默认设置第一个 Sheet 的数据为可见状态 if(i===0){ currentSheetData.value=tempJson; } } } }catch(ex){ console.error("Error processing file:", ex); } }; // 开始读取文件内容 reader.readAsArrayBuffer(files.item(0)); } </script> <style scoped> /* 添加一些简单的样式使表格更美观 */ table th, table td{text-align:center;} </style> ``` 此段代码实现了用户可以选择本地的一个 `.xls` 或者 `.xlsx` 类型的 Excel 文件通过浏览器内置的 `FileReader` 接口读取文件内容;接着利用 `xlsx` 这个第三方类库完成对 Excel 文档的实际解析操作,最后将得到的结果以表格的形式呈现在界面上[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值