背景
对于前端部署如何进行项目部署这里就不详细介绍了,这里主要介绍如何进行前端部署通知用户更新网站。
之前开发的网站, 每次都是让用户手动进行刷新网页打到网站更新的目的,但是这里有一个弊端,就是用户每次都需要手动,对我来讲,我比较懒,能自动的绝不手动刷新;
简单介绍
场景 1
开发完一个功能, 提交到 github 上, 然后通过jenkins
去部署发布, 之后你会发现页面还是之前的页面,因为你没有刷新网站,所以需要手动刷新一下网站。对于用户来讲能不能自动更新呢?
场景 2
用户在填写表单的时候,你项目网站更新了,需要一个提示,可以立即更新也可以忽略稍后更新, 这样就可以告知用户网站更新了那些功能? 更新的内容会不会影响你正在操作的功能?
场景 3
如果没有提示框,直接刷新页面导致用户正在操作的数据丢失, 这样的损失还是比较大的;
于是我今天研究了一下这个网站更新的方案, 然后通过本地nginx
模拟服务器去尝试了一下;
实现的方案
文章使用的是vite5 + vue3 + ts
的一个本地项目;
- 使用第三方插件vite-plugin-pwa, 具体的做法可以参考官网
- 手动是实现
manifest.json
+ 循环web worker
实现, 就是打包的时候创建一个manifest.json
然后将打包时间Date.now()
写入文件, 之后在根据里面的时间去做对应的逻辑 - 在
vite.config.ts
中使用build
, 开启manifest
,这样每次打包都会在dist/.vite
中生成一个新的文件, 这个文件就是manifest.json
这里我们具体讲vite.config.js
+ web worker
实现思路:
配置 vite.config.ts
vite.config.ts
如何配置呢? vite 后端集成
export default defineConfig(() => {
return {
plugins: [vue(), eslintPlugin()],
// 添加下面的build
build: {
manifest: true,
cssCodeSplit: false,
},
};
});
然后你可以通过npm run build
命令打包项目,等待项目打包成功之后, 项目根目录生成dist
目录, 大概的结构为:
.vite
assets
index.html
vite.svg
.vite
目录就是我们打包的manifest.json
文件, 这个文件就是我们后面需要读取的; manifest.json
中file
就是index.html
中引入js,css
的地址; index.html
中引入js,css
的地址为:
<!-- 大概这样的格式 -->
<script type="module" src="/assets/index.c0d0d0e9.js"></script>
<link rel="modulepreload" href="/assets/index.c0d0d0e9.css" />
这里manifest.json
文件可以理解为离线缓存文件, 就是吧所有的资源都缓存起来;
实现思路一
想要做到网站更新, 这里要获取manifest.json
文件,不过现在文件是在dist/.vite
中, 想要直接读取那就要修改打包命令
"scripts": {
"build": "vite build && node src/copyManifest.js",
}
copyManifest.js
的文件为就是复制一份manifest.json
到dist
目录下
import { copyFileSync } from "fs";
try {
copyFileSync("dist/.vite/manifest.json", "dist/manifest.json");
} catch (e) {
console.log(e);
process.exit(1);
}
然后在main.js
中引入一个文件,这个文件就是你更新网站的核心逻辑了;
大致的思路就是:
fetch('/manifest.json')
拿到最新的数据和上一次的数据去做比较或者结合发布订阅模式
- 循环获取, 因为网站不定时会更新,所以需要循环; 发现文件地址不一致直接弹框提示;
manifest.json
你也可以手动创建, 那就需要吧vite.config.ts
中manifest
关闭, 打包的时候创建一个文件manifest.json
内容为{time: 'xxxx', content: '更新内容'}
; 同样的循环操作去对比时间;
不过这样是比较麻烦的, 所以我们使用另外一种web worker
来实现;
实现思路二 推荐使用
首先需要创建俩个文件checkUpdate.ts
和checkUpdate.worker.ts
, 一个是入口文件, 另一个是web worker
循环检测机制;
// checkUpdate.ts:
import { ElMessageBox } from "element-plus";
// ./checkUpdate.worker?worker这是vite的写法. 如果使用webpack 是需要下载对应的插件识别的
import CheckWorker from "./checkUpdate.worker?worker";
// 判断是否是开发环境, 如果是开发环境就不需要检测更新
if (import.meta.env.MODE !== "development") {
const worker = new CheckWorker();
// 更新弹框
async function judgeUpdate(lastEtag: string) {
ElMessageBox.confirm("检测到网站有新内容, 是否需要更新?", "更新提示", {
confirmButtonText: "立即更新",
cancelButtonText: "10s之后再通知我",
type: "warning",
})
.then(async () => {
// 更新操作
worker.postMessage({
type: "close",
tip: "关闭",
lastEtag,
});
location.reload();
})
.catch(() => {
// 稍后更新操作
// 告知子线程稍后更新
worker.postMessage({
type: "recheck",
tip: "10s之后再通知我",
lastEtag,
});
});
}
// 首次进入页面检测是否需要更新
worker.postMessage({
type: "check",
tip: "检测是否更新",
});
// 接收到worker更新的消息, 那就进行弹框提示
worker.onmessage = ({ data }) => {
// 主线程接收到要更新的通知,那就弹框提示
if (data.type === "hasUpdate") {
judgeUpdate(data.lastEtag);
}
};
}
// checkUpdate.worker.ts:
let lastEtag: string | null | undefined = undefined;
let hasUpdate = false;
let intervalId: number | null | undefined = null;
async function checkUpdate() {
// 这里可以读取`manifest.json`, 我在这里直接读取网站的页面
const res = await fetch(`${self.location.protocol}//${self.location.host}`, {
method: "HEAD",
cache: "no-cache",
});
// 这一行就是如何判断是否有更新
const etag = res.headers.get("etag") || res.headers.get("last-modified");
hasUpdate = lastEtag !== undefined && etag !== lastEtag;
if (hasUpdate) {
// 向主线程发送消息, 告诉他网站需要更新了;
self.postMessage({
type: "hasUpdate",
tip: "需要更新",
lastEtag,
});
}
lastEtag = etag;
}
// 监听worker发送的消息
self.addEventListener("message", ({ data }) => {
// 检测更新
if (data.type === "check") {
checkUpdate();
intervalId && clearInterval(intervalId);
intervalId = setInterval(checkUpdate, 10 * 1000);
} else if (data.type === "recheck") {
// 稍后通知
hasUpdate = false;
lastEtag = data.lastEtag;
intervalId && clearInterval(intervalId);
intervalId = setInterval(checkUpdate, 10 * 1000);
} else if (data.type === "close") {
// 关闭
intervalId && clearInterval(intervalId);
close();
}
});
然后在main.ts
中引入import './checkUpdate'
; 基本上就可以了;
然后重新打包, 讲打包出来的文件放在本地服务器上;
然后打开页面: 等待时间就会有提示了:
知识补充
TIP
使用 Web Worker 来实现网站更新,尤其是处理与页面渲染无关但可能耗时的操作,如数据解析、复杂计算、大文件处理等,可以带来以下好处:
好处
- 防止主线程阻塞:
- Web Worker 在单独的后台线程中运行,与 UI 渲染线程互不干扰。这意味着即使在处理复杂的更新逻辑时,用户仍能流畅地与页面交互,避免了因长时间计算导致的页面冻结或卡顿现象,显著提升了用户体验。
- 提升响应速度:
- 对于涉及大量数据处理或计算密集型的更新任务,Web Worker 能够并行处理这些工作,使得更新过程更快完成,使得用户能更早看到更新后的结果。
- 分离任务,优化代码结构:
- 使用 Web Worker 可以将复杂的更新逻辑封装到独立的脚本中,与主线程的 UI 逻辑清晰分离。这有助于代码组织,提高可维护性,并允许开发者专注于各自线程的任务,遵循“关注点分离”原则
- 异步处理:
- Web Worker 通过消息传递机制
(postMessage() 和 onmessage)
与主线程通信,实现了真正的异步操作。这使得更新任务可以在不影响主线程的情况下异步进行,符合现代 Web 应用的非阻塞、事件驱动编程模式。
- Web Worker 通过消息传递机制
- 资源加载与预处理:
- 如果网站更新涉及到资源的加载或预处理(如压缩、解密等),Web Worker 可以在后台线程提前完成这些工作,待准备好后再通知主线程,避免了资源加载对主线程的直接压力。
弊端
兼容性问题:
- 虽然 Web Worker 是 HTML5 标准的一部分,但在较老的浏览器或某些非标准环境中可能存在兼容性问题。开发者需要进行兼容性检测和降级处理,或者为不支持 Web Worker 的环境提供备用方案。
额外的内存消耗:
- 启动 Web Worker 意味着创建一个新的线程,会增加浏览器的内存开销。对于资源受限的设备(如低端移动设备),过多或不当使用的 Web Worker 可能导致内存溢出或其他性能问题。
通信成本:
- 主线程与 Web Worker 之间的通信需通过序列化和反序列化数据,这会带来一定的性能开销。如果更新过程中频繁进行大量数据交换,可能会抵消部分多线程带来的优势。
限制访问某些对象与 API:
- Web Worker 环境中无法直接访问
window、document等与DOM
相关的全局对象,也无法直接操作 DOM。对于依赖这些功能的更新逻辑,需要通过消息传递将相关操作委托给主线程执行。
- Web Worker 环境中无法直接访问
调试复杂性:
- 由于 Web Worker 运行在独立线程中,调试起来可能比主线程代码更为复杂。开发者需要使用专门的工具或技巧来监控和调试 Worker 中的代码。
总结来说,使用 Web Worker 实现网站更新有利于提升用户体验、优化性能和代码结构,但同时也需要注意其可能带来的兼容性问题、额外资源消耗、通信成本、API 访问限制及调试复杂性。在实际应用中,应根据具体场景权衡利弊,合理利用 Web Worker 以实现高效、流畅的网站更新流程。