记录: 这两天在支援一个老项目时,发现了一个线上问题,在这里记录一下解决方案。
场景:同一个系统的地址链接,用户用两个浏览器标签页打开,分别登录不同的有效用户,当用户从一个标签页切换到另一个标签页,继续某些操作时,会因操作用户不一致导致数据信息展示错误或接口报错等问题。
解决方案: 和组内成员及领导讨论后,决定使用visibilitychange
事件和focus
焦点事件来监听用户的行为,从而完成校验用户信息的动作。
PS:至于为啥不让后端在接口层面做拦截?
- 让后端所有接口都做拦截不现实。
- 如果两个浏览器标签页上登录的用户都是有效用户且权限一样,那么后端接口依旧拦不住。
下面说说代码实现逻辑:
-
在页面初始化的时候,先从
cookie
中拿到对应的用户信息,存储到sessionStorage
中。 -
使用
visibilitychange
事件监听用户切换浏览器标签页的行为,如果状态为用户进入页面,则从cookie
中获取用户信息与之前sessionStorage
中存储的信息是否一致。如果不一致就弹框提示让用户重新登录。 -
至于为啥还要用
focus
事件判断,因为我做的这个项目有部分页面是使用iframe嵌入到别的项目里面的,这种情况就会导致visibilitychange
事件失效,所以再用focus
事件做最后一层保险。focus
事件处理的逻辑与visibilitychange
事件内的逻辑一样。
在实现逻辑代码之前先来看看下面这两个问题。
关于visibilitychange
事件的兼容性问题
此事件是用来检测用户进入标签页和离开标签页的行为。
从caniuse
网站上查看此事件的兼容性,其占比已经达到99%以上,完全能覆盖到我这个项目的使用用户,所以可以放心使用。
使用示例:
js
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log('用户已进入', document.visibilityState);
} else {
console.log('用户已离开', document.visibilityState);
}
});
效果:
为什么使用focus
- 因为
focus
事件的优先级要比click
事件优先级高! - js 是单线程机制,一次只能执行一个事件。
当用户点击页面上某个按钮时,会先触发focus
事件,由于处理用户信息代码逻辑是同步的,所以在执行完判断逻辑后才会执行click
事件内的逻辑,如果用户信息不一致,会有弹框提示,阻断用户的下一步动作。
来看个代码示例:
js
window.addEventListener('click', () => {
console.log('click')
});
window.addEventListener('focus', () => {
console.log('focus')
});
控制台打印效果:
代码实现
了解了上面两个事件的一些特点之后,接下来进行编码环节。
js
// 获取sessionStorage中存储的用户信息
function getSessionCookie(id) {
return window.sessionStorage.getItem(id);
}
// 将用户信息存储到sessionStorage中
function setSessionCookie(id, data) {
window.sessionStorage.setItem(id, data);
}
// 从cookie中获取用户信息对应的字段
function getCookie(key) {
return decodeURIComponent(document.cookie.replace(new RegExp(`(?:(?:^|.*;)\\s*${encodeURIComponent(key).replace(/[-.+*]/g, '\\$&')}\\s*\\=\\s*([^;]*).*$)|^.*$`), '$1')) || null;
}
// 对比当前cookie中的信息和sessionStorage中存储的信息是否一致
function compareData(data, key) {
return getCookie(key) === JSON.parse(data);
}
// 检测用户信息
function checkUser(cb, key) {
var data = getSessionCookie('checkusersdk');
if (!data) {
var k = getCookie(key);
if (k) {
setSessionCookie('checkusersdk', JSON.stringify(k));
}
} else {
var flag = compareData(data, key);
cb && cb(flag);
}
}
function init(options) {
var cb = options.cb;
var key = options.key || 'checkuserKey';
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkUser(cb, key);
}
});
window.addEventListener('focus', function() {
checkUser(cb, key);
});
}
// result为回调函数。
init({ cb: result, key: 'custom'});
init
方法接收一个回调函数cb
和存储用户信息标识的key
,共两个参数。init
方法在监听时间内调用checkUser
方法,将两个参数传递进去,checkUser
方法内部逻辑如下:
- 获取存储在
sessionStorage
中的数据,如果数据为空,则从cookie
中获取用户信息数据,然后再存储到sessionStorage
中 - 如果
sessionStorage
中的数据存在,再次从cookie
中获取用户信息数据,二者进行比较,返回一个布尔值,传递给回调函数。
检测用户信息的核心代码已经完成了,不过为了更好的使用体验,将它封装成一个sdk,可以方便给别的项目使用。
使用webpack将代码封装一个SDK
1. 创建一个sdk项目
- 在自己的工作空间内创建一个空白文件夹,命名为
checkuser-sdk
,作为项目名称。 - 在上一步刚刚创建的项目根目录下再创建一个文件夹
src
,用来存放核心代码。 src
目录下创建一个index.js
文件,用来当做入口文件。
2. 初始化项目
- 在项目根目录下执行命令
npm init
,根据命令提示编写项目的一些详细信息,然后回车确定即可。
此时项目目录应该如下:
js
checkuser-sdk
| - src
| -- index.js
| - package.json
3. 编写配置文件webpack.config.js
-
执行下面命令安装一些必要的依赖包
jsnpm i webpack webpack-cli clean-webpack-plugin uglifyjs-webpack-plugin -D
-
在项目根目录下创建一个
webpack.config.js
文件 -
在
webpack.config.js
文件内编写如下代码:
js
let path = require('path');
// 把本地已有的打包后的资源清空,来减少它们对磁盘空间的占用
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 用来缩小(压缩优化)js文件
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
'sdk': ['./src/index.js']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
library: {
name: 'CheckuserSdk',
type: 'umd',
}
},
optimization: {
minimizer: [new UglifyJsPlugin({
sourceMap: false,
parallel: true,
cache: false
})],
},
plugins: [
new CleanWebpackPlugin()
]
}
项目比较简单,使用这两个插件已足够
4. 编写核心代码
- 在
src
目录下新建一个目录,命名为lib
。 - 在
lib
下创建一个util.js
文件,用来存储一些工具方法。 - 在
src
目录下的index.js
引入工具方法。
两个文件的代码分别如下:
js
// src/lib/util.js
// 获取sessionStorage中存储的用户信息
function getSessionCookie(id) {
return window.sessionStorage.getItem(id);
}
// 将用户信息存储到sessionStorage中
function setSessionCookie(id, data) {
window.sessionStorage.setItem(id, data);
}
// 从cookie中获取用户信息对应的字段
function getCookie(key) {
return decodeURIComponent(document.cookie.replace(new RegExp(`(?:(?:^|.*;)\\s*${encodeURIComponent(key).replace(/[-.+*]/g, '\\$&')}\\s*\\=\\s*([^;]*).*$)|^.*$`), '$1')) || null;
}
// 对比当前cookie中的信息和sessionStorage中存储的信息是否一致
function compareData(data, key) {
return getCookie(key) === JSON.parse(data);
}
// 检测用户信息
function checkUser(cb, key) {
var data = getSessionCookie('checkusersdk');
if (!data) {
var k = getCookie(key);
if (k) {
setSessionCookie('checkusersdk', JSON.stringify(k));
}
} else {
var flag = compareData(data, key);
cb && cb(flag);
}
}
module.exports = {
getSessionCookie,
setSessionCookie,
getCookie,
compareData,
checkUser
}
js
// src/index.js
var util = require('./lib/util');
function init(options) {
var cb = options.cb;
var key = options.key || 'e2mf';
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
util.checkUser(cb, key);
}
});
window.addEventListener('focus', function() {
util.checkUser(cb, key);
});
}
module.exports = {
init
}
5. package.json中添加脚本命令
js
...省略
"scripts": {
"build": "webpack"
},
...省略
执行命令npm run build
后,在项目的根目录下会创建一个dist
文件夹,文件夹里面的sdk.js
就是我们的打包文件了。
6. 使用方式
- 链接方式
js
// 引用 --->临时
<script type="text/javascript" src="http://xxx.com/sdk.js"></script>
// 使用
if (window.CheckuserSdk) {
window.CheckuserSdk.init({ cb: callback, key: 'customKey' });
}
- npm方式
js
// 安装
npm i -S @xxx/checkuser-sdk;
// 引入
import CheckuserSdk from '@xxx/checkuser-sdk';
// 使用
if (window.CheckuserSdk) {
window.CheckuserSdk.init({ cb: callback, key: 'customKey' });
}
文章内如有错误的地方,欢迎掘友留言纠正~!