Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
本文源代码参考极客时间的《玩转 Vue3 全家桶》
1. 前置知识
1.1 koa
koa 是基于 node.js 的 web 开发框架。这里通过使用该框架,实现了 Vue 项目的开发访问。它拦截浏览器的请求,然后返回对应的内容。
2. 源码分析
首先,通过 koa 启动了一个服务器,通过该服务器建立浏览器请求和本地文件的关联。浏览器的请求分成了5种情况,rewriteImport 函数用于实现将引用的 npm 包变成相对路径。
2.1 rewriteImport
除了 . ../ / 开头的文件名称,其它导入的文件都会转化成 from '/@modules/${s1}'形式。比如将from 'vue' 转化成from '/@modules/vue'
// 导入文件替换,直接导入模块的变成 from /@modules/X
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {
// s0:from 后面所有匹配的内容,s1:捕获括号中匹配到的内容
// . ../ / 开头的,都是本地文件的相对路径
if (s1[0] !== "." && s1[0] !== "/") {
return `from '/@modules/${s1}'`;
} else {
return s0;
}
});
}
2.2 获取首页的内容
一般都是用于处理 index.html 文件,该部分代码会在原代码的基础上增加 process.env,用于标示运行的环境。
if (url == "/") {
ctx.type = "text/html";
let content = fs.readFileSync("./index.html", "utf-8");
content = content.replace(
"<script ",
`
<script>
window.process = {env:{ NODE_ENV:'dev'}}
</script>
<script
`
);
ctx.body = content;
}
2.3 处理 JS 文件
根据浏览器中 url 部分的名称找到对应的本地文件,返回文件内容
if (url.endsWith(".js")) {
const p = path.resolve(__dirname, url.slice(1));
ctx.type = "application/javascript";
const content = fs.readFileSync(p, "utf-8");
ctx.body = rewriteImport(content);
}
2.4 处理 CSS 文件
如果是 CSS 文件,去掉换行,并把 CSS 内容转化成对应的 DOM 操作。
if (url.endsWith(".css")) {
const p = path.resolve(__dirname, url.slice(1));
const file = fs.readFileSync(p, "utf-8");
const content = `
const css = "${file.replace(/\n/g, "")}"
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`;
ctx.type = "application/javascript";
ctx.body = content;
}
2.5 处理导入的第三方模块
通过 rewriteImport 函数处理后,第三方模块变成了/@modules/${s1}形式。这里转化成去 node_modules 文件夹查找对应的文件,先找到对应第三方模块的文件夹,再找到该文件夹中package.json的 module,该 module 对应的文件就是第三方模块打包后的文件内容,可以直接使用
if (url.startsWith("/@modules/")) {
const prefix = path.resolve(__dirname, "node_modules", url.replace("/@modules/", ""));
const module = require(prefix + "/package.json").module;
const p = path.resolve(prefix, module);
const ret = fs.readFileSync(p, "utf-8");
ctx.type = "application/javascript";
ctx.body = rewriteImport(ret);
}
2.6 处理 Vue 文件
该部分代码处理了 Vue 文件的 script 部分和 template 部分,通过 @vue/compiler-sfc 包将 Vue 单文件进行拆分,通过 @vue/compiler-dom 包处理 template 部分的内容,将 template 里面的内容转化成 render
if (url.indexOf(".vue") > -1) {
const p = path.resolve(__dirname, url.split("?")[0].slice(1));
const { descriptor } = compilerSfc.parse(fs.readFileSync(p, "utf-8"));
if (!query.type) {
// 1 处理 script 部分的内容
ctx.type = "application/javascript";
// 借用 vue 自带的 compile 框架 解析单文件组件,其实相当于 vue-loader 做的事情
ctx.body = `
${rewriteImport(descriptor.script.content.replace("export default ", "const __script = "))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`;
} else if (query.type === "template") {
// 2 编译 template 部分的内容
const template = descriptor.template;
// 服务端将 template 转化成 render
const render = compilerDom.compile(template.content, { mode: "module" }).code;
ctx.type = "application/javascript";
ctx.body = rewriteImport(render);
}
}
3. 源码
const fs = require("fs");
const path = require("path");
const Koa = require("koa");
const compilerSfc = require("@vue/compiler-sfc");
const compilerDom = require("@vue/compiler-dom");
const app = new Koa();
// 导入文件替换,直接导入模块的变成 from /@modules/X
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {
// s0:from 后面所有匹配的内容,s1:捕获括号中匹配到的内容
// . ../ / 开头的,都是本地文件的相对路径
if (s1[0] !== "." && s1[0] !== "/") {
return `from '/@modules/${s1}'`;
} else {
return s0;
}
});
}
app.use(async (ctx) => {
const {
request: { url, query },
} = ctx;
if (url == "/") {
// 1. 首页获取内容
ctx.type = "text/html";
let content = fs.readFileSync("./index.html", "utf-8");
content = content.replace(
"<script ",
`
<script>
window.process = {env:{ NODE_ENV:'dev'}}
</script>
<script
`
);
ctx.body = content;
} else if (url.endsWith(".js")) {
// 2. 处理 JS 文件
const p = path.resolve(__dirname, url.slice(1));
ctx.type = "application/javascript";
const content = fs.readFileSync(p, "utf-8");
ctx.body = rewriteImport(content);
} else if (url.endsWith(".css")) {
// 3. 处理 CSS 文件。将 css 文件内容变成 js 形式
const p = path.resolve(__dirname, url.slice(1));
const file = fs.readFileSync(p, "utf-8");
const content = `
const css = "${file.replace(/\n/g, "")}"
let link = document.createElement('style')
link.setAttribute('type', 'text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`;
ctx.type = "application/javascript";
ctx.body = content;
} else if (url.startsWith("/@modules/")) {
// 4. 处理 node_modules。
const prefix = path.resolve(__dirname, "node_modules", url.replace("/@modules/", ""));
const module = require(prefix + "/package.json").module;
const p = path.resolve(prefix, module);
const ret = fs.readFileSync(p, "utf-8");
ctx.type = "application/javascript";
ctx.body = rewriteImport(ret);
} else if (url.indexOf(".vue") > -1) {
// 5. 处理 vue 单文件
const p = path.resolve(__dirname, url.split("?")[0].slice(1));
const { descriptor } = compilerSfc.parse(fs.readFileSync(p, "utf-8"));
if (!query.type) {
// 5.1 处理 script 部分的内容
ctx.type = "application/javascript";
// 借用 vue 自带的 compile 框架 解析单文件组件,其实相当于 vue-loader 做的事情
ctx.body = `
${rewriteImport(descriptor.script.content.replace("export default ", "const __script = "))}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`;
} else if (query.type === "template") {
// 5.2 编译 template 部分的内容
const template = descriptor.template;
// 服务端将 template 转化成 render
const render = compilerDom.compile(template.content, { mode: "module" }).code;
ctx.type = "application/javascript";
ctx.body = rewriteImport(render);
}
}
});
app.listen(8008, () => {
console.log("服务已启动,端口是 8008");
});
4. 总结
- 总的来说,迷你 vite 的实现思路很清晰,它通过启动一个 web 服务器,拦截浏览器的请求,实现首页文件的按需加载,从而使项目启动时间和项目复杂解耦,提高启动效率。
- 本文耗时4个小时