Vue 接口监听遮罩 怎样在Vue项目中添加接口监听遮罩
RiverTT 人气:0一、业务背景
使用遮罩层来屏蔽用户的非正常操作,是前端经常使用的方式。但是在一些项目中,并没有对遮罩层进行统一管理,这就会造成如下的问题:
(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)对渲染的优化、请求带参数、路由的模式进行了优化。
加载全部内容