基本介绍
- 目的在于体会原生
ES
模块带来的快乐,浏览器递归请求各种包的流程 - 基于
Koa
构建解析一个基础Vue
项目,来体会Vite
对于html
、css
、js
的处理方式 - 这里建议想体验的小伙伴,直接安装、运行项目,根据下面说明自己打断点体会(代码注释非常全面)
- 源代码地址,边思考,边打断点,方便理解
Vite 实现原理
-
第一种
script
标签发送请求:从入口文件index.html
看到<script type="module" src="/src/main.js"></script>
- 这里定义
script
标签为module
类型 - 浏览器会向服务器发送一个
GET
http://localhost:3000/src/main.js
请求main.js
文件
- 这里定义
-
第二种基于 JavaScript modules 模块化发送请求:
- 请求
vue
文件:GET
http://localhost:3000/@modules/vue
- 请求
App.vue
文件:GET
http://localhost:3000/src/App.vue
- 请求
-
Vite
的本质就是通过本地服务端对请求进行劫持加工处理,然后返还给前端浏览器 -
对于浏览器无法处理的文件格式,全部处理成浏览器可执行的
javascript
内容,返回给浏览器 -
这种方式很像以前使用
asp
php
开发网站的方式,服务端把页面处理完成,一并返回给前端进行展示,只不过现在更加快捷和集成
入口文件 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
- 多出来的
script
模块是为了解决浏览器无法识别process
和css
处理函数 - 在
koa
服务器最开始读取index.html
js
if (url === "/") {
// 加载首页 index.html 文件
ctx.type = "text/html"; // 设置内容类型为 html
const file = fs.readFileSync("./index.html", "utf-8"); // 读取 html 文件,并设置 unicode 编码
ctx.body = file; // 设置 http 协议响应体
}
路由处理
- 这里对
js
文件、裸模块(非相对路径的文件)、vue
单文件组件,不同路由采用不同的解析方法,返回不同的内容 vue
文件,对于javascript
部分、template
部分、style
部分进行针对性处理和解析
js 文件处理
js
else if (url.includes(".js")) {
// 处理 js 文件
const pathJs = path.join(__dirname, "..", url); // 获取 js 文件的绝对路径
ctx.type = "application/javascript"; // 设置内容类型为 javascript
const file = fs.readFileSync(pathJs, "utf-8"); // 读取 javascript 文件,并设置 unicode 编码
ctx.body = rewriteImport(file); // 设置 http 协议响应体
}
- 这里要处理的问题是如果获取到
.js
文件的绝对路径
裸模块,非相对路径文件处理
js
else if (url.startsWith("/@modules/")) {
// 处理裸模块,即非相对路径的文件
const moduleName = url.replace("/@modules/", ""); // 裸模块名称
// 拼接 node_modules 目录中前缀
const prefix = path.join(__dirname, "../node_modules", moduleName);
// 获取 package.json 中获取 module 字段
const module = require(prefix + "/package.json").module;
// 拼接最终文件地址
const filePath = path.join(prefix, module);
ctx.type = "application/javascript"; // 设置内容类型为 javascript
const file = fs.readFileSync(filePath, "utf-8"); // 读取 javascript 文件,并设置 unicode 编码
const file2 = `// ${filePath}\n${file}`; // 可删除,把文件绝对路径放在文件顶部,用户查看对比
ctx.body = rewriteImport(file2); // 设置 http 协议响应体
}
- 问题1:重写模块地址,让浏览器能够正常发送请求,添加自定义标识
/@modules/
方便区分和处理 - 问题2:通过获取
node_modules
已经打包好的裸模块文件,来让浏览器加载可执行的裸模块文件- 正规的
npm
包根目录都有package.json
文件,文件里面有一个module
字段记录当前包的输出文件地址 - 我们只要把所有裸模块地址替换成
module
的相对地址,就可以实现浏览器递归请求各种依赖包
- 正规的
裸模块相对地址处理方法 rewriteImport
js
// 裸模块(非相对地址文件)地址重写
function rewriteImport(content) {
// 地址重写
return content.replace(/ from ['|"](.*)['|"]/g, (s1, s2) => {
const list = ["/", "./", "../"]; // 确定浏览器识别范围
const isList = list.some((item) => s2.startsWith(item)); // 浏览器能识别部分不处理
if (isList) {
// 可识别部分
return s1;
} else {
// 浏览器无法识别部分
return ` from '/@modules/${s2}'`;
}
});
}
- 浏览器只能通过相对路径读取文件,对于裸模块这种路径,浏览器目前无法知道如何请求
- 在我们本地项目中之所以能够请求到,本质就是已经由
webpack
、vite
等打包工具处理过了,是架构给我们提供的语法糖
处理 vue
文件 公共部分 部分
- 公共部分:
js
else if (url.includes(".vue")) {
// 解析 vue 自定义文件
const pathVue = path.join(__dirname, "..", url.split("?")[0]); // 获取 vue 文件的绝对路径
const compileResult = compilerSFC.parse(fs.readFileSync(pathVue, "utf-8")); // 获取编译结果,ast 树
}
- 问题1:类型区分,这里为了模块划分请求,方便管理,不会一次性把
vue
文件中的html
、css
、js
三部分全部处理,而是把三者区分开,把html
和css
分成新的请求,进行单独处理- 这里的
url
会存在多种类型,如:.vue
、.vue?type=template
、.vue?type=style
- 所以文件的绝对路径获取要把多余的参数处理掉,否则会导致错误
- 这里的
- 问题2:如何编译
vue
文件,这里使用官方提供的工具包@vue/compiler-sfc
把内容解析成ast
树,方便我们后续针对不同部分进行处理
处理 vue
文件 javascript
部分
js
// 获取文件 query 参数,针对不同类型的文件独立处理
if (!query.type) {
// 处理 App.vue 文件的 javascript 部分
const scriptContent = compileResult.descriptor.script.content; // 获取编译后的 javascript content
// 把脚本部分放在一个常量里面
const script = scriptContent.replace(
"export default ",
"const __script = "
);
// 处理 style 部分,是数组,允许有多个 style 部分
const styles = compileResult.descriptor.styles;
// 返回 App.vue 解析结果,单独处理 template 部分
ctx.type = "application/javascript";
ctx.body = `
${rewriteImport(script)}
// 存在 style 发送请求 style 部分
${styles.length > 0 ? `import "${url}?type=style"` : ""}
// 发送请求获取 template 部分
import { render as __render } from '${url}?type=template'
__script.render = __render
export default __script
`;
}
- 问题1:通过
ast
树返回结构,获取javascript
部分的内容,形成一个render
函数 - 问题2:
style
部分处理,获取核心样式内容,这里调用index.html
文件里面的方法,处理样式(官方用的是HMR
热更新包里面的处理方法,这里做了简化) - 问题3:处理
template
部分,拼接成另一个请求形式,用户后续单独处理,浏览器会再次发送请求
处理 vue
文件 template
部分
js
else if (query.type === "template") {
// 处理模板内容
const template = compileResult.descriptor.template.content;
// template 部分编译为 render,即浏览器可执行的 js
const render = compilerDom.compile(template, {
mode: "module"
}).code;
ctx.type = "text/javascript";
ctx.body = rewriteImport(render);
}
- 问题1:通过公共部分的
ast
树获取template
部分的内容 - 问题2:使用
@vue/compiler-dom
包进行解析,处理成render
函数形式
处理 vue
文件 style
部分
js
else if (query.type === "style") {
const styles = compileResult.descriptor.styles;
ctx.type = "application/javascript";
ctx.body = `
const css = ${JSON.stringify(styles[0].content)};
updateStyle(css);
export default css;
`;
}
- 问题1:通过公共部分的
ast
树获取style
部分 - 问题2:把获取的内容,通过
updateStyle
方法处理成浏览器能够识别的方式即可
从头创建项目流程
- 初始化项目
shell
npm init -y
-
创建
vue
项目index.html
src/main.js
src/App.vue
-
安装依赖工具包
shell
pnpm i vue
pnpm i koa
pnpm i @vue/compiler-sfc
pnpm i @vue/compiler-dom
- 安装文件处理工具包
shell
pnpm i @vue/runtime-dom
pnpm i @vue/runtime-core
pnpm i @vue/shared
pnpm i @vue/reactivity
对工具变迁的感悟
- 前端历史有
webpack
、Rollup
和Parcel
等多种工具的变迁,这些工具由于历史原因,只能把架构的各种文件,通过不同的loader
plugin
处理成浏览器能够识别的方式,但随着项目的增强,TC39
组织也在考虑如何保证项目的与时俱进,ESM
就是在这种背景下诞生的,也注定要成为新工具的核心功能宣传点 Vite
启动快的核心是把曾经由工具(webpack)处理的工作,转移到了浏览器,再利用浏览器的缓存功能,让二次启动达到神速状态- 在生产环境依然使用打包方式,避免出现生产环境冷启动缓慢问题;目前由于打包工具的局限性,官方也不得不采用两种解决方案,就像官方所说,随着工具的进步,后续会进行统一
- 这里也有个疑问,就是为什么官方不自己动手实现一个高性能的打包工具,这或许就是上下游的问题,开发资源的分配问题了