问题描述:
vite项目中,尽管每次只是部分文件更新,但构建之后,由于组件依赖关系,只要其中一个文件内容发生变化并且文件使用了[hash] 配置(相当于内容hash),其他文件的导入路径都会更新,导致内容变化,文件指纹也会变化。
解决思路:
vite 仓库相关 issue 和讨论 github.com/vitejs/vite…
主要是入口文件(entryFileNames)的路径名变化,影响绝大部分模块。
所以想要实现增量更新,这个入口文件名最好固定。
- 固定入口文件会导致浏览器缓存,整个应用都不再更新。
- 虽然可以在加载入口文件时加一个 query 时间戳。但是动态的组件中,对这个入口文件引用是不带query的,控制台会报错,页面会白屏。
- 要解决上面问题,就需要所有引用入口文件的地方,全部带上时间戳。可以 借助 importmap 进行映射。实现一个 plugin,自动在入口文件后面拼接时间戳,同时在 head 头中注入 importmap。
- 构建完成时,终端输出变化的文件列表。(相对于本地上次构建缓存中的记录文件),适用于开发者在本地 build 上传 dist 包的场景。如果想在 jenkins 等线上构建时,可以将记录上次构建结果的 json 从
.cache文件中拿出来。
entryFileNames: "assets/[name].isEntry.js"
chunkFileNames: "assets/[name].[hash].js"
import type { Plugin } from "vite";
import fs from "fs";
import path from "path";
export default function viteImportMapPlugin(): Plugin {
// 缓存文件路径(放在 node_modules/.cache 下)
const cacheDir = path.resolve("node_modules/.cache");
const recordFile = path.join(cacheDir, "vite-build-files.json");
return {
name: "html-entry-version-with-importmap",
transformIndexHtml(html) {
const version = Date.now();
// 1. 给入口文件加 query,入口文件在 entryFileNames 处配置,使用 isEntry.js 作为标识
const updatedHtml = html.replace(
/(<script[^>]+src=")([^"]*isEntry)(.js)"/,
`$1$2.js?v=${version}"`
);
// 2. 在 HTML 中插入 importmap
// 提取 isEntry.js 路径(不带 query)
const match = html.match(/<script[^>]+src="([^"]*isEntry).js"/);
if (!match) return updatedHtml;
const entryPath = match[1] + ".js";
const entryWithVersion = `${entryPath}?v=${version}`;
const importmap = `\n <script type="importmap">\n {\n "imports": {\n "${entryPath}": "${entryWithVersion}"\n }\n }\n </script>\n`;
// 3. 把 importmap 插入到 </head> 前
return updatedHtml.replace("</head>", `${importmap}\n</head>`);
},
// 构建完成后,输出新增文件列表
closeBundle() {
const distPath = path.resolve("dist");
const currentFiles: string[] = [];
// 递归获取 dist 中的所有文件名(相对路径)
function walkDir(dir: string) {
for (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else {
currentFiles.push(path.relative(distPath, fullPath));
}
}
}
if (fs.existsSync(distPath)) {
walkDir(distPath);
}
// 确保缓存目录存在
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// 读取上一次的文件列表
let previousFiles: string[] = [];
if (fs.existsSync(recordFile)) {
try {
previousFiles = JSON.parse(fs.readFileSync(recordFile, "utf-8"));
} catch {
previousFiles = [];
}
}
// 对比文件名,找出新增的
const newFiles = currentFiles.filter((f) => !previousFiles.includes(f));
// 颜色工具
const green = (text: string) => `\x1b[32m${text}\x1b[0m`; // 绿色
const yellow = (text: string) => `\x1b[33m${text}\x1b[0m`; // 黄色
const cyan = (text: string) => `\x1b[36m${text}\x1b[0m`; // 青色
// 输出数量
console.log(
`${green("-----此次构建新增了")} ${yellow(
String(newFiles.length)
)} ${green("个文件-----")}`
);
// 输出文件列表(如果有新增)
if (newFiles.length > 0) {
console.log(green("新增文件列表:"));
newFiles.forEach((f) => {
console.log(` ${cyan(f)}`);
});
}
// 保存当前文件列表
fs.writeFileSync(recordFile, JSON.stringify(currentFiles, null, 2));
},
};
}
plugins: [
command === 'build' ? viteImportMapPlugin() : null, // 仅在构建时启用
]
需关注的问题:
- importmap 兼容性目前还可以,但如果浏览器不支持,需要使用 es-module-shims 进行 polyfill。并且这个文件需要写在头部,使用
async属性,因为 importmap 是静态映射表,加载完成后,无法热更新。
<script async src="/es-module-shims.min.js"></script>
- 不适合多入口文件(应该可搞),不支持 SSR等。
- 如果修改公共文件,如
common.css中的全局变量,仍然会导致大量文件变化。 - 观察一段时间看看是否有副作用。
其他优化
- 利用
manualChunks进行三方包分割