vite 为什么快
一、ES module 减少服务启动时间
import { foo } from './other-module'
由于大多数现代浏览器都支持上面的 ES module 语法,所以在开发阶段,我们就不必对其进行打包,这节省了大量的服务启动时间。另外,vite 按需加载当前页面所需文件,一个文件一个http请求,进一步减少启动时间。
二、缓存减少页面更新时间
每个文件通过 http 头缓存在浏览器端,当编辑完一个文件,只需让此文件缓存失效。当基于 ES module 进行热更新时,仅需更新失效的模块,这使得更新时间不随包的增大而增大。
vite 怎么做到的?
当我们执行yarn dev
时,vite-cli 会在本地启动一个 koa 服务:
export function createServer(config) {
...
const app = new Koa()
const server = resolveServer(config, app.callback())
...
// 加载插件
...
const listen = server.listen.bind(server)
server.listen = (async (port, ...args) => {
if (optimizeDeps.auto !== false) {
await require('../optimizer').optimizeDeps(config)
}
return listen(port, ...args)
})
...
return server;
}
监听端口,执行其他服务之前,会执行optimizeDeps
方法,即优化依赖。vite 文档将这部分优化叫做依赖预打包Dependency Pre-Bundling,这么做的理由有两个:一是将非 ES module转化为可被浏览器导入的 ESM;二是将 ESM 依赖的多个内部模块转化为一个模块,以减少浏览器请求从而提升页面加载速度。
按需加载
上面我们提到,vite通过按需加载减少等待时间,这是如何做到的?所谓一生二,二生三,三生万物。一切的根源就是服务启动后,我们访问的初始地址http://localhost:3000/
。首页,浏览器根据这个 url 发出第一个请求,通过serverPluginServeStatic.ts
插件获取到位于项目根目录的index.html
文件,再通过serverPluginHtml.ts
向其插入<script type="module">import "/vite/client"</script>
,最终我们看到第一个请求返回的内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">import "/vite/client"</script>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
我们先看<script type="module" src="/src/main.js"></script>
,浏览器向我们的本地服务器请求这个文件,koa 插件serverPluginModuleRewrite.ts
会重写main.js
源文件的 import 的解析方式:
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
上面的内容经过插件处理后,将以下内容返回给浏览器:
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'
这里文档npm-dependency-resolving也有部分提及。浏览器解析此文件,依次发送三个请求:
- http://localhost:3000/@modules/vue.js
- http://localhost:3000/src/App.vue
- http://localhost:3000/src/index.css?import
当浏览器发送第二个请求,koa 插件serverPluginVue.ts
会编译单文件组件 App.vue,并打上 etag(用于缓存)后再返回给浏览器(A流程)。浏览器再解析此文件并更新页面,重复这个过程(发送请求、服务端响应、浏览器解析)直到不再发送请求。
组件热更新
vue 组件热更新,由serverPluginVue.ts
、serverPluginHmr.ts
、client.ts
三个部分共同完成。其中client.ts
会被发送到浏览器端,位于服务端的 serverPluginHmr.ts 通过 websocket 和 client.ts 进行通信。
我们以vite-cli默认模板项目举例,假设更改了App.vue
文件内容,此时:
-
1)serverPluginVue.ts 使用 serverPluginHmr.ts 提供的 send 方法向 client.ts 所在的浏览器端发送数据,即数据通过 websockt 从服务端推送到浏览器,数据内容如下:
{ "type": "vue-reload", "path": "/src/App.vue", "changeSrcPath": "/src/App.vue", "timestamp": 1609815863830 }
-
2) client.ts 收到数据后,执行
import('/src/App.vue?t=1609815863830')
-
3)接着浏览器发起请求:
http://localhost:3000/src/App.vue?t=1609815863830
,时间戳的作用是使url缓存失效 -
4)最后,执行重复上面的A流程。
经过上述步骤,页面得以更新。当然,更新的类型还有full-reload
、js-update
以及样式更新等,但总体流程都框定在以上步骤,这里就不展开了。
p.s.以上内容搭配vite源码再看更易理解