手写 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)处理的工作,转移到了浏览器,再利用浏览器的缓存功能,让二次启动达到神速状态
  • 在生产环境依然使用打包方式,避免出现生产环境冷启动缓慢问题;目前由于打包工具的局限性,官方也不得不采用两种解决方案,就像官方所说,随着工具的进步,后续会进行统一
  • 这里也有个疑问,就是为什么官方不自己动手实现一个高性能的打包工具,这或许就是上下游的问题,开发资源的分配问题了
相关推荐
海天胜景23 分钟前
无法加载文件 E:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
MingT 明天你好!26 分钟前
在vs code 中无法运行npm并报无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查
前端·npm·node.js·visual studio code
老兵发新帖31 分钟前
pnpm 与 npm 的核心区别
前端·npm·node.js
超级土豆粉33 分钟前
怎么打包发布到npm?——从零到一的详细指南
前端·npm·node.js
OpenTiny社区37 分钟前
TinyEngine 2.5版本正式发布:多选交互优化升级,页面预览支持热更新,性能持续跃升!
前端·低代码·开源·交互·opentiny
声声codeGrandMaster1 小时前
Django框架的前端部分使用Ajax请求一
前端·后端·python·ajax·django
重生之后端学习2 小时前
02-前端Web开发(JS+Vue+Ajax)
java·开发语言·前端·javascript·vue.js
繁依Fanyi3 小时前
用 CodeBuddy 实现「IdeaSpark 每日灵感卡」:一场 UI 与灵感的极简之旅
开发语言·前端·游戏·ui·编辑器·codebuddy首席试玩官
来自星星的坤5 小时前
【Vue 3 + Vue Router 4】如何正确重置路由实例(resetRouter)——避免“VueRouter is not defined”错误
前端·javascript·vue.js
香蕉可乐荷包蛋9 小时前
浅入ES5、ES6(ES2015)、ES2023(ES14)版本对比,及使用建议---ES6就够用(个人觉得)
前端·javascript·es6