本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
上篇我们提到webpack相关的设计和实现,Vite的出现确实动摇了一下老大哥webpack的江湖地位,从2021年2月份Vite2更新,到2022年7月份Vite3发布,到今年2022年11月份Vite 4.0.0-alpha.0 (2022-11-07)开始更新,不得不说,Vite在改善前端开发体验的道路上越来越卷了,下面我们还是带着问题去探索Vite的核心设计和实现:
一、Vite号称下一代的前端工具链,解决了什么问题?
1、改进了前端开发服务器启动时间
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了传统工具,在开发服务器启动时间
依赖,Vite 将会使用 esbuild 预构建依赖,esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍
源码,这里通常是指一些需要转换的文件(比如JSX,CSS 或者 Vue/Svelte 组件),Vite 以 原生 ESM 方式提供源码,让浏览器接管打包程序的工作,并按需提供源码。
二、Vite启动都做了什么?
注: 本文vite源码版本为 v3.2.4
因为在之前的文章有写类似查看源码的方式,这里就快速定位了 packages/vite/src/node/cli.ts
可以看到这里创建server,是通过引用packages\vite\src\node\server\index.ts
里的createServer()
方法,我们重点看看这个方法主要干了什么事情
我们看着这段代码往下读:
...
const config = await resolveConfig(inlineConfig, 'serve', 'development')
...
(1)首先会通过resolveConfig
函数解析启动服务时候需要的配置,这包含plugins
用户插件和内建插件、cacheDir
npm 依赖预构建之后的缓存目录、在之后浏览器按需获取文件时对请求进行截获,返回相对应内容的处理函数 createResolve
,以及定义在 vite.config.js
里面的 resolve
,包含用户自定义的一些 alias
文件的处理等。
...
const middlewares = connect() as Connect.Server
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
...
(2)创建httpServer
、ws
服务,创建watcher
,设置代码文件监听,创建server
对象,
文件监听变动,websocket
向前端通信。然后注册一系列中间件用于处理浏览器请求,包括对 /
、js/css/vue
的请求等
...
const container = await createPluginContainer(config, moduleGraph, watcher)
...
(3)创建插件处理中心container
...
const server: ViteDevServer = {
...
}
server.transformIndexHtml = createDevHtmlTransformFn(server)
...
(4)处理 html
进行转换,在 html
文件中注入我们在 localhost
network 面板中看到的 <script type="module" src="/@vite/client"></script>
脚本,运行 vite
相关的 client
脚本内容。
(5)在服务启动前,首先执行 container.buildStart({})
调用所有注册插件的 buildStart
钩子函数,然后运行 initDepsOptimizer()
优化预构建的依赖,接收来自浏览器的请求。
三、解析通过源码vite的中的一些原理
3.1 Vite
为什么要做预构建?
个人认为这是Vite
在追求极速的服务启动,牺牲首屏渲染速度之间的一个选择。实现Vite 针对用户项目中的各种文件都是不做打包处理的,而是在浏览器运行时按需请求,并进行转换处理,相比其他构建工具需要打包依赖再启动服务来说,在耗时上是质的飞跃。
前面我们对vite
启动过程的分析知道,启动Vite
它会收集处理config
、注册各种中间件、初始化一些之后会用到的插件容器 container
以及模块依赖图 moduleGraph
等事情都不消耗时间的,最消耗时间的就依赖预构建。我们可以简单看一下Vite的预构建做了那些事情
(1) 缓存判断,node_modules\.vite\_metadata.json
看看是不是有上次缓存的预构建结果,通过判断存储缓存信息文件hash值是否与最新的hash值相同,要是相同则返回上次缓存的预构建结果
(2)依赖扫描,如果没有缓存结果和缓存hash值不一致,通过discoverProjectDependencies()
进行依赖扫描,从入口开始收集依赖,得到deps,将这些依赖项添加到已发现的列表中,preAliasPlugin用来支持别名和优化的deps。
(3)构建依赖,通过esbuildDepPlugin()
在这个过程中它将将非 ESM
规范的代码转换为符合 ESM
规范的代码,将第三方依赖内部的多个文件合并为单一的可缓存文件,减少 http
请求数量。
网上有人专门拿出import { debounce } from "lodash-es"
做实验,直接使用会时浏览器会导入600+个文件,需要1秒多,而经过依赖预构建之后,浏览器只需要导入一个文件,且只需 20 ms
(4)最后,执行writeFile
,再将相关信息保存到_metadata.json
,我们可以看看它的内容
{
"hash": "1a547ddf",
"browserHash": "2065b8ab",
"optimized": {
"@vue/runtime-core": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/@vue_runtime-core.js",
"src": "E:/codeWorlk/xxx/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js",
"needsInterop": false
},
"vue": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/vue.js",
"src": "E:/codeWorlk/xxx/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"needsInterop": false
},
"vue-router": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/vue-router.js",
"src": "E:/codeWorlk/xxx/node_modules/vue-router/dist/vue-router.esm-bundler.js",
"needsInterop": false
},
"@vue/reactivity": {
"file": "E:/codeWorlk/xxx/node_modules/.vite/@vue_reactivity.js",
"src": "E:/codeWorlk/xxx/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js",
"needsInterop": false
}
}
}
从上面我们可以看到 Vite 只对 npm 依赖进行预构建,对于用户编写的文件不进行预处理,而是通过浏览器支持的 ES Module 来进行按需读取,所以如果用户文件过多,且没有进行一定的 Code Spliting 等操作。这时候首屏加载渲染是非常慢的。
3.2 一个请求到Vite服务的过程是怎么样的?
<script type="module" src="/src/main.js"></script>
// or
import { get } from './utils'
我们通过上文Vite创建服务的过程源码看到 通过transformMiddleware()
函数去处理请求
// main transform middleware
middlewares.use(transformMiddleware(server))
(1)首先它会判断是否请求, 如果是请求,就会进入使用插件容器解析、加载和转换,不是请求就交给其他插件处理
(2)transformRequest()进行解析、加载和转换请求
(3)判断依赖图谱中是否已经存在,存在的话直接返回module
(4)没有话调用插件的路径解析钩子(对于不同的资源会有不同的插件去处理),创建module并保存
(5)最后调用esbuildPlugin插件的transfrom钩子,返回编译后的code,并缓存到依赖map里
(3)
Vite
是怎么实现快速的热重载(HMR)?
HMR的原理大多差不多,主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
我们回顾启动Vite服务的(createServer)时候
const ws = createWebSocketServer(httpServer, config, httpsOptions)
就启动了WebSocketServer服务,然后监听文件的变化
watcher.on('change', async (file) => {
file = normalizePath(file)
if (file.endsWith('/package.json')) {
return invalidatePackageData(packageCache, file)
}
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err)
})
}
}
})
如果有变化,则则执行监听回调函数handleHMRUpdate()
,重新打包文件,并通知到客户端,客户端接收到信息,重新加载到更新后的模块。
最后
Vite4.0.0都悄悄在“卷”出来了,期待它在一次次更新中对前端人更加的友好,本文也是浅读源码其中的核心流程,更多细节,比如Vite的插件机制是怎么样的,具体是怎么样做文件扫描的,都值得各位前端人探索!