Vite:起源与实现机制
2022-1-22
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 03:54 PM
type
Post
status
Published
date
Jan 22, 2022
slug
summary
Vite:起源与实现机制
tags
Vue
Vite
category
源码
icon

一. 前置知识

1. 模块化方案

目前主流的模块化方案主要有三种:
  1. ESM
ECMAScript Module 是随着ES6发布的ES标准模块解决方案, 提倡依赖预定义后使用, 同时, 引入的模块只能在顶层, 不能在函数或判断语句中引入
  1. CJS
CommonJS规范, 在Nodejs诞生以来是主流的服务端模块化方案, 通过require语句来引入模块, 动态性强, 能够在函数或判断语句中引入
  1. AMD
是一种浏览器端的模块化方案, 区别于CMD来说, AMD提倡预加载, 提前执行. 而CMD提倡预加载, 懒执行
  1. UMD
UMD其实是 AMD + CJS + 全局变量的能力集合, UMD集合支持了这三种引入方式以实现多种调用方式的支持

2. 浏览器端的ESM

1. 与传统script的区别

  • 自动为严格模式
  • 有模块作用域, 所有的文件内声明都在作用域内, 只能通过export来导出内部变量
  • 顶层this是undefined, 但可以用globalThis来代替this进行window对象调用
  • 支持顶层await

2. 引入方式

 <script type="module" src="main.mjs"></script>  <script nomodule src="fallback.js"></script>
使用type="module"来指定我们页面需要使用到的ESM, 同时, 可以通过nomodule标签来降级处理浏览器不支持ESM的场景

3. 文件后缀

ESM在nodejs上可以直接通过定义mjs的文件来使nodejs识别这是一个ESM的模块
但是在浏览器中, 主要还是通过script的type标签和js文件的content-type是否为application/javascript来进行判断, 因为mjs并不是一个浏览器标准的文件类型, 可能造成无法识别的情况, 因此更建议使用js作为文件扩展名

4. 跨域

ESM 在浏览器中会有 CORS 跨域问题,所有跨域的 ESM 资源加载都需要在资源响应头上添加 Access-Control-Allow-Origin 的响应头,而在之前的 js 资源加载上是不需要的。

5. 加载机制

默认的 <script> 标签加载资源会阻塞 HTML 解析,可以通过 deferasync 属性来让 JS 脚本异步加载。
defer: 异步下载, 渲染完执行, 多个defer按顺序加载
async: 异步下载, 下载完执行, 多个async不保证加载顺序
而 ESM 默认是通过 defer 的方式加载的,所以是不需要在 script 标签上加 defer 属性的。
notion image

6. import机制

当 JS 引擎执行一个 ESM,大概有以下4个步骤:
  1. Parsing: 读取模块的代码并检查语法错误。
  1. Loading: 递归的加载所有导入的模块,建立 module graph。
  1. Linking: 对于每个新加载的模块,都会创建一个模块实例 Module.Instantiate,并使用该模块中所有导出的内容的 内存地址import 进行映射。
  1. Run time: 最后,运行每个新加载模块的主体代码,此时,import 已经处理完成了。
所以,所有模块的静态依赖在该模块代码执行前都必须下载、解析并进行 Linking。 一个应用程序可能有几百个依赖,如果某个依赖加载出错了,则不会运行任何代码。

7. DynamicImport 异步加载

按需加载通过import函数, 传入一个路径作为参数, 返回一个包含ESM export对象的promise
 <script type="module">    const moduleSpecifier = './05/lib.js';    import(moduleSpecifier)     .then(({ repeat, shout }) => {        repeat('hello');    // → 'hello hello'        shout('Dynamic import in action');    // → 'DYNAMIC IMPORT IN ACTION!'     });  </script>

8. Import Maps

通过 URL 引入依赖不是很方便,如果想通过 pacakge name 引入,可以使用 Import Maps 来实现。
 <script type="importmap">  {   "imports": {     "moment": "/node_modules/moment/src/moment.js",     "lodash": "/node_modules/lodash-es/lodash.js"   }  }  </script>
Import Maps 定义了模块导入名称的映射,可以使用 bare 导入。
除了直接制定 package name 外,还可以当作 path resolve:

9. import.meta

在代码中可以通过 import.meta 获取前端模块的元数据,获取的元数据内容 ECMAScript 中没有制定标准,所以取决于具体的 runtime 运行环境。在浏览器一般会有 import.meta.url,表示模块的资源链接。

10. 性能优化

1. 打包

使用 ESM,完全可以不借助于 webpack/parcel/rollup 等打包工具直接进行网站开发,但目前只有少部分场景适用:
  • 本地开发
  • 依赖少于100个,依赖层级比较浅(最深不超过5层)的比较简单的页面。
notion image
在生产环节还是需要进行打包优化,可以减少代码体积,提升加载速度,Vite 也是采用 rollup.js 进行生产环境打包。

2. 预加载

可以使用 <link rel="modulepreload"> 进行 ESM 的预加载。通过这种方式,浏览器可以预加载甚至预编译 ESM 及其依赖。
 <link rel="modulepreload" href="lib.mjs">  <link rel="modulepreload" href="main.mjs">  <script type="module" src="main.mjs"></script>  <script nomodule src="fallback.js"></script>
这对依赖比较多,层级比较深的应用很有帮助。但是如果不使用 rel="modulepreload",那么浏览器需要实际加载 ESM 的时候通过多个 HTTP 请求构建 module graph,如果把所有的模块都进行预加载,可以大大节省依赖加载的时间。

3. 使用 HTTP/2、HTTP/3

通过 HTTP/2 或 HTTP/3多路复用,可以同时传输多个请求和响应消息,对于 模块树 加载有很高的性能提升。
其实多文件加载在首次加载有劣势,在多次访问的时候也有优势,当我们有100个模块时,有一个模块改了东西,打包成一个文件的话,整个文件浏览器都需要重新下载,不能被缓存。而使用 ESM,模块就可以单独的压缩上线,而不影响其他没有修改的模块。

11. 参考

二. Vite做了什么

传统的打包工具(如webpack), 会将资源进行依赖分析之后打包汇总成为一个Bundle
在热修复的场景下, 会跟踪更新内容, 再通过websocket+jsonp来更新bundle
Vite的思路则不同, 采用了nobundle方案, 基于浏览器支持的ESM, 将文件转为ESM模块化方案之后直接输出到前端, 再结合缓存的手段, 极大的提升了构建性能

三. Vite实现

Vite的打包与生产构建是截然不同的流程, 开发阶段主要是启动开发服务器, websocket服务器, 进行文件监听
而生产构建则核心是使用rollup的构建过程

1. 开发阶段

开发阶段的基本步骤流程如下:
启动流程:
  1. 解析config配置, 对默认配置等进行处理(config hooks 完成之后, 装载完plugins之后, 调用 configResolved hooks)
  1. 启动httpServer的middleware中间件壳子
  1. 启动websocket服务
  1. 启动file watch, 开始监听文件变更
  1. 初始化模块依赖关系的图
  1. 创建插件容器, 调用插件的options hooks. 对参数进行插件化的修改, 同时注册传入的各种插件
  1. 创建server对象, 执行transformIndexHtml hooks来初始化入口的html文件(就是在这里注入了/@vite/clitent)
  1. 启动各种回调, 如文件监听回调, websocket消息回调等等
  1. 调用configureServer hooks, 从插件中读取serverConfig, 注册httpServer中间件, 其中有核心的transformer中间件, 等等, 用户在请求的时候进行请求资源构建与缓存等
  1. 初始化server initServer , 先执行buildStart hooks, 然后同步执行depsOptimize, 进行依赖的预构建, 如果没有cachedMetadata, 则进行依赖与构建
  1. 在esbuild的plugin中, 通过onResolved生命周期找到node_modules中的依赖, 对其进行处理和记录生成metadata
调用流程:
  1. 启动之后, 我们请求对应的文件, 就会启动对应的构建流程, 因为是使用的esm模块, 所以浏览器会很方便的进行依赖的分开请求
  1. 在html入口中, 注入了/@vite/client/src/main.js, 因此第一次会请求这两个对象
  1. 每个对象都会按顺序流程走 resolveId hooks > load hooks > transform hooks 流程, moduleParsed hooks在开发中是不会调用的, 因为esbuild构建不会暴露出ast相关的流程, 这也是vite的设计哲学
  1. 服务关闭后, 会执行 buildEnd hooks > closeBundle hooks

2. 生产构建

生产构建主要是基于rollup的构建流程
基本流程如下
  1. 进行内置的config初始化, 基本初始化完成后, 会调用config hooks, 完事儿后调用configResolved hooks
  1. 通过配置的entry来处理构建入口信息, 通过output处理构建出口信息
  1. 把整个的构建参数丢给rollup来进行构建
  1. 使用rollup的generate来进行构建产物生成

四. Vite的插件机制

源码
  • Vue
  • Vite
  • Webpack的基础设施:Tapable关于Babel的那些事儿
    目录