Vite 是什么
Vite(法语单词,“快” 的意思)由 vue 作者尤雨溪开发的 web 开发工具,是一种新型的前端构建工具。
Vite,一个基于浏览器原生 ES imports 的开发服务器,利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随启随用,它是下一代前端开发与构建工具。
Vite 优缺点
优点:
💡 极速的服务启动:使用原生 ESM 文件,无需打包
⚡️ 轻量快速的热重载:无论应用程序大小如何,都始终极快的模块热重载(HMR)
🛠️ 丰富的功能:对 TypeScript、JSX、CSS 等支持开箱即用
📦 优化的构建:可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
🔩 通用的插件:在开发和构建之间共享 Rollup-superset 插件接口
🔑 完全类型化的API:灵活的 API 和完整的 TypeScript 类型
缺点:
- 生态不如 webpack 丰富
- 生产环境的构建,目前用的 Rollup:原因在于 ESBuild 对于代码分割和 CSS 处理方面不是很友好
- …
为什么选 Vite
当代的前端构建工具有很多,比较受欢迎的有 Webpack、Rollup、Parcel等,绝大多数脚手架工具都是使用 Webpack 作为构建工具,如 Vue-CLI。
在利用 Webpack 作为构建工具时,开发过程中,每次修改代码,都会导致重新编译,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。这就会导致热更新的速度也随之变慢,甚至要几秒钟才能看到视图的更新。生产环境下,它将各个模块之间通过编码的方式联系在一起,最终生成一个庞大的 bundle 文件。
导致这些问题出现的原因,有以下几点:
HTTP 1.1 时代,各个浏览器资源请求并发是有上限的
如谷歌浏览器为 6 个,这导致你必须要减少资源请求数
浏览器并不支持 CommonJS 模块化系统
它不能直接运行在浏览器环境下,它是 Node 提出的模块化规范,所以需要经过 Webpack 的打包,编译成浏览器可识别的 JS 脚本)
模块与模块之间的依赖顺序和管理问题
文件依赖层级越多,静态资源也就变得越多,如果一个资源有 100 个依赖关系,可能需要加载 100 个网络请求,这对生产环境可能是灾难,所以在生产环境最终会打包成一个 bundle 脚本,会提前进行资源按需加载的配置。
不打包的构建趋势原因
工程越来越庞大,热更新变得缓慢,影响开发体验。
各大浏览器已经开始慢慢的支持原生 ES Module (谷歌、火狐、Safari、Edge 的最新版本,都已支持。)
HTTP 2.0 采用的多路复用。不用太担心请求并发量的问题。
越来越多的 npm 包开始采用了原生 ESM 的开发形式。
bundle 和 bundleless 的区别
Bundle(Webpack) | Bundleless(Vite) | |
开发环境启动 | 需完成打包构建,存入内存之后才能启动 | 只需启动开发服务器,按需加载 |
项目构建时间 | 随项目体积线性增长 | 构建时间复杂度O(1) |
文件加载 | 加载打包后的 bundle | 通过请求,映射到本地 |
文件更新 | 重新打包构建 | 不重新打包 |
开发调试 | 依赖 Source Map | 可单文件直接调试 |
周边生态 | loader和plugin非常丰富 | 生态不够webpack成熟 |
聊一聊 ES Module
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的 require、Python 的 import,甚至就连 CSS 都有 @import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块 let { stat, exists, readfile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过 export
命令显式指定输出的代码,再通过 import
命令输入。
// ES6模块 import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
模块功能主要由两个命令构成:export 和 import。
export 命令用于规定模块的对外接口
import 命令用于输入其他模块提供的功能
ES Module 基本特性
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用 this
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。
ESM 是通过 CORS 的方式请求外部 JS 模块的
ESM 的 script 标签会延迟执行脚本(浏览器页面渲染后执行)
Vite 进行了什么改进
1、缓慢的服务器启动
当冷启动开发服务器时,基于打包器的方式是在提供服务前去急切地抓取和构建你的整个应用。
那么 Vite 是怎么处理的?
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。
依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。
Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
Vue 脚手架工具 vue-cli 使用 webpack 进行打包,开发时可以启动本地开发服务器,实时预览。因为需要对整个项目文件进行打包,开发服务器启动缓慢。
webpack 打包过程:
识别入口文件
通过逐层识别模块依赖。(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析。来获取代码的依赖)
webpack 做的就是分析代码,转换代码,编译代码,输出代码
最终形成打包后的代码
- Vite 只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理,实现真正的按需加载。
举个例子:
当声明一个 script 标签类型为 module 时:
<script type="module" src="/src/main.js"></script>
浏览器就会像服务器发起一个GET:http://localhost:3000/src/main.js
去请求 main.js
文件:
// /src/main.js: import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
浏览器请求到了 main.js
文件,检测到内部含有 import
引入的包,又会对其内部的 import
引用发起 HTTP 请求获取模块的内容文件
GET http://localhost:3000/@modules/vue.js GET http://localhost:3000/src/App.vue
而 Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译。
2、缓慢的更新
基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。
一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活,但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验 —— 然而,在实践中我们发现,即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。
那么 Vite 是怎么处理的?
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。
Vite 构建原理
Vite 的生产模式和开发模式是不同的概念。Vite 在开发模式下,有一个 依赖预构建
的概念。
什么是依赖预构建
在 Vite 启动开发服务器之后,它将第三方依赖的多个静态资源整合为一个,比如 lodash、qs、axios
等这类资源包,存入 ·node_modules/.vite
文件下。
为什么需要依赖预构建
如果直接采用 ES Module 的形式开发代码,会产生一大串依赖,就好像俄罗斯套娃一样,一层一层的嵌套,在浏览器资源有限的情况下,同时请求大量的静态资源,会造成浏览器的卡顿,并且资源响应的时间也会变慢。
下面通过两个例子来详细分析为什么需要依赖预构建。