手写 Vite 近距离体会前端发展

基本介绍

  • 目的在于体会原生 ES 模块带来的快乐,浏览器递归请求各种包的流程
  • 基于 Koa 构建解析一个基础 Vue 项目,来体会 Vite 对于 htmlcssjs 的处理方式
  • 这里建议想体验的小伙伴,直接安装、运行项目,根据下面说明自己打断点体会(代码注释非常全面)
  • 源代码地址,边思考,边打断点,方便理解

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 模块是为了解决浏览器无法识别 processcss 处理函数
  • 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}'`;
    }
  });
}
  • 浏览器只能通过相对路径读取文件,对于裸模块这种路径,浏览器目前无法知道如何请求
  • 在我们本地项目中之所以能够请求到,本质就是已经由 webpackvite等打包工具处理过了,是架构给我们提供的语法糖

处理 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 文件中的 htmlcssjs三部分全部处理,而是把三者区分开,把 htmlcss 分成新的请求,进行单独处理
    • 这里的 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

对工具变迁的感悟

  • 前端历史有 webpackRollupParcel 等多种工具的变迁,这些工具由于历史原因,只能把架构的各种文件,通过不同的 loader plugin 处理成浏览器能够识别的方式,但随着项目的增强,TC39 组织也在考虑如何保证项目的与时俱进,ESM 就是在这种背景下诞生的,也注定要成为新工具的核心功能宣传点
  • Vite 启动快的核心是把曾经由工具(webpack)处理的工作,转移到了浏览器,再利用浏览器的缓存功能,让二次启动达到神速状态
  • 在生产环境依然使用打包方式,避免出现生产环境冷启动缓慢问题;目前由于打包工具的局限性,官方也不得不采用两种解决方案,就像官方所说,随着工具的进步,后续会进行统一
  • 这里也有个疑问,就是为什么官方不自己动手实现一个高性能的打包工具,这或许就是上下游的问题,开发资源的分配问题了
相关推荐
Qrun14 分钟前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp14 分钟前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.1 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl3 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front8 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰8 小时前
纯flex布局来写瀑布流
前端·javascript·css