如何在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 相关文章推荐
vue element-ul实现展开和收起功能的实例代码
Nov 25 Vue.js
vue中defineProperty和Proxy的区别详解
Nov 30 Vue.js
Vue在H5 项目中使用融云进行实时个人单聊通讯
Dec 14 Vue.js
vue 导航守卫和axios拦截器有哪些区别
Dec 19 Vue.js
Vue 简单实现前端权限控制的示例
Dec 25 Vue.js
vue实现简易计算器功能
Jan 20 Vue.js
vue实现禁止浏览器记住密码功能的示例代码
Feb 03 Vue.js
vue首次渲染全过程
Apr 21 Vue.js
详解Vue router路由
Nov 20 Vue.js
vue使用echarts实现折线图
Mar 21 Vue.js
Vue.Draggable实现交换位置
Apr 07 Vue.js
Axios代理配置及封装响应拦截处理方式
Apr 07 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数据库类
2009/05/27 PHP
php学习之数据类型之间的转换代码
2011/05/29 PHP
PHP静态调用非静态方法的应用分析
2013/05/02 PHP
PHP实现使用优酷土豆视频地址获取swf播放器分享地址
2014/06/05 PHP
php处理抢购类功能的高并发请求
2018/02/08 PHP
js 事件小结 表格区别
2007/08/13 Javascript
jQuery控制输入框只能输入数值的小例子
2013/03/20 Javascript
Jquery 实现表格颜色交替变化鼠标移过颜色变化实例
2013/08/28 Javascript
javascript实现点击按钮弹出一个可关闭层窗口同时网页背景变灰的方法
2015/05/13 Javascript
javascript制作的滑动图片菜单
2015/05/15 Javascript
js+html5实现canvas绘制镂空字体文本的方法
2015/06/05 Javascript
require简单实现单页应用程序(SPA)
2016/07/12 Javascript
jQuery Ajax实现跨域请求
2017/01/21 Javascript
JQ中$(window).load和$(document).ready区别与执行顺序
2017/03/01 Javascript
使用veloticy-ui生成文字动画效果
2018/02/08 Javascript
React Native使用fetch实现图片上传的示例代码
2018/03/07 Javascript
Vue中props的使用详解
2018/06/15 Javascript
微信小程序实现收藏与取消收藏切换图片功能
2018/08/03 Javascript
react.js组件实现拖拽复制和可排序的示例代码
2018/08/20 Javascript
vue2实现搜索结果中的搜索关键字高亮的代码
2018/08/29 Javascript
JavaScript数据结构与算法之二叉树实现查找最小值、最大值、给定值算法示例
2019/03/01 Javascript
解决layui表格内文本超出隐藏的问题
2019/09/12 Javascript
[01:10]3.19DOTA2发布会 三代刀塔人第一代
2014/03/25 DOTA
[03:26]《DAC最前线》之EG经理自述DOTA2经历
2015/02/02 DOTA
解决pycharm运行出错,代码正确结果不显示的问题
2018/11/30 Python
Python输出指定字符串的方法
2020/02/06 Python
CSS3 实现弹幕的示例代码
2017/08/07 HTML / CSS
基于HTML5超酷摄像头(HTML5 webcam)拍照功能实现代码
2012/12/13 HTML / CSS
HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题
2021/01/19 HTML / CSS
欧洲领先的技术商店:eibmarkt.com
2019/05/10 全球购物
物流专业大学生职业生涯规划书范文
2014/01/15 职场文书
大学英语演讲稿范文
2014/04/24 职场文书
优秀员工自荐书
2015/03/06 职场文书
爱护环境卫生倡议书
2015/04/29 职场文书
python使用glob检索文件的操作
2021/05/20 Python
MySQL 数据表操作
2022/05/04 MySQL