写在前面
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
官方文档,使用parse
API将源代码转换为描述符
,我们打印一下这个所谓的描述符
一探究竟。
描述符
中,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
包调用compiler
API,将模版字符串传入,看看输出结果。
返回了一个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预编译。