如何在Vue项目中添加接口监听遮罩


Posted in Vue.js onJanuary 25, 2021

一、业务背景

使用遮罩层来屏蔽用户的非正常操作,是前端经常使用的方式。但是在一些项目中,并没有对遮罩层进行统一管理,这就会造成如下的问题:
(1)所有的业务组件都要引入遮罩层组件,也就是每个.vue业务组件,都在template中引入了Mask组件。组件在项目的各个角落都存在,不利于管理,代码极度冗余。
(2)Mask组件都分散到业务的各个角落,所以控制是否显示遮罩层的变量也散在业务组件中。比如使用maskShow来控制是否展示遮罩层时,一个较为复杂的项目中会产生200+的maskShow变量。
(3)maskShow过多且融入在业务中,同时maskShow的变量往往写在接口的回调函数中,经常会出现忘记改变变量的情况,造成遮罩层该显示和不该显示的逻辑出错。
(4)项目经常是在本地调试,而真实运行却又在线上,(3)中的问题在本地经常无法验证出。因为这些问题经常是在线上网络环境较差的情况出现。如一个按钮按完之后,需要等接口返回才能再次点击,但是本地因为返回速度较快,如果忘记添加遮罩层也不会有什么问题。但如果是网络有问题的线上环境,就很容易出现,且该问题一旦出现,很难定位,大大影响工作效率。

二、问题分析

根据上述的背景,在实际项目中添加一个公共的遮罩层组件进行管理,就变的十分有意义。经过分析,具体需要解决如下问题:
(1)遮罩层出现和关闭的时机。
(2)Mask组件设计。
(3)该组件如何优雅的引入到项目中,不产生耦合。
(4)如何在已有的项目中,渐进式的更换原有的maskShow的方式,从而不造成大面积问题。
(5)细节问题

三、组件设计

1、遮罩层出现和关闭的时机

该问题根据不同业务需求决定,但是笔者认为,大部分遮罩的出现和关闭主要取决于接口的请求和返回,一个接口在请求pending状态下,显示遮罩层,所有接口返回则关闭遮罩。本文主要解决的是接口请求遮罩问题,使用ts进行编写,且并不会罗列所有细节。

2、Mask组件设计

Mask组件为一个class,将细节屏蔽在class内部。
(1)class内部最主要功能为添加和删除遮罩层,传输的当前请求接口的url。

class Mask {
 // 显示遮罩层
 appendMask(url: string): void{}

 // 删除遮罩层
 removeMaskl(url: string): void{}
}

(2)添加遮罩层函数,请求时调用该函数,传入当前接口url。函数内部维护一个监听对象,用以监听当前是否存在pending状态的请求。该对象的value为该接口pending状态的数量。通过假设遮罩视图组件已经挂载到了Vue原型链上,如果没有,则在组件上方引入即可。

// 监听对象数据类型定义
interface HTTPDictInterface {
 [index: string]: number;
}

appendMask(url: string): void{ 

 if(!this.monitorHTTPDict[url]){
 this.monitorHTTPDict[url] = 0;
 }
 this.monitorHTTPDict[url] += 1;

 // 如果存在监听接口,则显示遮罩层
 if(!this.mask && Object.keys(this.monitorHTTPDict).length){

 // 在body上添加遮罩层样式,$Mask为遮罩层样式组件
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

 document.body.appendChild(this.mask.$el);
 }
}

(3)删除遮罩层函数,每次请求结束之后都会调用该函数,当发现请求监听对象为空时,删除的遮罩层。如果没有pending状态的接口,删除该对接的key。该对象为空且有遮罩层的情况下,删除遮罩层。

removeMask(url: string): void{

 // 成功返回后
 if (this.monitorHTTPDict[monitorUrl]) {
 this.monitorHTTPDict[monitorUrl] -= 1;
 if (this.monitorHTTPDict[monitorUrl] <= 0) {
 delete this.monitorHTTPDict[monitorUrl];
 }
 }

 // hasMask用以检测页面是否存在遮罩层标签元素
 if (this.mask && this.hasMask() && !Object.keys(this.monitorHTTPDict).length) {
 document.body.removeChild(this.mask.$el);
 this.mask = null;
 }

 this.timer = null;
}

3、该组件如何优雅的引入到项目中,不产生耦合。

使用该组件,需要在所有的请求发起之前调用appendMask函数,所有的请求结束之后调用removeMask函数。这就有如下两种调用方式。
(1)使用axios等组件的回调,完成函数调用。但是这种做法并没有将Mask组件的代码独立于项目,它依赖于具体接口框架的API。

instance.interceptors.request.use((config) => {

 // 添加遮罩层
 mask.appendMask(config.url);

 return config;
});

(2)添加init函数,直接在原生XMLHttpRequest对象中注入回调。更改原生XMLHttpRequest函数,在事件'loadstart'和'loadend'中注入回调,需要注意的是,loadstart接收的传参中,并没有当前请求的url,所以还需要改写open函数,把open接收传参的url挂载到新的xhr对象上。慎用该方法。因为更改原生API的方式十分危险,在很多编码规范中是禁止的,如果所有人都对原生API进行改写,当同时引入这些框架会产生冲突,造成无法意料的后果。

// 通过传参来决定是否使用该方法

init(){
 if (this.autoMonitoring){
 this.initRequestMonitor();
 }
}

// 新的xmlhttprequest类型
interface NewXhrInterface extends XMLHttpRequest{
 requestUrl?: string
}

// 原生注入
initRequestMonitor(): void{

 let OldXHR = window.XMLHttpRequest;
 let maskClass: Mask = this;

 // @ts-ignore,编码规范不允许修改XMLHttpRequest
 window.XMLHttpRequest = function () {

 let realXHR: NewXhrInterface = new OldXHR();
 let oldOpen: Function = realXHR.open;

 realXHR.open = (...args: (string | boolean | undefined | null)[]): void => {

 realXHR.requestUrl = (args[1] as string);
 oldOpen.apply(realXHR, args);

 };

 realXHR.addEventListener(`loadstart`, () => {

 const requestUrl: string = (realXHR.requestUrl as string);

 const url: string = maskClass.cleanBaseUrl(requestUrl);

 // 开启遮罩
 maskClass.appendMask(url);
 });

 realXHR.addEventListener(`loadend`, () => {

 const responseURL: string = (realXHR as XMLHttpRequest).responseURL;
 const url: string = maskClass.cleanBaseUrl(responseURL);

 // 删除遮罩
 maskClass.removeMask(url);
 });

 return realXHR;
 };
}

(3)注入使用方式,直接调用init。这样改项目的所有请求都会经过Mask。

new Mask().init()

4、如何在已有的项目中,渐进式的更换原有的maskShow的方式,从而不造成大面积问题。

如果直接在全项目中使用,牵扯的面积就会变得很广,会大面积的产生问题,反而得不偿失。所以应该采取一种渐进更换的方式,做到平滑过渡。主要思路是通过配置页面和黑名单的方式,来决定哪些页面引入该组件,从而让每个组员自己修改,毕竟页面的负责人才是最了解当前页面业务的人。至于如何黑名单还是白名单,则由项目的具体业务决定。

// key需要监听的路由页面,value为一个数组,数组中填写的接口为黑名单,不需要监听的接口
const PAGE_ONE = `/home`;
const PAGE_TWO = `/login`;
const HTTO_ONE = `xxx`

export const maskUrlList = {
 [PAGE_ONE]: [HTTO_ONE],
 [PAGE_TWO]: [],
};

appendMask方法过滤黑名单和没有配置的页面。maskUrlList为控制的对象,先检查页面路由,之后检查是否存在黑名单。

appendMask(url: string): void{

 // 获取当前页面的path,获取页面路径,根据hash和history模式进行区分
 const monitorPath: string = this.getMonitorPath();

 // maskUrlList为配置项,先检查页面路由,之后检查是否存在黑名单
 if (this.maskUrlList[monitorPath]
 && !this.maskUrlList[monitorPath].includes(url)) {
 if (this.monitorHTTPDict[url] === undefined) {
 this.monitorHTTPDict[url] = 0;
 }
 this.monitorHTTPDict[monitorUrl] += 1;
 }

 // 添加遮罩层
 if (!this.mask && this.hasMonitorUrl()) {
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

 document.body.appendChild(this.mask.$el);
 }
}

5、细节问题

(1)渲染之后才关闭遮罩层,将实际删除遮罩层逻辑放到定时器中,Vue的异步渲染采用的promise,所以关闭在如果放在渲染之后,需要放入setTimeout中。这里涉及到事件循环的知识。当接口返回,如果需要渲染页面,则会异步执行一个Promise,Promise为微任务,setTimeout为宏任务,当主线程执行完毕后,会先执行微任务,之后才会执行异步的宏任务setTimeout。

// 清理遮罩层
if (!this.timer) {
 this.timer = window.setTimeout(() => {

 if (this.mask && this.hasMask() && !this.hasMonitorUrl()) {
 document.body.removeChild(this.mask.$el);
 this.mask = null;
 }

 this.timer = null;

 }, 0);
}

(2)过滤接口的‘?',以及hash模式下的‘#',

// 获取请求接口的url
getMonitorUrl(url: string): string{
 const urlIndex: number = url.indexOf(`?`);
 let monitorUrl: string = url;
 if (urlIndex !== -1) {
 monitorUrl = url.substring(0, urlIndex);
 }
 return monitorUrl;
}
// 获取当前路由path
getMonitorPath(): string{

 const path: string = this.mode === HASH_TYPE ? window.location.hash : window.location.pathname;

 let monitorPath: string = path;

 if (this.mode === HASH_TYPE) {
 monitorPath = monitorPath.substring(path.indexOf(`#`) + 1);
 }

 // 截图路径,删除请求参数
 const hashIndex: number = monitorPath.indexOf(`?`);

 if (hashIndex !== -1) {
 monitorPath = monitorPath.substring(0, hashIndex);
 }

 return monitorPath;
}

(3)接口过滤baseUrl。细心的话,会发现在使用axios的接口时,自行决定是否带入baseUrl,那是因为axios会在请求时进行区分过滤。如果项目前期并没有很好的定义使用方式的话,会有两种不同使用axios的方式。那么,就需要对baseUrl进行过滤。

// 去除baseUrl
cleanBaseUrl(fullUrl: string): string {

 const baseUrlLength: number = this.baseUrl.length;
 return fullUrl.substring(baseUrlLength);
 
}

(4)组件初始化,通过传入params的方式,将对象实例化出来。

new Mask({
 modeType, // hash或history
 autoMonitoring, // 是否更写原生XMLHttpRequest对象
 maskUrlList, // 配置引入的页面和接口
 baseUrl, // 当前项目的baseUrl
 ...
}).init()

四、总结

本文介绍了统一遮罩层的背景、问题及设计方案。但并没有将所有细节进行列举,这需要根据实际业务进行选择。但大体方案已经列出:
(1)遮罩层应该在一些接口pending装的时候显示,所有接口返回后自动关闭。这里的接口是指需要监听的接口
(2)组件最重要的两个函数为appendMask添加遮罩层和removeMask删除遮罩层。
(3)如果想Mask完全独立,并不想依赖于第三方库(axios)的回调,可以直接对XMLHttpRequest进行改写,但这样做风险很大,并不建议。
(4)组件更换统一组员自己配置路由及监听接口的方式。这里的逻辑可以自行决定,如果要监听的接口多,可以采用黑名单,反之则白名单。
(5)对渲染的优化、请求带参数、路由的模式进行了优化。

到此这篇关于如何在Vue项目中添加接口监听遮罩的文章就介绍到这了,更多相关Vue 接口监听遮罩内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
springboot+vue实现文件上传下载
Nov 17 Vue.js
vue实现登录、注册、退出、跳转等功能
Dec 23 Vue.js
Vue实现省市区三级联动
Dec 27 Vue.js
Vue+Bootstrap实现简易学生管理系统
Feb 09 Vue.js
vue-router路由懒加载及实现的3种方式
Feb 28 Vue.js
vue项目实现分页效果
Mar 24 Vue.js
vue实现可拖拽的dialog弹框
May 13 Vue.js
详解Vue的options
May 15 Vue.js
vue 实现弹窗关闭后刷新效果
Apr 08 Vue.js
Vue OpenLayer测距功能的实现
Apr 20 Vue.js
vue.js 使用原生js实现轮播图
Apr 26 Vue.js
vue router 动态路由清除方式
May 25 Vue.js
使用vue3重构拼图游戏的实现示例
Jan 25 #Vue.js
vue 计算属性和侦听器的使用小结
Jan 25 #Vue.js
vue keep-alive的简单总结
Jan 25 #Vue.js
vue form表单post请求结合Servlet实现文件上传功能
Jan 22 #Vue.js
Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤
Jan 22 #Vue.js
Vue仿Bibibili首页的问题
Jan 21 #Vue.js
如何在vue 中使用柱状图 并自修改配置
Jan 21 #Vue.js
You might like
解析php中用PHPMailer来发送邮件的示例(126.com的例子)
2013/06/24 PHP
PHP实现移除数组中为空或为某值元素的方法
2017/01/07 PHP
PDO::errorInfo讲解
2019/01/28 PHP
关于laravel模板中生成URL的几种模式总结
2019/10/18 PHP
javascript中自定义对象的属性方法分享
2013/07/12 Javascript
复制网页内容,粘贴之后自动加上网址的实现方法(脚本之家特别整理)
2014/10/16 Javascript
jQuery前端框架easyui使用Dialog时bug处理
2014/12/05 Javascript
jQuery源码解读之removeClass()方法分析
2015/02/20 Javascript
JS实现网页滚动条感应鼠标变色的方法
2015/02/26 Javascript
JS模式之简单的订阅者和发布者模式完整实例
2015/06/30 Javascript
[原创]Javascript 实现广告后加载 可加载百度谷歌联盟广告
2016/05/11 Javascript
获取当前按钮或者html的ID名称实例(推荐)
2017/06/23 Javascript
JS实现的文字间歇循环滚动效果完整示例
2018/02/13 Javascript
iview在vue-cli3如何按需加载的方法
2018/10/31 Javascript
微信小程序保存多张图片的实现方法
2019/03/05 Javascript
vue-model实现简易计算器
2020/08/17 Javascript
[49:27]2018DOTA2亚洲邀请赛 4.4 淘汰赛 TNC vs VG 第一场
2018/04/05 DOTA
python爬虫入门教程--利用requests构建知乎API(三)
2017/05/25 Python
python-opencv颜色提取分割方法
2018/12/08 Python
解决项目pycharm能运行,在终端却无法运行的问题
2019/01/19 Python
利用python脚本如何简化jar操作命令
2019/02/24 Python
python矩阵运算,转置,逆运算,共轭矩阵实例
2020/05/11 Python
python如何调用java类
2020/07/05 Python
6种非常炫酷的CSS3按钮边框动画特效
2016/03/16 HTML / CSS
英国品牌男装折扣网站:Brown Bag
2018/03/08 全球购物
Manuka Doctor英国官网:真正的麦卢卡蜂蜜和护肤品
2018/10/26 全球购物
大门门卫岗位职责
2013/11/30 职场文书
历史专业学生的自我评价
2014/02/28 职场文书
社区居务公开实施方案
2014/03/27 职场文书
社区爱国卫生月活动总结
2014/06/30 职场文书
行政专员岗位职责范本
2014/08/26 职场文书
党校学习个人总结
2015/02/15 职场文书
企业财务管理制度范本
2015/08/04 职场文书
CSS3点击按钮圆形进度打钩效果的实现代码
2021/03/30 HTML / CSS
mybatis使用oracle进行添加数据的方法
2021/04/27 Oracle
Python中使用subprocess库创建附加进程
2021/05/11 Python