写在前面
vue SFC是以.vue结尾的单文件组件,为vue开发者提供了便捷,然而浏览器并不认识该类型的文件,所以需要在编译时将SFC编译成浏览器认识的js供浏览器识别,vite需要编译vue文件需要借助两个官方提供的包 @vue/compiler-dom和@vue/compiler-sfc,
compiler-sfc
- 将
sfc文件中的template模块转换成html代码- 将
sfc文件中的script内容转换成javascript代码- 将
sfc文件中的style内容转换成css代码
compiler-dom将带有vue内部指令{{}}、v-等代码转换成render函数, render函数就是封装好一系列包括但不限于创建节点、删除节点,更新节点等一系列dom操作并实现数据与实图响应式的函数。
快速开始
解析sfc
运行命令npm install @vue/compiler-dom @vue/compiler-sfc -S,安装这两个包。 创建一个App.vue文件
vue
// App.vue
<template>
<div class="abc">{{ title }}</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const title = ref('hello world');
return {
title
}
}
}
</script>
<style>
.abc {
background-color: red;
color: #ffffff;
}
</style>
实现一个简单的sfc文件,然后再修改main.js引入App.vue进行挂载。
js
// main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
目前代码肯定不支持.vue文件的,运行后浏览器会抛出错误GET http://localhost:3000/App.vue net::ERR_ABORTED 404 (Not Found),根据前面的经验,我们需要编写一个新的路由用于解析.vue文件的请求
js
// index.js
const complierSFC = require('@vue/compiler-sfc');
... 省略代码
else if (url.indexOf('.vue') > -1) {
const source = path.join(__dirname, '/src', url);
const code = complierSFC.parse(fs.readFileSync(source, 'utf8'));
console.log(code);
}
...省略代码
引入@vue/compiler-sfc,新增一个路由匹配'.vue'文件,用path模块拼接文件路径,用fs模块读取该文件,都是换汤不换药的操作。接着根据compiler-sfc官方文档,使用parseAPI将源代码转换为描述符,我们打印一下这个所谓的描述符一探究竟。
描述符中,template很显然就是刚刚App.vue中写的template模版代码,因此提取这一段代码进行处理。可是问题又来了,如果直接返回这一段代码,肯定是无法做到数据渲染和响应的,只会单纯的渲染这一段html字符串代码,需要通过调用compiler-dom解析为render函数返回才能正常识别。在vite源码中为了更好的进行功能区分,会重新发起一个新的请求来进行解析template部分。
js
// index.js
const compilerDOM = require('@vue/compiler-dom');
...代码省略
else if (url.indexOf('.vue') > -1) {
// 因为url可能是App.vue?template这种形式,所以要split一下取第一项
const source = path.join(__dirname, '/src', url.split('?')[0]);
const code = complierSFC.parse(fs.readFileSync(source, 'utf8'));
if (!query.type) {
console.log(code)
ctx.type = jsCtxType;
ctx.body = `
// 解析template,重新生成一个引用请求,去解析template内容
import { render as __render } from '${url}?type=template';
`;
} else if (query.type === 'template'){
// 有type查询参解析template部分内容
const tpl = code.descriptor.template.content;
const render = compilerDOM.compile(tpl, { mode: 'module' }).code;
ctx.type = jsCtxType;
ctx.body = transformModuleImport(render);
}
}
...代码省略
- 第11行代码,重新通过
import的方式引入自己,并加一个query入参为template,浏览器会重新发起一个请求。 - 第4行代码,再次发起请求时,url可能为
/App.vue?type=template,因此需要split切割一下保证路径无误。 - 第13行代码,二次发起请求时,判断
query是否有type入参且等于template,获取到描述符中template内容,引入compilerDOM包调用compilerAPI,将模版字符串传入,看看输出结果。
返回了一个render函数,我们需要把这个render函数返回给上一次请求使用,即代码第13行,再次调用transformModuleImport函数转换render函数里的import,然后将其返回。
js
// index.js
...代码省略
else if (url.indexOf('.vue') > -1) {
// 因为url可能是App.vue?template这种形式,所以要split一下取第一项
const source = path.join(__dirname, '/src', url.split('?')[0]);
const code = complierSFC.parse(fs.readFileSync(source, 'utf8'));
if (!query.type) {
// 处理sfc文件
// 读取vue文件,获取脚本部分内容
// 硬编码
// 将script内容替换掉export default作为变量存起来
const scriptContent = code.descriptor.script.content.replace(/export default/g, 'const __script = ');
ctx.type = jsCtxType;
ctx.body = `
${transformModuleImport(scriptContent)}
// 解析template,重新生成一个引用请求,去解析template内容
import { render as __render } from '${url}?type=template';
__script.render = __render;
export default __script;
`;
} else if (query.type === 'template'){
// 有type查询参解析template部分内容
const tpl = code.descriptor.template.content;
const render = compilerDOM.compile(tpl, { mode: 'module' }).code;
ctx.type = jsCtxType;
ctx.body = transformModuleImport(render);
}
}
...代码省略
- 代码第12行,获取
描述符中的script内容,将这段字符串中的export default部分替换为const __script = - 代码第14行,设置第一次请求
.vue文件的返回内容 - 代码第18行,设置
__script变量的render函数为第二次请求template返回的render函数,抛出该__script。
看下效果,App.vue已经成功挂载到了页面上。

最后我们需要对css进行处理,参考一下vite的处理方案。
和template一样,重新发起了一个type=css的请求单独处理css,再看看type=css的请求内容是什么?
读取css内容,从/@vite/client中引入了updateStyle,并将css内容传进去,我们找下源码看看updateStyle都做了些什么。
关键代码是圈出来的地方,创建一个style标签,将style标签插入到document.head中,如此简单,照葫芦画瓢。
js
// index.js
else if (url.indexOf('.vue') > -1) {
const source = path.join(__dirname, '/src', url.split('?')[0]);
const code = complierSFC.parse(fs.readFileSync(source, 'utf8'));
if (!query.type) {
// 处理sfc文件
// 读取vue文件,获取脚本部分内容
// 硬编码
// 将script内容替换掉export default作为变量存起来
const scriptContent = code.descriptor.script.content.replace(/export default/g, 'const __script = ');
console.log(code)
ctx.type = jsCtxType;
ctx.body = `
${transformModuleImport(scriptContent)}
// 解析template,重新生成一个引用请求,去解析template内容
import { render as __render } from '${url}?type=template';
import '${url}?type=style';
__script.render = __render;
export default __script;
`;
} else if (query.type === 'template'){
// 有type查询参解析template部分内容
const tpl = code.descriptor.template.content;
const render = compilerDOM.compile(tpl, { mode: 'module' }).code;
ctx.type = jsCtxType;
ctx.body = transformModuleImport(render);
} else if (query.type === 'style') {
// 有type查询参解析style部分内容
const styles = code.descriptor.styles;
let __style = '';
for (let i = 0; i < styles.length; i++) {
__style += styles[i].content.replace(/(\r\n|\n|\r)/g, ''); ;
}
ctx.type = jsCtxType;
ctx.body = `
const style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = '${__style}';
document.head.appendChild(style);
`;
}
- 代码第17行,重新发起一个带有
type=style的请求 - 代码第27行,处理
type=style的路由。 - 代码第29行,从
描述符中获取到style内容是一个数组,循环拼接所有的css字符串,用变量存起来,需要注意的是,由于描述符中的字符串是带换行符的,所以需要将所有换行符替换掉,否则会报错。 - 代码第35行,照葫芦画瓢
- 创建一个
style标签 - 设置标签的
type为text/css类型 - 设置标签内容为刚刚从描述符获取到的
__style变量 - 将
style标签动态插入到document.head
- 创建一个
看下效果

结束语
到此已经完全实现了一个mini vite,具备了宿主index.html返回,返回js代码和裸模块的引入,解析vue单文件组件SFC的解析,后续可以继续完善支持typeScript编译,jsx的支持,sass和less的css预编译。