认识
一、认识
对于 Vite
而言, 代码其实分为两部分,一部分是源代码,也就是业务代码,另一部分是第三方依赖的代码,即 node_modules
中的代码。所谓的 no-bundle
只是对于源代码而言,对于第三方依赖而言,Vite
还是选择 bundle
(打包),并且使用速度极快的打包器 Esbuild
来完成这一过程,达到秒级的依赖编译速度。
Vite
在开发阶段, Vite
项目的启动可以分为两步。第一步是依赖预构建,借助 Esbuild
超快的编译速度来做第三方库构建和 TS/JSX
语法编译,对于第三方依赖,需要在应用启动前进行打包并且转换为 ESM
格式。 第二步是 Dev Server
的启动, 基于浏览器原生 ESModule
的支持实现了 no-bundle
服务,实现开发阶段的 Dev Server
, 进行模块的按需加载, 可以直接在浏览器中运行源码,无需事先打包。首次启动 Vite
时, Vite
在本地加载你的站点之前预构建了项目依赖。默认情况下,它是自动且透明地完成的。Vite
做依赖预构建, 主要有以下两种原因:
-
兼容
CommonJS
和UMD
, 将其他格式(如UMD
和CommonJS
)的产物转换为ESM
格式: 在开发阶段中,Vite
的开发服务器将所有代码视为原生ES
模块。因此,Vite
必须先将以CommonJS
或UMD
形式提供的依赖项转换为ES
模块。 -
打包第三 方库的代码, 解决请求瀑布流问题: 将各个第三方库分散的文件合并到一起,减少
HTTP
请求数量,避免页面加载性能劣化。每个import
都会触发一次新的文件请求,因此在这种依赖层级深、涉及模块数量多的情况下,会触发成百上千个网络请求,巨大的请求量加上Chrome
对同一个域名下只能同时支持6
个HTTP
并发请求的限制,导致页面加载十分缓慢,与Vite
主导性能优势的初衷背道而驰。不过,在进行依赖的预构建之后,lodash-es
这个库的代码被打包成了一个文件,这样请求的数量会骤然减少,页面加载也快了许多。
Vite
预构建 使用 EsBuild
编译执行, 通常速度非常快
二、工作
-
缓存判断
-
依赖扫描
-
依赖打包
-
元信息写入磁盘
三、缓存判断
首先是预构建缓存的判断。Vite
在每次预构建之后都将一些关键信息写入到了 _metadata.json
文件中,第二次启动项目时会通过这个文件中的 hash
值来进行缓存的判断,如果命中缓存则不会进行后续的预构建流程,代码如下所示:
// _metadata.json 文件所在的路径
const dataPath = path.join(cacheDir, "_metadata.json");
// 根据当前的配置计算出哈希值
const mainHash = getDepHash(root, config);
const data: DepOptimizationMetadata = {
hash: mainHash,
browserHash: mainHash,
optimized: {},
};
// 默认走到里面的逻辑
if (!force) {
let prevData: DepOptimizationMetadata | undefined;
try {
// 读取元数据
prevData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
} catch (e) {}
// 当前计算出的哈希值与 _metadata.json 中记录的哈希值一致,表示命中缓存,不用预构建
if (prevData && prevData.hash === data.hash) {
log("Hash is consistent. Skipping. Use --force to override.");
return prevData;
}
}
值得注意的是哈希计算的策略,即决定哪些配置和文件有可能影响预构建的结果,然后根据这些信息来生成哈希值。这部分逻辑集中在 getHash
函数中,我把关键信息放到了注释中:
const lockfileFormats = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"];
function getDepHash(root: string, config: ResolvedConfig): string {
// 获取 lock 文件内容
let content = lookupFile(root, lockfileFormats) || "";
// 除了 lock 文件外,还需要考虑下面的一些配置信息
content += JSON.stringify(
{
// 开发/生产环境
mode: config.mode,
// 项目根路径
root: config.root,
// 路径解析配置
resolve: config.resolve,
// 自定义资源类型
assetsInclude: config.assetsInclude,
// 插件
plugins: config.plugins.map((p) => p.name),
// 预构建配置
optimizeDeps: {
include: config.optimizeDeps?.include,
exclude: config.optimizeDeps?.exclude,
},
},
// 特殊处理函数和正则类型
(_, value) => {
if (typeof value === "function" || value instanceof RegExp) {
return value.toString();
}
return value;
}
);
// 最后调用 crypto 库中的 createHash 方法生成哈希
return createHash("sha256").update(content).digest("hex").substring(0, 8);
}
四、依赖扫描
如果没有命中缓存,则会正式地进入依赖预构建阶段。不过 Vite
不会直接进行依赖的预构建,而是在之前探测一下项目中存在哪些依赖,收集依赖列表,也就是进行依赖扫描的过程。这个过程是必须的,因为 Esbuild
需要知道我们到底要打包哪些第三方依赖。关键代码如下:
({ deps, missing } = await scanImports(config));
在 scanImports
方法内部主要会调用 Esbuild
提供的 build
方法:
const deps: Record<string, string> = {};
// 扫描用到的 Esbuild 插件
const plugin = esbuildScanPlugin(config, container, deps, missing, entries);
await Promise.all(
// 应用项目入口
entries.map((entry) =>
build({
absWorkingDir: process.cwd(),
// 注意这个参数
write: false,
entryPoints: [entry],
bundle: true,
format: "esm",
logLevel: "error",
plugins: [...plugins, plugin],
...esbuildOptions,
})
)
);