如果没有时间想直接解决问题,看最下面的最终代码即可
场景需求
总结:
20分钟内如果不操作,页面就是提示失效并且回到列表页面,如果操作了,计时就会清零。
如果 A 在编辑,B 点击编辑会提示正在编辑。A 在编辑期间,每分钟会向后端发送续租(即正在编辑)的请求,后端收到请求后,会在服务端帮你保留这一分钟的编辑状态,别人就无法在编辑了。并且别人编辑时,后端会返回相应的信息。
前言
乐了,产品提出了需求,然后我去找导师问问团队中有没有现成的解决方案。。。没有,然后导师提出了 web worker 的思路,让我自己思考解决方案。好吧,那就开始吧。
一开始,我想着能否能用 setInterval
来进行定时的,结果后端发来消息
emm......后端大佬,惹不起~
如图上所说,如果切换了页面,setInterval
会停止计时的(咱就说不信的可以试试),也就是说这个线程被停止了。
那么就需要新建一个线程,也就是 web worker 了,用它单纯来进行计时,不用管其他逻辑,切换页面也不会终止。
正文思路
基本demo
首先,百度了下 web worker 的基本实现案例,一文彻底学会使用web worker
需要该需求的页面
js
// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
</template>
<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名
// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('Greeting from Main.js');
myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息
console.log(e.data); // Greeting from Worker.js,worker线程发送的消息
});
</script>
放入 public 文件下 worker.js
js
// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
});
其中,worker.js 的存放路径和 new Worker()里的值有关,比如此时我是在本地资源的根路径创建的 /worker.js
,那么就是放在public下的。
而如果是 ./worker.js
,或者 ../worker.js
,这是无法找到的,因为此时的 worker.js 已经被打包编译成了 app.js。
注意,public 文件的变动需要重启项目,和 vue.config.js一样
worker.js 和 主线程通信走通后,开始分析需求了。
1. 每分钟续租一次 =》 1秒钟续租一次
什么叫续租,每分钟你向服务端发送一个续租请求,后端就会帮你保持正在编辑的状态(假设为 edit: true
),而且后端其实也在计时一分钟。在这一分钟内,由于 edit 为 true
,如果别人想要编辑,就会拒绝别人的编辑。如果你一分钟后没发送这个续租请求,后端会把 true
改成 false
,这时别人想要编辑,后端就会接受别人的编辑了。
因此,前端就需要每隔一分钟发送一次续租请求,来维持此时的编辑状态。
当然,由于产品要求的更复杂,你发送续租请求的时候请求头往往会携带用户信息,来反馈谁在进行编辑以提高用户体验感。
下述代码为了更好的测试,把每分钟续租变为了每秒续租一次
2. 20分钟期间不操作就会提示页面失效 =》 10秒钟一到就会触发提示事件
当然,就算 setInterval
不能作为解决方案,但还是需要用它来做定时器的,这还是挺香的。
js
// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
setInterval(() => {
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
}, 1 * 1000)
});
如上代码,Greeting from Worker.js
这条消息每隔 1 秒钟就会向 editEmail.vue
页面发送,这时就算你切换浏览器标签页也仍然会发送。
好,简单的定时器做完了,那就开始进行计时了。
js
// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
let sum = 0;
let msg = {
text: 'editing',
sum
}
setInterval(() => {
sum += 1;
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
});
每过一秒,worker.js 都会发送一次信息,用来持续触发续租事件,而 sum 则是用来进行计时过了多少秒。
js
// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
</template>
<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名
// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');
myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
}
});
</script>
OK,这样,基本的需求就完成了,10
秒一到就会提示页面失效,并且在这 10 秒内谁都无法进入编辑页面(在进入编辑页面前得先向后端请求看看是否有人在编辑)。
但是,10 秒后呢,这个计时器仍然在进行中,所以我需要在 10 秒过后清除这个计时器了。也就是在 e.data.sum >= 10
这个条件内对 worker 进程进行通信,触发清除事件。
js
// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>
<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名
// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');
myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
myWorker.postMessage('end');
}
});
</script>
在这里我们分别向 worker 进程发送了 start
和 end
两个信息,worke r 进程拿到信息后进行判断,如果是 start
,那么就开始每秒续租,如果为 end
,那么就清除定时器来终止续租(即停止每秒向主线程进行通信来触发续租请求)。
js
// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
let sum = 0;
let msg = {
text: '编辑中',
sum
}
if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
} else {
clearInterval(timer);
}
});
如上代码,定义一个全局变量 timer
用来存储定时器,以便能够随时清除。
定时重置
Stop,别冲太猛,这里我们需要总结一下了
开启定时
js
myWorker.postMessage('start');
就会重新 worker.js 中的 self.addEventListener('message',()=>{})
函数,sum 重置为 0,计时重新开始计算。
停止定时
js
myWorker.postMessage('end');
就会触发 worker 中的 clearInterval(timer)
来清除定时器
重置定时
js
myWorker.postMessage('end');
myWorker.postMessage('start');
先清除定时器停止定时,然后再重新开启定时
最后
js
// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}
3. 10 秒内如果进行了表单操作则重置计时
js
const onChange = () => {
onTime();
}
优化代码
js
// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>
<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名
// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');
// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}
myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
onTimeEnd(); // 停止计时,终止续租
}
});
const onChange = () => {
onTime();
}
</script>
js
// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
let sum = 0;
let msg = {
text: 'editing',
sum
}
if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
} else {
clearInterval(timer);
}
});
而到这里,只是实现了单纯的停留在页面,但切换浏览器标签页时,没有做相应的监听事件。虽然有着另一个 worker
线程在运行着,但当你切换页面后过 10s
再返回原页面,提示虽然会有,但是一闪即逝,基本看不到提示信息。
4. 切换浏览器标签页
而监听浏览器标签页的切换事件是 visibilitychange
和 document.visibilityStat
属性
javascript
document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible") {
message.error("页面已失效");
} else if (document.visibilityState == "hidden") {
message.error("页面已隐藏");
}
});
其中的隐藏我们并不需要用到,而且过了 10s 后如果反复的切换标签,"页面已失效"的提示会反复的弹出,因为我们并没有进行控制。
此时我们也需要区分过了 10s 后用户是停留在当前页面还是离开了页面又返回了。
如果是停留,那么页面属性为 visible
。如果是返回,那么就需要监听 visibilitychange
事件并且页面属性为 visible
。
js
let timeCount = 0; // 全局中定义变量,用以控制切换标签页后的提示次数。
myWorker.addEventListener("message", (e) => {
if (e.data.notime >= 10) {
onTimingEnd();
if (document.visibilityState === "visible") {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible" && timeCount == 0) {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
});
}
})
最终代码
替换成20分钟了
js
// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>
<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名
// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');
// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}
myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 20) { // 超过 20 分钟,终止续租并提示页面失效
onTimingEnd();
if (document.visibilityState === "visible") {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible" && timeCount == 0) {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
});
}
});
const onChange = () => {
onTime();
}
</script>
js
// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
let sum = 0;
let msg = {
text: 'editing',
sum
}
if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 60 * 1000) // 每分钟 sum 加 1 标识积累了 1 分钟
} else {
clearInterval(timer);
}
});