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. 模块化方案
目前主流的模块化方案主要有三种:
- ESM
ECMAScript Module 是随着ES6发布的ES标准模块解决方案, 提倡依赖预定义后使用, 同时, 引入的模块只能在顶层, 不能在函数或判断语句中引入
- CJS
CommonJS规范, 在Nodejs诞生以来是主流的服务端模块化方案, 通过require语句来引入模块, 动态性强, 能够在函数或判断语句中引入
- AMD
是一种浏览器端的模块化方案, 区别于CMD来说, AMD提倡预加载, 提前执行. 而CMD提倡预加载, 懒执行
- 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 解析,可以通过 defer
和 async
属性来让 JS 脚本异步加载。defer: 异步下载, 渲染完执行, 多个defer按顺序加载async: 异步下载, 下载完执行, 多个async不保证加载顺序
而 ESM 默认是通过
defer
的方式加载的,所以是不需要在 script
标签上加 defer
属性的。6. import机制
当 JS 引擎执行一个 ESM,大概有以下4个步骤:
- Parsing: 读取模块的代码并检查语法错误。
- Loading: 递归的加载所有导入的模块,建立 module graph。
- Linking: 对于每个新加载的模块,都会创建一个模块实例
Module.Instantiate
,并使用该模块中所有导出的内容的 内存地址 对import
进行映射。
- 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层)的比较简单的页面。
在生产环节还是需要进行打包优化,可以减少代码体积,提升加载速度,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. 开发阶段
开发阶段的基本步骤流程如下:
启动流程:
- 解析config配置, 对默认配置等进行处理(config hooks 完成之后, 装载完plugins之后, 调用 configResolved hooks)
- 启动httpServer的middleware中间件壳子
- 启动websocket服务
- 启动file watch, 开始监听文件变更
- 初始化模块依赖关系的图
- 创建插件容器, 调用插件的options hooks. 对参数进行插件化的修改, 同时注册传入的各种插件
- 创建server对象, 执行transformIndexHtml hooks来初始化入口的html文件(就是在这里注入了/@vite/clitent)
- 启动各种回调, 如文件监听回调, websocket消息回调等等
- 调用configureServer hooks, 从插件中读取serverConfig, 注册httpServer中间件, 其中有核心的transformer中间件, 等等, 用户在请求的时候进行请求资源构建与缓存等
- 初始化server initServer , 先执行buildStart hooks, 然后同步执行depsOptimize, 进行依赖的预构建, 如果没有cachedMetadata, 则进行依赖与构建
- 在esbuild的plugin中, 通过onResolved生命周期找到node_modules中的依赖, 对其进行处理和记录生成metadata
调用流程:
- 启动之后, 我们请求对应的文件, 就会启动对应的构建流程, 因为是使用的esm模块, 所以浏览器会很方便的进行依赖的分开请求
- 在html入口中, 注入了
/@vite/client
和/src/main.js
, 因此第一次会请求这两个对象
- 每个对象都会按顺序流程走 resolveId hooks > load hooks > transform hooks 流程, moduleParsed hooks在开发中是不会调用的, 因为esbuild构建不会暴露出ast相关的流程, 这也是vite的设计哲学
- 服务关闭后, 会执行 buildEnd hooks > closeBundle hooks
2. 生产构建
生产构建主要是基于rollup的构建流程
基本流程如下
- 进行内置的config初始化, 基本初始化完成后, 会调用config hooks, 完事儿后调用configResolved hooks
- 通过配置的entry来处理构建入口信息, 通过output处理构建出口信息
- 把整个的构建参数丢给rollup来进行构建
- 使用rollup的generate来进行构建产物生成