vite主张的是no bundle, 但是在开发环境,对于第三方依赖会基于esbuild进行依赖预构建,为什么选择esbuild,根据测试,发现esbuild构建速度对比rollup webpack非常快,开发环境注重效率。
依赖为什么需要预构建
性能方面考虑
- 如果依赖不构建,那么加载依赖的时候,比如lodash,有数百个模块,加载多次,影响性能,所以将依赖构建后,统一一个出口透出。
统一成ES Module
- 有些第三方包对外透出的是CommonJS和UMD等格式,需要将其转换成ESM格式,然后才能在浏览器中正常加载
vite预构建产物
构建产物会缓存在node_modules/.vite目录下
依赖预构建源码流程解析
主流程如下,主要实现在createDepsOptimizer()中
第一步:缓存判断
- 主要根据metadata中的lock文件的hash值和lock文件的生成的hash比对是否一致, metadata中的configHash值和config内容生成的hash比对是否一致;主要判断optimizeDeps上的配置,是否有变更;
js
const lockfileFormats = [
{ name: "package-lock.json", checkPatches: true, manager: "npm" },
{ name: "yarn.lock", checkPatches: true, manager: "yarn" },
// Included in lockfile for v2+
{ name: "pnpm-lock.yaml", checkPatches: false, manager: "pnpm" },
// Included in lockfile
{ name: "bun.lockb", checkPatches: true, manager: "bun" }
].sort((_, { manager }) => {
return process.env.npm_config_user_agent?.startsWith(manager) ? 1 : -1;
});
js
optimizeDeps: {
include: optimizeDeps2?.include ? unique(optimizeDeps2.include).sort() : void 0,
exclude: optimizeDeps2?.exclude ? unique(optimizeDeps2.exclude).sort() : void 0,
esbuildOptions: {
...optimizeDeps2?.esbuildOptions,
plugins: optimizeDeps2?.esbuildOptions?.plugins?.map((p) => p.name)
}
}
lua
if (cachedMetadata) {
if (cachedMetadata.lockfileHash !== getLockfileHash(config)) {
config.logger.info(
"Re-optimizing dependencies because lockfile has changed"
);
} else if (cachedMetadata.configHash !== getConfigHash(config, ssr)) {
config.logger.info(
"Re-optimizing dependencies because vite config has changed"
);
} else {
log?.("Hash is consistent. Skipping. Use --force to override.");
return cachedMetadata;
}
}
第二步:发现依赖&依赖收集
扫描入口判断
判断是否有指定的optimizeDeps的entries和input等配置,找到依赖扫描的入口,如果没有入口就指定根路径的html文件为入口;
js
async function computeEntries(config) {
let entries = [];
const explicitEntryPatterns = config.optimizeDeps.entries;
const buildInput = config.build.rollupOptions?.input;
if (explicitEntryPatterns) {
entries = await globEntries(explicitEntryPatterns, config);
} else if (buildInput) {
const resolvePath = (p) => path$n.resolve(config.root, p);
if (typeof buildInput === "string") {
entries = [resolvePath(buildInput)];
} else if (Array.isArray(buildInput)) {
entries = buildInput.map(resolvePath);
} else if (isObject$1(buildInput)) {
entries = Object.values(buildInput).map(resolvePath);
} else {
throw new Error("invalid rollupOptions.input value.");
}
} else {
entries = await globEntries("**/*.html", config);
}
entries = entries.filter(
(entry) => isScannable(entry, config.optimizeDeps.extensions) && fs__default.existsSync(entry)
);
return entries;
}
准备扫描器
主要基于esbuild.context(), 生成依赖扫描器,并把自定义的插件esbuildScanPlugin传递进去;
生成依赖, 存储在deps
js
return context.rebuild().then(() => {
return {
// Ensure a fixed order so hashes are stable and improve logs
deps: orderedDependencies(deps),
missing
};
第三步:打包依赖
将上一步收集到的depsInfo依赖列表,传递给esbuild.context()来进行分析,然后调用rebuild()这一步依赖就生成了。
依赖路径重写
我们在源码中的写的Import, 最后被重写成esbuild预构建后的路径
实现原理
在createServer阶段,就注册了多个中间件,当浏览器请求main.ts时,会触发transformMiddleware
主要流程
vite:import-analysis插件
- 通过
es-module-lexer
提取出所有的import数据; - 通过上面依赖预构建生成的dep,可以获取到esbuild生成的路径,interopNamedImports()中替换源码中的路径生成新的code;