什么是 Vite

官网上已经说明啦:下一代前端开发与构建工具,提高开发者的开发体验在这里插入图片描述


分析

首先看一下空项目里都有些什么吧,项目的根目录下会有个 index.html ,叫宿主页,页面内会有一个 script 标签
在这里插入图片描述
这里使用浏览器新支持的 type="module" ,这样写有一个好处,就是在 main.js 中可以以 ESModule 的方式编写,且不需要打包工具进行打包,浏览器会发送相应请求,去寻找 vue 以及 ./App.vue,如下如所示

在这里插入图片描述

等待加载完这两个资源之后,在进行 createApp() 创建应用程序,在页面上显示

这样的处理方法有以下好处

  • 在开发阶段不需要打包,这样不管我的项目有多大,加载的速度快
  • 按需加载,用到相关的模块就去加载相关的模块,不相关的不加载,提高速度

需要知道的知识点

  • 浏览器只知道相对地址,它根据当前的 index.html 的地址,去发送请求,获取资源
  • 浏览器只认识 htmljscss,如果给我一个vue 文件,浏览器是不知道怎么处理,所以 vite 需要额外去处理 vue 文件,(也就是把 vue 转化成 js)其他的文件格式同理,把特殊的文件类型进行解析和转换。

思路

vite 相当于一个服务器,处理一下宿主文件,以及 vue 文件等


目标

自己实现一个基础的 vite ,包括一下几个功能

  • 返回宿主页面
  • 处理裸模块
  • 处理单文件组件(SFC)

开发

先写一个 Node 服务

在这里插入图片描述


返回宿主页面

我们需要有个宿主页面,来当我们的单页面的宿主

之后,当我们请求 / 时,就需要返回宿主页,那我们可以用 fs 模块
在这里插入图片描述
但是这样也不对,因为请求的 main 也是 html 了,那是因为我们没写路由

在这里插入图片描述
写完路由之后如下,main.js 会找不到,之后再来处理 js 文件


处理 js 文件

大概的处理方式就是找到文件之后,返回即可

在这里插入图片描述

演示如下
在这里插入图片描述

这里需要说明一下,ctx.type 默认是 text/plain 也就是纯文本,我们需要设置成 application/javascript ,告诉浏览器这是个 js 代码
在这里插入图片描述

path.join 的使用与否,不影响结果,不过输出的处理后的内容的话,还是能看出区别的,使用 path.join 拼接的路径会更规范一些

在这里插入图片描述


裸模块的路径重写

浏览器是不认识绝对路径的,只知道相对路径
在这里插入图片描述

比方说入口文件中的 'vue‘ ,它就是一个裸模块,浏览器发送请求,是找不到这个文件的,需要 vite 来做一下处理,帮我们找到这个模块然后返回

其中,我们需要处理一下导入的文字,在前面加上些标志,说明她是裸模块,此时需要一个函数来批量处理这些东西

处理完之后,浏览器会发送相关依赖的请求,从而请求资源

在这里插入图片描述

之后浏览器就可以请求资源了

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
// 重写导入 的函数如下
function rewriteImport(content) {
return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) {
const startsWithResult =
s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../");
if (startsWithResult === true) {
return s1;
} else {
return ` from '/@modules/${s2}'`;
}
});
}

加载裸模块

大概的处理逻辑就是这样的
先到 package.json 中找到真正的入口位置路径,然后 fs 一下,返回

在这里插入图片描述

在这里插入图片描述

process 报错

如果遇到如下报错,那么需要在页面上声明一下变量

模拟一下
在这里插入图片描述
可以稍微查看一下报错的位置
在这里插入图片描述

因为这些代码里有些是 node 的代码,而浏览器环境中是没有 process 变量的,所以我们需要在页面上设置一下

1
2
3
4
5
6
7
8
9
// 在页面上
<script>
// 模拟一下环境,否则不会渲染
window.process = {
env: {
NODE_ENV: "dev",
},
};
</script>
1
2
3
4
5
6
// main.js
import { createApp, h } from "vue";
// import App from "./App.vue";

createApp(h("div", "我是内容")).mount("#app");


解析 sfc

就是解析单文件的 app.vue

主要是处理 app.vue 这个请求,然后把他变成 js 代码

思路还是一样的,只不过这里引入个模块 compiler-sfc,此模块是专门分析 vue 文件,把他转化成 js 对象

1
const compilerSFC = require("@vue/compiler-sfc");

打印一下分析后的内容,如下图所示
在这里插入图片描述

我们需要先拿到组件中的 script 的内容,然后返回,其中 template 转换成 渲染函数 需要另外 一个请求去处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p = path.join(__dirname, url.split("?")[0]); // 找到文件位置
const content = fs.readFileSync(p, "utf-8"); // 以纯文本读取
const compileContent = compilerSFC.parse(content); // vue 词法分析
const { type } = query;
if (!type) {
// 获取组件中的脚本
const { content: scriptContent } = compileContent.descriptor.script;
const script = scriptContent.replace(
"export default ",
"const __script = "
); // 将组件中的脚本变成变量
ctx.type = "application/javascript";
ctx.body = `
${rewriteImport(script)}
import { render as __render } from '${url}?type=template' // 请求 html 获取,render 函数
__script.render = __render // 将 render 函数 挂载到 脚本变量 中
export default __script // 导出脚本变量
`;

模板编译

模板编译有另外一个模块去处理 @vue/compiler-dom,它可以将 template 转换成渲染函数

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
else if (type === "template") {
const { content: templateContent } = compileContent.descriptor.template;
const render = compilerDOM.compile(templateContent, {
mode: "module",
}).code;
console.log("render:>>", render);
ctx.type = "application/javascript";
ctx.body = rewriteImport(render);
}
}

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const Koa = require("koa");
const fs = require("fs");
const path = require("path");
const compilerSFC = require("@vue/compiler-sfc");
const compilerDOM = require("@vue/compiler-dom");

const app = new Koa();

app.use(async (ctx) => {
const { url, query } = ctx;
if (url === "/") {
ctx.type = "text/html";
ctx.body = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8");
} else if (url.endsWith(".js")) {
const p = path.join(__dirname, url); // 首先找到文件的位置
ctx.type = "application/javascript"; // 这里如果不设置的话,默认是 text/plain
ctx.body = rewriteImport(fs.readFileSync(p, "utf-8"));
} else if (url.startsWith("/@modules/")) {
// console.log("裸模块的加载");
const moduleName = url.replace("/@modules/", "");
const p = path.join(__dirname, "node_modules", moduleName);
const modulePath = require(path.join(p, "/package.json")).module; // 这里的 json 是 require 引入
const realPath = path.join(p, modulePath);
ctx.type = "application/javascript";
ctx.body = rewriteImport(fs.readFileSync(realPath, "utf-8"));
} else if (url.indexOf(".vue") > -1) {
const p = path.join(__dirname, url.split("?")[0]); // 找到文件位置
const content = fs.readFileSync(p, "utf-8"); // 以纯文本读取
const compileContent = compilerSFC.parse(content); // vue 词法分析
const { type } = query;
if (!type) {
// 获取组件中的脚本
const { content: scriptContent } = compileContent.descriptor.script;
const script = scriptContent.replace(
"export default ",
"const __script = "
); // 将组件中的脚本变成变量
ctx.type = "application/javascript";
ctx.body = `
${rewriteImport(script)}
import { render as __render } from '${url}?type=template' // 请求 html 获取,render 函数
__script.render = __render // 将 render 函数 挂载到 脚本变量 中
export default __script // 导出脚本变量
`;
} else if (type === "template") {
const { content: templateContent } = compileContent.descriptor.template;
const render = compilerDOM.compile(templateContent, {
mode: "module",
}).code;
console.log("render:>>", render);
ctx.type = "application/javascript";
ctx.body = rewriteImport(render);
}
}
});

function rewriteImport(content) {
return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) {
const startsWithResult =
s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../");
if (startsWithResult === true) {
return s1;
} else {
return ` from "/@modules/${s2}"`;
}
});
}

app.listen(3000);


gitee 地址

仓库地址