本文基于vite 4.3.0-beta.1
版本的源码进行分析
文章内容
vite
本地服务器的创建流程分析vite
预构建流程分析vite
middlewares拦截请求资源分析vite
热更新HMR流程分析
1. 入口npm run dev
在项目的package.json
中注册对应的scripts
命令,当我们运行npm run dev
时,本质就是运行了vite
{
"scripts": {
"dev": "vite",
}
}
而vite
命令是在哪里注册的呢?
在node_modules/vite/package.json
中
{
"bin": {
"vite": "bin/vite.js"
}
}
在node_modules/vite/bin/vite.js
中
#!/usr/bin/env node
const profileIndex = process.argv.indexOf('--profile')
function start() {
return import('../dist/node/cli.js')
}
if (profileIndex > 0) {
//...
} else {
start()
}
最终调用的是打包后的dist/node/cli.js
文件
处理用户的输入后,调用./chunks/dep-f365bad6.js
的createServer()
方法,如下面所示,最终调用server.listen()
const { createServer } = await import('./chunks/dep-f365bad6.js').then(function (n) { return n.G; });
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
server: cleanOptions(options),
});
await server.listen();
createServer()
async function createServer(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve');
if (isDepsOptimizerEnabled(config, false)) {
// start optimizer in the background, we still need to await the setup
await initDepsOptimizer(config);
}
const { root, server: serverConfig } = config;
const httpsOptions = await resolveHttpsConfig(config.server.https);
const { middlewareMode } = serverConfig;
const resolvedWatchOptions = resolveChokidarOptions(config, {
disableGlobbing: true,
...serverConfig.watch,
});
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const ws = createWebSocketServer(httpServer, config, httpsOptions);
const watcher = chokidar.watch(
// config file dependencies and env file might be outside of root
[root, ...config.configFileDependencies, path$o.join(config.envDir, '.env*')], resolvedWatchOptions);
const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }));
const server = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
...
};
const initServer = async () => {
if (serverInited)
return;
if (initingServer)
return initingServer;
initingServer = (async function () {
await container.buildStart({});
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
if (!middlewareMode && httpServer) {
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port, ...args) => {
try {
await initServer();
}
catch (e) {
httpServer.emit('error', e);
return;
}
return listen(port, ...args);
});
}
else {
await initServer();
}
return server;
}
createServer()
源码太长,下面将分为多个小点进行分析,对于一些不是该点分析的代码将直接省略:
- 创建本地node服务器
- 预构建
- 请求资源拦截
- 热更新HMR
createServe思维导图
2. 创建本地node服务器
// 只保留本地node服务器的相关代码
async function createServer(inlineConfig = {}) {
// 创建http请求
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const server = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container
...,
async listen(port, isRestart) {
await startServer(server, port);
if (httpServer) {
server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
if (!isRestart && config.server.open)
server.openBrowser();
}
return server;
}
}
const initServer = async () => {
if (serverInited)
return;
if (initingServer)
return initingServer;
initingServer = (async function () {
await container.buildStart({});
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
//...
await initServer();
return server;
}
上面代码蕴含着多个知识点,我们下面将展开分析
2.1 connect()创建http服务器
Connect模块介绍
Connect是一个Node.js
的可扩展HTTP服务框架,用于将各种"middleware"
粘合在一起以处理请求
var app = connect();
app.use(function middleware1(req, res, next) {
// middleware 1
next();
});
app.use(function middleware2(req, res, next) {
// middleware 2
next();
});
var connect = require('connect');
var http = require('http');
var app = connect();
// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());
// respond to all requests
app.use(function(req, res){
res.end('Hello from Connect!\n');
});
//create node.js http server and listen on port
http.createServer(app).listen(3000);
源码分析
会先使用connect()
创建middlewares,然后将middlewares作为app属性名传入到resolveHttpServer()
中
最终也是使用Node.js
的Http
模块创建本地服务器
// 只保留本地node服务器的相关代码
async function createServer(inlineConfig = {}) {
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
//...
}
async function resolveHttpServer({ proxy }, app, httpsOptions) {
if (!httpsOptions) {
const { createServer } = await import('node:http');
return createServer(app);
}
// #484 fallback to http1 when proxy is needed.
if (proxy) {
const { createServer } = await import('node:https');
return createServer(httpsOptions, app);
}else {
const { createSecureServer } = await import('node:http2');
return createSecureServer({
// Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
// errors on large numbers of requests
maxSessionMemory: 1000,
...httpsOptions,
allowHTTP1: true,
},
// @ts-expect-error TODO: is this correct?
app);
}
}
2.2 启动http服务器
在dist/node/cli.js
文件的分析中,我们知道
在创建server
完成后,我们会调用server.listen()
// dist/node/cli.js
const { createServer } = await import('./chunks/dep-f365bad6.js').then(function (n) { return n.G; });
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
server: cleanOptions(options),
});
await server.listen();
而server.listen()
最终调用的也是Node.js
的Http
模块的监听方法,即上面Connect
模块介绍示例中的http.createServer(app).listen(3000)
async function createServer(inlineConfig = {}) {
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const server = {
httpServer,
//...
async listen(port, isRestart) {
await startServer(server, port);
if (httpServer) {
server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
if (!isRestart && config.server.open)
server.openBrowser();
}
return server;
}
};
}
async function startServer(server, inlinePort) {
const httpServer = server.httpServer;
//...
await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger,
});
}
async function httpServerStart(httpServer, serverOptions) {
let { port, strictPort, host, logger } = serverOptions;
return new Promise((resolve, reject) => {
httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError);
resolve(port);
});
});
}
3. 预构建
3.1 预构建的原因
CommonJS 和 UMD 兼容性
在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。
在转换 CommonJS 依赖项时,Vite 会进行智能导入分析,这样即使模块的导出是动态分配的(例如 React),具名导入(named imports)也能正常工作:
// 符合预期
import React, { useState } from 'react'
性能
为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。
有些包将它们的 ES 模块构建为许多单独的文件,彼此导入。例如,lodash-es
有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP 请求!即使服务器能够轻松处理它们,但大量请求会导致浏览器端的网络拥塞,使页面加载变得明显缓慢。
通过将 lodash-es
预构建成单个模块,现在我们只需要一个HTTP请求!
注意
依赖预构建仅适用于开发模式,并使用esbuild
将依赖项转换为 ES 模块。在生产构建中,将使用@rollup/plugin-commonjs
3.2 预构建整体流程(流程图)
接下来会根据流程图的核心流程进行源码分析
3.3 预构建整体流程(源码整体概述)
Vite 会将预构建的依赖缓存到 node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:
- 包管理器的 lockfile 内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
- 补丁文件夹的修改时间
- 可能在 vite.config.js 相关字段中配置过的
- NODE_ENV 中的值
只有在上述其中一项发生更改时,才需要重新运行预构建。
我们会先检测是否有预构建的缓存,如果没有缓存,则开始预构建:发现文件依赖并存放于deps
,然后将deps
打包到node_modules/.vite
中
async function createServer(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve');
if (isDepsOptimizerEnabled(config, false)) {
// start optimizer in the background, we still need to await the setup
await initDepsOptimizer(config);
}
//...
}
async function initDepsOptimizer(config, server) {
if (!getDepsOptimizer(config, ssr)) {
await createDepsOptimizer(config, server);
}
}
async function createDepsOptimizer(config, server) {
// 第一步:3.4获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
// 第二步:3.5没有缓存时进行依赖扫描
const deps = {};
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6没有缓存时进行依赖扫描,然后进行依赖打包到node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
3.4 获取缓存loadCachedDepOptimizationMetadata
async function createDepsOptimizer(config, server) {
// 第一步:3.4获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
if (!cachedMetadata) {
// 第二步:3.5没有缓存时进行依赖扫描
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6依赖扫描后进行打包runOptimizeDeps(),存储到node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
}
async function loadCachedDepOptimizationMetadata(config, ssr, force = config.optimizeDeps.force, asCommand = false) {
const depsCacheDir = getDepsCacheDir(config, ssr);
if (!force) {
// 3.4.1 获取_metadata.json文件数据
let cachedMetadata;
const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
// 3.4.2 比对hash值
if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {
return cachedMetadata;
}
}
// 3.4.3 清空缓存
await fsp.rm(depsCacheDir, { recursive: true, force: true });
}
3.4.1 获取_metadata.json文件数据
通过getDepsCacheDir()
获取node_modules/.vite/deps
的缓存目录,然后拼接_metadata.json
数据,读取文件并且进行简单的整理parseDepsOptimizerMetadata()
后形成校验缓存是否过期的数据
const depsCacheDir = getDepsCacheDir(config, ssr);
let cachedMetadata;
const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
下面metadata
的数据结构是在_metadata.json
数据结构的基础上叠加一些数据
function parseDepsOptimizerMetadata(jsonMetadata, depsCacheDir) {
const { hash, browserHash, optimized, chunks } = JSON.parse(jsonMetadata, (key, value) => {
if (key === 'file' || key === 'src') {
return normalizePath$3(path$o.resolve(depsCacheDir, value));
}
return value;
});
if (!chunks ||
Object.values(optimized).some((depInfo) => !depInfo.fileHash)) {
// outdated _metadata.json version, ignore
return;
}
const metadata = {
hash,
browserHash,
optimized: {},
discovered: {},
chunks: {},
depInfoList: [],
};
//...处理metadata
return metadata;
}
3.4.2 比对hash值
if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {
return cachedMetadata;
}
最终生成预构建缓存时,_metadata.json
中的hash
是如何计算的?是根据什么文件得到的hash值?
getDepHash()
的逻辑也不复杂,主要的流程为:
- 先进行
lockfileFormats[i]
文件是否存在的检测,比如存在yarn.lock
,那么就直接返回yarn.lock
,赋值给content
- 检测是否存在
patches
文件夹,进行content += stat.mtimeMs.toString()
- 将一些配置数据进行
JSON.stringify()
添加到content
的后面 - 最终使用
content
形成对应的hash
值,返回该hash
getDepHash()
的逻辑总结下来就是:
- 包管理器的锁文件内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
- 补丁文件夹的修改时间
- vite.config.js 中的相关字段
- NODE_ENV 的值
只有在上述其中一项发生更改时,
hash
才会发生变化,才需要重新运行预构建
const lockfileFormats = [
{ name: 'package-lock.json', checkPatches: true },
{ name: 'yarn.lock', checkPatches: true },
{ name: 'pnpm-lock.yaml', checkPatches: false },
{ name: 'bun.lockb', checkPatches: true },
];
const lockfileNames = lockfileFormats.map((l) => l.name);
function getDepHash(config, ssr) {
// 第一部分:获取配置文件初始化content
const lockfilePath = lookupFile(config.root, lockfileNames);
let content = lockfilePath ? fs$l.readFileSync(lockfilePath, 'utf-8') : '';
// 第二部分:检测是否存在patches文件夹,增加content的内容
if (lockfilePath) {
//...
const fullPath = path$o.join(path$o.dirname(lockfilePath), 'patches');
const stat = tryStatSync(fullPath);
if (stat?.isDirectory()) {
content += stat.mtimeMs.toString();
}
}
// 第三部分:将配置添加到content的后面
const optimizeDeps = getDepOptimizationConfig(config, ssr);
content += JSON.stringify({
mode: process.env.NODE_ENV || config.mode,
//...
});
return getHash(content);
}
function getHash(text) {
return createHash$2('sha256').update(text).digest('hex').substring(0, 8);
}
拿到getDepHash()
计算得到的hash
,跟目前node_modules/.vite/deps/_metadata.json
的hash
属性进行比对,如果一样说明预构建缓存没有任何改变,无需重新预构建,直接使用上次预构建缓存即可
下面是_metadata.json
的示例
{
"hash": "2b04a957",
"browserHash": "485313cf",
"optimized": {
"lodash-es": {
"src": "../../lodash-es/lodash.js",
"file": "lodash-es.js",
"fileHash": "d69f60c8",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "98c38b51",
"needsInterop": false
}
},
"chunks": {}
}
3.4.3 清空缓存
如果缓存过期或者带了force=true
参数,代表缓存不可用,使用fsp.rm
清空缓存文件夹
"dev": "vite --force"