什么是 Vite
官网上已经说明啦:下一代前端开发与构建工具,提高开发者的开发体验
分析
首先看一下空项目里都有些什么吧,项目的根目录下会有个 index.html ,叫宿主页,页面内会有一个 script 标签 这里使用浏览器新支持的 type="module" ,这样写有一个好处,就是在 main.js 中可以以 ESModule 的方式编写,且不需要打包工具进行打包,浏览器会发送相应请求,去寻找 vue 以及 ./App.vue ,如下如所示
等待加载完这两个资源之后,在进行 createApp() 创建应用程序,在页面上显示
这样的处理方法有以下好处
- 在开发阶段不需要打包,这样不管我的项目有多大,加载的速度快
- 按需加载,用到相关的模块就去加载相关的模块,不相关的不加载,提高速度
需要知道的知识点
- 浏览器只知道相对地址,它根据当前的
index.html 的地址,去发送请求,获取资源 - 浏览器只认识
html 、js 、 css ,如果给我一个vue 文件,浏览器是不知道怎么处理,所以 vite 需要额外去处理 vue 文件,(也就是把 vue 转化成 js )其他的文件格式同理,把特殊的文件类型进行解析和转换。
思路
vite 相当于一个服务器,处理一下宿主文件,以及 vue 文件等
目标
自己实现一个基础的 vite ,包括一下几个功能
开发
先写一个 Node 服务
返回宿主页面
我们需要有个宿主页面,来当我们的单页面的宿主
之后,当我们请求 / 时,就需要返回宿主页,那我们可以用 fs 模块 但是这样也不对,因为请求的 main 也是 html 了,那是因为我们没写路由
写完路由之后如下,main.js 会找不到,之后再来处理 js 文件
处理 js 文件
大概的处理方式就是找到文件之后,返回即可
演示如下
这里需要说明一下,ctx.type 默认是 text/plain 也就是纯文本,我们需要设置成 application/javascript ,告诉浏览器这是个 js 代码
path.join 的使用与否,不影响结果,不过输出的处理后的内容的话,还是能看出区别的,使用 path.join 拼接的路径会更规范一些
裸模块的路径重写
浏览器是不认识绝对路径的,只知道相对路径
比方说入口文件中的 'vue‘ ,它就是一个裸模块,浏览器发送请求,是找不到这个文件的,需要 vite 来做一下处理,帮我们找到这个模块然后返回
其中,我们需要处理一下导入的文字,在前面加上些标志,说明她是裸模块,此时需要一个函数来批量处理这些东西
处理完之后,浏览器会发送相关依赖的请求,从而请求资源
之后浏览器就可以请求资源了
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 变量的,所以我们需要在页面上设置一下
<script>
window.process = {
env: {
NODE_ENV: "dev",
},
};
</script>
import { createApp, h } from "vue";
createApp(h("div", "我是内容")).mount("#app");
解析 sfc
就是解析单文件的 app.vue
主要是处理 app.vue 这个请求,然后把他变成 js 代码
思路还是一样的,只不过这里引入个模块 compiler-sfc ,此模块是专门分析 vue 文件,把他转化成 js 对象
const compilerSFC = require("@vue/compiler-sfc");
打印一下分析后的内容,如下图所示
我们需要先拿到组件中的 script 的内容,然后返回,其中 template 转换成 渲染函数 需要另外 一个请求去处理
const p = path.join(__dirname, url.split("?")[0]);
const content = fs.readFileSync(p, "utf-8");
const compileContent = compilerSFC.parse(content);
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 转换成渲染函数
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);
}
}
整体代码
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";
ctx.body = rewriteImport(fs.readFileSync(p, "utf-8"));
} else if (url.startsWith("/@modules/")) {
const moduleName = url.replace("/@modules/", "");
const p = path.join(__dirname, "node_modules", moduleName);
const modulePath = require(path.join(p, "/package.json")).module;
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);
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 地址
仓库地址
|