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预编译。

相关推荐
DT——4 小时前
Vite项目中eslint的简单配置
前端·javascript·代码规范
学习ing小白6 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进7 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er7 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063717 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl7 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码7 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347548 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t8 小时前
新峰商城之分类三级联动实现
前端·html
辛-夷8 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js