vite
目前大多数框架的前端构建工具都已经被vite
取代,相信你已经使用过vite
了。可是在使用过程中,vite
对我来说一直是模糊的,现在就来一探究竟,为啥它更好?
接下来我将为从以下几点出发,究其原理
一、原生ES模块
1、Common JS
- 文件即模块,每一个文件都是一个 Module 实例,所有代码都运行带模块作用域,不会污染全局作用域(因为CommonJS 输出的是值的拷贝)
- 所有文件运行时同步加载,模块加载的顺序是按照其在代码中出现的顺序,加载完再执行。
- 每个模块加载一次后就会被缓存。后续require时,①先检查缓存中是否存在,②如果缓存中没有,检查是否是核心模块,如果是就直接加载,③如果不是核心模块,检查是否是文件模块,解析路径,根据解出的路径定位文件,然后执行,④如果以上都不是,沿当前路径向上级逐级递归,直到根目录的
node_modules
目录 CommonJS
模块直接放在浏览器中是无法执行的,所以要想在浏览器中运行,需要使用额外的工具(browserify
)
2、ES Module
- 浏览器支持:
ES6 Module
也被成为ES Module
,是由ESMAScript 官方提出的模块化规范。目前已经得到了现代浏览器的支持。如果在HTML中加入type ="module"
属性的 script 标签,那么浏览器会按照ES Module
规范来进行以来加载和模块解析,这也是 vite 在开发阶段实现no-bundle
的原因。由于浏览器可以加载模块,所以即使不打包可以顺利运行模块代码。 - 动态导入,按需加载
js
<script type="module">
(async () => {
const moduleSpecifier = './lib.mjs';
const {repeat, shout} = await import(moduleSpecifier);
repeat('hello');
// → 'hello hello'
shout('Dynamic import in action');
// → 'DYNAMIC IMPORT IN ACTION!'
})();
</script>
- import map:支持绝对路径(www.baidi.com)、相对路径(./module),同时支持直接写一个第三方包名,如 react、lodash。前两种都是浏览器原生支持的,但是对于第三中,放在浏览器中是无法直接执行的,
ESM
解决了这个问题,这里不详细解释,可自行搜索
vite 利用浏览器对原生ES模块的支持,采用按需加载的方式,每次请求一个模块,Vite仅仅返回该模块的内容,而不是整个应用的捆绑文件。
但是!webpack是通过捆绑的方式,将所有模块打包成一个或多个文件,这意味着什么,意味着我们在启动项目是需要对整个应用进行打包,会导致长时间的等待
二、预构建
1、预构建解决了什么问题
- 将其他格式(例如 UMD 、CommonJS)的产物转换为 ESM 格式,使其在浏览器通过
<script type = "module"><script/>
的方式正常加载。举个例子:大名鼎鼎的 React 就是基于 CommonJS ,而这种格式在vite中是无法直接运行的,但是Vite 没有办法限制第三方的规范,所以改变不了别人,那就改变自己,内部转换 - 打包第三方库的代码,将各个第三方库分散的文件合并在一起,减少 HTTP 请求,避免页面加载性能劣化。举个例子:
loadsh-es
是ES 版本,vite可以直接运行,但实际上,在它加载时会发出特别多的请求,导致页面加载的前几秒几乎处于卡顿状态。每次 import 都会触发一个新的文件请求,如果不加控制,会触发成百上千个网络请求,而 Chrome 对同一个域名在只能同时支持 6 个 HTTP 并发请求,导致页面加载十分缓慢。在加入依赖预构建之后,loadsh-es
的代码被打包成一个文件,这样请求的数量会减少很多,减轻浏览器压力,页面加载速度变快。
三、双引擎(EsBuild 和 Rollup)
1、EsBuild 预构建
esbuild
在 Vite 中的作用集中在提升构建速度、优化依赖处理和简化开发体验
- 快速构建
esbuild
是用 go 编写的构建工具,具有极快的编译速度。使得 Vite 在开发环境中可以快速响应文件变更,提供及时反馈 - 多核并存
js 是单线程串行,esbuild
直接新开一个线程 ,多线程并行,充分发挥多核优势 - 代码压缩,减少内存
esbuild 提供了一个 minify 配置允许用户去压缩代码体积
2、Rollup 打包
vite 选择在生产环境中利用Rollup
打包,并基于Rollup 本身成熟的打包能力进行扩展和优化
- css代码分割。如果某个异步模块中引入了一些 CSS 代码,Vite 就会自动将这些 CSS 抽取出来生成单独的文件,提高线上产物的缓存复用率。
- 自动预加载。Vite 会自动为入口 chunk 的依赖自动生成预加载标签。这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能。
- 异步Chunks 加载优化。在异步引入的 Chunk 中,通常会有一些公用的模块,如现有两个异步引入的 Chunk: A 和 B,而且两者有一个公共依赖 C一般情况下,Rollup 打包之后,会先请求 A,然后浏览器在加载 A 的过程中才决定请求和加载 C,但 Vite 进行优化之后,请求 A的同时会自动预加载 C,通过优化 Rollup 产物依赖加载方式节省了不必要的网络开销。
- 多产物配置。同一个入口文件,打包出多种格式的产物
javascript
const buildOptions = {
input: ["src/index.js"],
// 将 output 改造成一个数组
output: [
{
dir: "dist/es",
format: "esm",
},
{
dir: "dist/cjs",
format: "cjs",
},
],
};
export default buildOptions;
- 多入口配置,将 input 设置为一个数组或者一个对象:
javascript
{
input: ["src/index.js", "src/util.js"]
}
// 或者
{
input: {
index: "src/index.js",
util: "src/util.js",
},
}
四、热模块更新(HMR)
vite 热更新过程
- 创建一个 websocket 服务端和 client 文件,启动服务
- 通过 chokidar 监听文件变更
- 当代码变更后,服务端进行判断并推送到客户端
- 客户端根据推荐的信息执行不同操作的更新
webpack 热更新过程
- 使用
webpack-dev-server
(后面简称 WDS)托管静态资源,同时以 Runtime 方式注入一段处理 HMR 逻辑的客户端代码; - 浏览器加载页面后,与 WDS 建立 WebSocket 连接;
Webpack 监听到文件变化后,增量构建发生变更的模块, - 通过 WebSocket 发送
hash
事件; - 浏览器接收到 hash 事件后,请求
manifest
资源文件,确认增量变更范围; - 浏览器加载发生变更的增量模块;
- Webpack 运行时触发变更模块的
module.hot.accept
回调,执行代码变更逻辑; - done。