文章目录
什么是 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
,包括一下几个功能
- 返回宿主页面
- 处理裸模块
- 处理单文件组件(SFC)
开发
先写一个 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>
// 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
对象
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); // 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
转换成渲染函数
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"; // 这里如果不设置的话,默认是 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);