前言
Hello~大家好。我是秋天的一阵风
在一个阳光明媚的午后,我的同事 前端英雄小帅 正沉浸在代码的世界里,突然,他的钉钉响起了急促的铃声——是 测试女神小美 发来的紧急bug修复请求。小帅没有丝毫犹豫就开始了bug修复的征程。提交、部署、更新状态,一气呵成,仿佛在跳一支优雅的代码华尔兹。终于,小帅伸了个大大的懒腰,准备享受片刻的宁静。
然而,宁静被打破了。小美不知怎的,又将bug状态改回了 “待修复”。
小帅瞪大了眼睛,心想:“我不是刚搞定了吗?我的开发环境里一切都完美无瑕啊!”
他立刻在微信群里发出了质疑:“小美,你刷新页面了吗?或者清一下浏览器缓存?”
小美回怼:“我怎么知道你刚部署了?你也没说啊,页面缓存又不是我的问题。”
小帅不甘示弱:“这不是常识吗?”
气氛一下子紧张起来,两人你来我往,仿佛一场没有硝烟的战争即将爆发,看得我目瞪口呆。
这时,作为曾荣获本市优秀市民奖的我,挺身而出,我说:“停!别争了,我来给你们写个插件。每次部署后,它会在页面上温馨提示用户:‘更新已完成,请刷新页面’。这样,是否刷新就交给用户自己决定吧。”
一、思路
-
在项目根目录创建一个
VersionFilePlugin.js
文件 -
webpack
中提供了一系列 hooks生命周期,在项目编译的时候我们创建一个version.jso
n文件 -
每次编译,我们都往
version.json
写入新的版本号,这里我为了方便直接使用了当前时间的时间戳。 -
在
App.vue
中我们通过fetch请求这个version.json
文件,取出版本号进行判断 -
如果新的版本号与旧的版本号不一样,则抛出提示,让用户决定是否需要刷新页面。
二、webpakc4 插件
1. 插件是什么
Webpack插件是一个具有apply
方法的类,该方法在编译过程中被Webpack编译器(compiler)
调用。
apply
方法接收一个编译器(compiler)
对象作为参数,你可以通过这个对象访问Webpack的钩子(hooks)
。
2. 插件的生命周期
在Webpack中,生命周期钩子(也称为事件钩子或插件钩子)是编译过程中的不同阶段,Webpack会在这些阶段触发事件,允许插件介入并执行自定义操作。这些钩子是Webpack插件系统的核心,使得开发者可以在Webpack构建流程的不同阶段插入自己的逻辑。
以下是Webpack中一些重要的生命周期钩子:
-
compile
- 这个钩子在每次开始新的编译时触发。它提供了一个
Compilation
对象,你可以用它来访问和修改编译过程中的各种信息。这个钩子常用于初始化任务,比如设置环境变量或者清理之前的构建结果。
- 这个钩子在每次开始新的编译时触发。它提供了一个
-
seal
seal
钩子在Compilation
对象被封闭(seal)之前触发,这意味着所有的模块和chunk都已经被处理,但是还没有开始生成最终的输出文件。这个钩子适合用于最后的优化和修改Compilation
对象,比如删除不必要的模块或者修改chunk的顺序。
-
afterEmit
afterEmit
钩子在emit阶段之后触发,即所有的文件已经被写入到输出目录。这个阶段是处理最终输出结果的好时机,比如生成额外的资源文件、记录构建信息或者执行清理工作。
-
done
done
钩子在编译完成时触发,无论是成功还是失败。这个钩子可以用来执行清理工作,比如关闭数据库连接,或者发送构建完成的通知。
这些钩子是Webpack插件系统中非常核心的部分,它们允许开发者在构建流程的关键点插入自定义逻辑,从而实现高度定制化的构建过程。
3. 关键对象:compilation
我们再重点了解一下afterEmit
钩子以及它的 compilation
对象。
在Webpack 4中,afterEmit
是一个非常重要的生命周期钩子,它在资源生成并输出到输出目录之后被触发。这个钩子允许开发者在Webpack的构建过程结束之后执行自定义逻辑,例如清理旧文件、记录构建信息或者执行其他后续处理。
(1) afterEmit
钩子的参数
afterEmit
钩子接收两个参数:
-
compilation
:这是一个包含了当前编译上下文详细信息的对象。它提供了对编译过程中生成的资源和模块的访问。通过compilation
对象,你可以获取构建过程中的各种数据,包括编译生成的资源(assets)、模块(modules)和警告(warnings)等。 -
callback
:这是一个必须被调用的回调函数,用于指示异步操作的完成。如果你的钩子处理逻辑是异步的,你需要在操作完成后调用这个回调函数。
Tips: 关于webpack4的生命周期hooks
你可以参考我的这篇文章:自定义Webpack插件:五步清除项目中的“僵尸”文件!
三、具体代码
1. vue.config.js
//...
const VersionFilePlugin = require('./VersionFilePlugin');
module.exports = {
// ...
configureWebpack: {
//...
plugins: [new VersionFilePlugin()],
},
}
2. VersionFilePlugin.js
class VersionFilePlugin {
apply(compiler) {
// Webpack编译过程 compiler.hooks.afterEmit异步钩子
compiler.hooks.afterEmit.tapAsync('versionPlugin', (compilation, callback) => {
const versionInfo = {
version: Date.now(),
buildTime: Date.now(),
};
const fileContent = JSON.stringify(versionInfo, null, 2);
// 将此列表作为新的文件资产插入到webpack生成中:
compilation.assets['version.json'] = {
source: () => {
return fileContent;
},
size: () => {
return fileContent.length;
},
};
callback();
});
}
}
module.exports = VersionFilePlugin;
3. App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {};
},
created() {
this.checkVersionUpdate();
},
methods: {
checkVersionUpdate() {
setInterval(() => {
fetch(`${window.location.origin}/version.json`)
.then((response) => response.json())
.then((data) => {
const currentVersion = localStorage.getItem('currentVersion');
if (currentVersion && currentVersion !== data.version.toString()) {
this.showMessageBox();
}
localStorage.setItem('currentVersion', data.version.toString());
});
}, 30000); // 每30秒检查一次
},
showMessageBox() {
this.$alert('系统更新,是否要刷新页面?', '系统提示', {
confirmButtonText: '需要',
cancelButtonText: '不需要',
showCancelButton: true,
type: 'info',
center: true,
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action === 'confirm') {
window.location.reload();
}
},
});
},
},
};
</script>
4. 效果展示
三、可优化的地方
上面的代码例子中,我们是采用了最简单的轮询做法定时去获取版本号,如果仅仅是在我们自己的开发环境使用这种做法那么确实问题不大,但是如果要真正投入到生产环境中肯定是有不小问题的。
因为轮询存在着资源浪费、延迟、可维护性不足等等不足之处。所以我提供几种优化方案供大家选择和参考
1. 使用 Web Workers
将轮询逻辑放入 Web Workers
中运行,避免阻塞主线程。
// **`versionWorker.js`**
self.addEventListener('message', (event) => {
const { currentVersion, delay } = event.data;
const checkForUpdate = () => {
fetch(`${window.location.origin}/version.json`)
.then((response) => response.json())
.then((data) => {
if (currentVersion && currentVersion !== data.version.toString()) {
// 发送消息到主线程
self.postMessage({ action: 'updateAvailable', version: data.version });
}
// 设置下一次检查
setTimeout(checkForUpdate, delay);
})
.catch((error) => {
console.error('Error checking for update:', error);
// 重试
setTimeout(checkForUpdate, delay);
});
};
// 启动轮询
checkForUpdate();
});
// App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {};
},
created() {
this.initVersionWorker();
},
methods: {
initVersionWorker() {
// 检查浏览器是否支持 Web Workers
if (window.Worker) {
const worker = new Worker('versionWorker.js'); // 确保路径正确
const currentVersion = localStorage.getItem('currentVersion');
const delay = 30000; // 每30秒检查一次
// 向 Web Worker 发送初始数据
worker.postMessage({ currentVersion, delay });
// 监听 Web Worker 发送的消息
worker.onmessage = (event) => {
if (event.data.action === 'updateAvailable') {
this.showMessageBox();
localStorage.setItem('currentVersion', event.data.version.toString());
}
};
} else {
console.error('Web Workers are not supported in this browser.');
}
},
showMessageBox() {
this.$alert('系统更新,是否要刷新页面?', '系统提示', {
confirmButtonText: '需要',
cancelButtonText: '不需要',
showCancelButton: true,
type: 'info',
center: true,
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action === 'confirm') {
window.location.reload();
}
},
});
},
},
};
</script>
2. 根据页面活跃状态动态调整轮询
当用户离开页面时暂停轮询,回到页面时恢复轮询
let pollingInterval = null;
const startPolling = () => {
pollingInterval = setInterval(() => {
// 检测逻辑
}, 600000); // 每10分钟检查一次
};
const stopPolling = () => {
clearInterval(pollingInterval);
pollingInterval = null;
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
stopPolling();
} else {
startPolling();
}
});
startPolling();
3. 使用 WebSocket 或 Server-Sent Events
如果服务器支持,可以使用 WebSocket 或 SSE 实现实时推送,服务器在版本更新时主动通知客户端
const eventSource = new EventSource('/updates');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
const currentVersion = localStorage.getItem('currentVersion');
if (currentVersion && currentVersion !== data.version.toString()) {
this.showMessageBox();
}
localStorage.setItem('currentVersion', data.version.toString());
};
Tips: 关于websocket和Server-Sent Events的介绍与对比,你可以参考我这篇文章: WebSocket太笨重?试试SSE的轻量级魅力!