vite 的由来
# vite 上手与解读
# Vite 的定义
Vite 是面向现代浏览器的一个更轻、更快的 Web 应用开发工具,核心基于 ECMAScript 标准原生模块系统(ES Modules)实现。
表象功能上看,Vite 可以取代基于 Webpack 的 vue-cli 或者 cra 的集成式开发工具,提供全新的一种开发体验。
具体细节往下看。
# Vite 的由来
1、webpack 的使命与缺陷
使命:
webpack 是一个由 nodejs 编写的模块打包机,它会把项目存在的文件视为一个个模块,在编译时将模块依赖进行编译、压缩等处理,最终打包成浏览器可正常执行的文件
在以往的浏览器中,往往就是因为需要考虑各种各样的兼容,各种各样的模块关系处理(如:tree shaking、公共模块抽离)等,而 webpack 可以使得开发者无需太关注这些
缺陷:
由于首次启动没有缓存,且需要全量编译,Webpack Dev Server 冷启动时间会比较长,稍大一点的项目启动开发服务都需要等待 10 - 20 秒,大型项目启动时间更是要四五分钟
而由于每次均需要重新编译,因此 Webpack HMR 热更新的反应速度比较慢,修改完代码需要等待编译器全部编译完成才能开始同步到浏览器
2、webpack 的接力棒
现在,随着 IE 的市场份额越来越小,微软最新版的 Edge 甚至采用了 Chromium 内核,这就使得低版本的兼容是否必要存在了存在了些许争议
我们都知道 ESM 有兼容问题需要 webpack 来处理,但是倘若我们不再考虑去兼容,而直接使用浏览器支持的 ESM 呢?
vue 的作者尤雨溪(尤小右)基于这个角度,就在 vue3.0 上做实验,并将之进行了实现,称这个思想实现的工具为 vite
尤小右微博这样写道:
“Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。”
# vite 的基本使用
vite 官方搭配的 Demo 是配合 vue3 使用的,通过以下命令可以将 vite + vue3.0 拉取到本地,并装上依赖
Vite 官方目前提供了一个比较简单的脚手架:create-vite-app,可以使用这个脚手架快速创建一个使用 Vite 构建的 Vue.js 应用
npm init vite-app vite-demo
cd vite-demo
npm install
npm run dev
2
3
4
注:
npm init vite-app vite-demo 相当于 npx create-vite-app vite-demo,即先执行 npm install create-vite-app,再执行 create-vite-app vite-demo
# 进一步分析 vite
1、对比差异点
打开生成的项目过后,你会发现就是一个很普通的 Vue.js 应用,没有太多特殊的地方。
但是比起之前基于 vue2 的 webpack 项目,这个基于 vue3 的 vite 项目开发依赖只有 vue、 vite 和 @vue/compiler-sfc。
{
"name": "vite-demo",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"vite": "^1.0.0-rc.1",
"@vue/compiler-sfc": "^3.0.0-rc.1"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vite 取代了原来的 webpack 作为本地开发和构建工具,而 @vue/compiler-sfc 是编译项目中的 vue 单文件组件(SFC),这个是取代 Vue2 的 vue-template-compiler 的。
Vite 目前只支持 Vue.js 3.0 版本。
如果你想,在了解完实现原理过后,你也可以改造 Vite 让它支持 Vue2 或者 react。
2、基础体验
这里我们所安装的 vite 模块提供了两个子命令:
- serve:启动一个用于开发的服务器
- build:构建整个项目(上线)
当我们执行 vite serve 的时候,你会发现响应速度非常快,几乎就是秒开。
对比基于 Webpack 的 vue-cli-service 开发服务器,差距十分明显。
前面我们说过了,Webpack Dev Server 在启动时,需要先 build 一遍,而 build 的过程是需要耗费很多时间的。
但是 Vite 则完全不同,当我们执行 vite serve 时,内部直接启动了 Web Server,并不会先编译所有的代码文件。
只是启动 Web Server,速度上必然会就快很多。
Webpack 这类工具的做法是将所有模块提前编译、打包,不管模块是否会被执行,都要被编译和打包。随着项目体积增大,打包的速度也会变慢。
Vite 利用现代浏览器原生支持 ESM 特性,省略了对模块的打包。
对于需要编译的文件,Vite 采用的是另外一种模式:即时编译。
也就是说,只有具体去请求某个文件时才会编译这个文件。
所以,这种即时编译
的好处主要体现在:按需编译。
3、Optimize
ES 的模块支持其实只支持绝对路径和相对路径,但是并不会像 webpack 一样,会支持各种写法去查找依赖。
这里,其实 Vite 还提供了一个目前在帮助列表中并没有呈现的一个子命令:optimize。
在我们执行npm run dev
启动项目的时候,会发现命令行返回了一个 [vite] Optimizable dependencies detected: vue
的提示,接着我们可以在 node_modules 中发现存在.vite_opt_cache
的文件夹,这个文件夹是 vite 的缓存目录,会将首次执行所需要的第三方依赖(node_modules)缓存到该路径。这其实就是优化依赖
。
我们在代码中通过 import 载入了 vue 这个模块,那通过这个命令就会自动将这个模块打包成一个单独的 ESM bundle, 放到 node_modules/.vite_opt_cache
目录中。
这样后续请求这个文件时就不需要再即时去加载了。
随后打开页面localhost:3000
,先查看 index.html 源码,页面先通过 esm 的方式引入了/vite/client
,这是 vite 的客户端部分,再通过 src 的方式引入/src/main.js
,
浏览器的 esm 在 import 的时候只能是文件的相对路径或绝对路径,很显然,在浏览器中的@符号是无法正常解析的,这里 vite 对路径做了替换,使得 esm 可以正常读取到模块。
4、HMR
在热更新的时候,Vite 只需要编译修改过的文件即可,因此响应速度非常快。
5、Build
使用 vite build 进行生产模式打包。
vite 采用了 Rollup 来完成打包,最终会把文件编译并打包到一起,这点和 webpack 区别不大,但是 rollup 显然更轻量。
而且,对于代码拆分的需求,Vite 内部采用了原生的动态加载(Dynamic imports)特性来进行实现,因此打包结果还是只能够支持现代浏览器。
不过 Dynamic imports 可以通过 Polyfill (opens new window) 来兼容低版本浏览器。
# vite 的原理
Vite 的核心功能:Static Server + Compile + HMR
核心思路:
- 将当前项目目录作为静态文件服务器的根目录
- 拦截部分文件请求
- 处理代码中 import node_modules 中的模块
- 处理 vue 单文件组件(SFC)的编译
- 通过 WebSocket 实现 HMR
# 手写实现
详细参考 https://github.com/zce/vite-essentials
#!/usr/bin/env node
const path = require("path");
const { Readable } = require("stream");
const Koa = require("koa");
const send = require("koa-send");
const compilerSfc = require("@vue/compiler-sfc");
const cwd = process.cwd();
const streamToString = (stream) =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
stream.on("error", reject);
});
const app = new Koa();
// 重写请求路径,/@modules/xxx => /node_modules/
app.use(async (ctx, next) => {
if (ctx.path.startsWith("/@modules/")) {
const moduleName = ctx.path.substr(10); // => vue
const modulePkg = require(path.join(
cwd,
"node_modules",
moduleName,
"package.json"
));
ctx.path = path.join("/node_modules", moduleName, modulePkg.module);
}
await next();
});
// 根据请求路径得到相应文件 /index.html
app.use(async (ctx, next) => {
await send(ctx, ctx.path, { root: cwd, index: "index.html" }); // 有可能还需要额外处理相应结果
await next();
});
// .vue 文件请求的处理,即时编译
app.use(async (ctx, next) => {
if (ctx.path.endsWith(".vue")) {
const contents = await streamToString(ctx.body);
const { descriptor } = compilerSfc.parse(contents);
let code;
if (ctx.query.type === undefined) {
code = descriptor.script.content;
code = code.replace(/export\s+default\s+/, "const __script = ");
code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script`;
// console.log(code)
ctx.type = "application/javascript";
ctx.body = Readable.from(Buffer.from(code));
} else if (ctx.query.type === "template") {
const templateRender = compilerSfc.compileTemplate({
source: descriptor.template.content,
});
code = templateRender.code;
}
ctx.type = "application/javascript";
ctx.body = Readable.from(Buffer.from(code));
}
await next();
});
// 替换代码中特殊位置
app.use(async (ctx, next) => {
if (ctx.type === "application/javascript") {
const contents = await streamToString(ctx.body);
ctx.body = contents
.replace(/(from\s+['"])(?![\.\/])/g, "$1/@modules/")
.replace(/process\.env\.NODE_ENV/g, '"production"');
}
});
app.listen(8080);
console.log("Server running @ http://localhost:8080");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 附录
# 1、各大浏览器对 ES6 的支持
pc 端:
- Chrome:Chrome 51 起便可以支持 97% 的 ES6 新特性,目前最新版的 Chrome 85 支持 98% 的特性
- Firefox:Firefox 53 起便可以支持 97% 的 ES6 新特性,目前最新版的 Firefox 81 与 Chrome 保持一致, 支持 98% 的特性
- Safari:Safari 10 起便可以支持 99% 的 ES6 新特性,Safari 14 实现了 100%特性的支持
- IE:IE7~11 基本不支持 ES6,到目前为止 IE11 仅支持 11%左右的特性
- Edge: Edge 14 可以支持 93% 的 ES6 新特性,Edge15 至最新的 18 可以支持 96% 的 ES6 新特性
移动端:
- iOS Safari:10.0 版起便可以支持 99% 的 ES6 新特性。
- Android Browser:Android 4.4.4 及以下是完全不支持的,Android5.1 开始才支持 25%,Android6.0 以上可以支持大多数 ES6 新特性
- QQ 浏览器:2017 年末,QQ 浏览器开始使用 X5 内核(基于 Chromium 57), 支持了大多数的 ES6 特性
- UC 浏览器:2017 年末,UC 浏览器开始使用 U4 内核(基于 Chromium 57), 支持了大多数的 ES6 特性
服务端:
- Node.js:6.5 版起便可以支持 97% 的 ES6 新特性。(6.0 支持 92%)