背景介绍
monorepo 项目发版是轮询接口返回的 hash 值,如果 hash 值变化则通知发版,实际情况下,只是更新了 A 模块,用户仅使用 B 模块也会收到发版通知。
期望调整:前端每次模块打包生成 hash 值,当前模块 hash 值变化则提示发版。
注意事项
- hash 文件生成要在打包构建之后
- hash 文件存储路径要跟轮询时请求的路径一致
webpackConfig.output = {
...webpackConfig.output,
path: path.resolve(__dirname, "dist"), // 修改输出文件目录
// 注意线上资源访问路径
publicPath: process.env.NODE_ENV === "production" ? "/mfs/asset/" : "/",
library: `${name}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
globalObject: "window",
};
过程描述
线上代码资源请求路径在域名/mfs/模块名下,该路径下以 static/js,static/css 区分不同类型的静态资源
打包后生成 version.json 在 static 目录下
之后在模块顶层 layout.tsx 轮询 version.json,检测到变化则提示用户刷新
实现
自定义 Plugin:GenerateFileHashPlugin
const path = require("path");
const fs = require("fs");
class GenerateFileHashPlugin {
filePath;
constructor(filePath) {
this.filePath = filePath;
}
apply(compiler) {
// 文件生成时机在构建之后,否则缺少static目录会报错
compiler.hooks.afterEmit.tap("GenerateFileHashPlugin", compilation => {
if (process.env.NODE_ENV !== "production") return;
// 该路径同线上资源请求路径一致
const configPath = path.join(
compiler.options.output.path,
"static",
this.filePath
);
fs.writeFileSync(
configPath,
JSON.stringify({ hash: compilation.hash }, null, 2)
);
});
}
}
module.exports = GenerateFileHashPlugin;
载入 Plugin
const GenerateFileHashPlugin = require('./src/utils/generateFileHashPlugins');
...
module.exports = {
webpack:{
plugins:[new GenerateFileHashPlugin('version.json'),]
}
}
轮询 updater
interface Options {
timer?: number;
}
export class Updater {
oldHash: string; // 存储第一次值也就是script 的hash 信息
newHash: string; // 获取新的值 也就是新的script 的hash信息
dispatch: Record<string, Function[]>; // 小型发布订阅通知用户更新了
constructor(options?: Options) {
this.oldHash = "";
this.newHash = "";
this.dispatch = {};
this.init(); // 初始化
this.timing(options?.timer); // 轮询
}
async init() {
const html: string = await this.getHtml();
this.oldHash = this.parserScript(html);
}
async getHtml() {
if (process.env.NODE_ENV !== "production") return undefined;
const json = await fetch(
process.env.NODE_ENV !== "production"
? "/version.json"
: "/mfs/asset/static/version.json",
{
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
).then(res => {
return res.json();
}); // 读取index json
return json;
}
parserScript(json: any) {
return json?.hash || "";
}
// 发布订阅通知
on(key: "no-update" | "update", fn: Function) {
(this.dispatch[key] || (this.dispatch[key] = [])).push(fn);
return this;
}
compare(oldString: string, newString: string) {
// 如果新旧length 一样无更新
if (oldString === newString || (!oldString && !newString)) {
this.dispatch["no-update"]?.forEach(fn => {
fn();
});
} else {
// 否则通知更新
this.dispatch?.update?.forEach(fn => {
fn();
});
}
}
refresh() {
this.oldHash = this.newHash;
}
timing(time = 60000) {
console.log(time, "time");
// 轮询
setInterval(async () => {
const newHtml = await this.getHtml();
this.newHash = this.parserScript(newHtml);
this.compare(this.oldHash, this.newHash);
}, time);
}
}
通知提示
import { Updater } from 'src/utils/updater';
const updater = new Updater({ timer: 60000 });
updater.on('update', () => {
console.warn('更新');
setIsRefresh(true);
...
// 自定义提示方式
});
小结
做的事情很简单,发版记录,轮询比较,通知发版。
但是该功能几乎在每一个项目上都能够使用,无论是 monorepo,单模块,小程序等项目都能基于该思路实现纯前端的发版通知。
另外,看到一些文章里是直接比较构建后文件的 hash 指纹,后续可以再做调整。
感谢您的阅读,如果对文章内容有任何疑问或者建议,欢迎在掘金社区私信我