企业实战:20分钟页面不操作,页面失效

如果没有时间想直接解决问题,看最下面的最终代码即可

场景需求

总结:

  1. 20分钟内如果不操作,页面就是提示失效并且回到列表页面,如果操作了,计时就会清零。

  2. 如果 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 进程发送了 startend 两个信息,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. 切换浏览器标签页

而监听浏览器标签页的切换事件是 visibilitychangedocument.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);
    }
});
相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪8 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试