vite实现原理-解析SFC

写在前面

源码地址

vue SFC是以.vue结尾的单文件组件,为vue开发者提供了便捷,然而浏览器并不认识该类型的文件,所以需要在编译时将SFC编译成浏览器认识的js供浏览器识别,vite需要编译vue文件需要借助两个官方提供的包 @vue/compiler-dom@vue/compiler-sfc

compiler-sfc

  1. sfc文件中的template模块转换成html代码
  2. sfc文件中的script内容转换成javascript代码
  3. 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标签
    • 设置标签的typetext/css类型
    • 设置标签内容为刚刚从描述符获取到的__style变量
    • style标签动态插入到document.head

看下效果

结束语

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

相关推荐
passerby606137 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc