How to add an interface listening mask in an Vue project

  • 2021-10-25 05:44:18
  • OfStack

1. Operational context

Using a mask layer to shield the abnormal operation of the user is a common way for the front end. However, in some projects, the mask layer is not managed uniformly, which will cause the following problems:
(1) Mask layer components are introduced into all business components, that is, Mask components are introduced into template for each. vue business component. Components exist in every corner of the project, which is not conducive to management and the code is extremely redundant.
(2) Mask components are scattered in every corner of the business, so the variables that control whether the mask layer is displayed are also scattered in the business components. For example, when maskShow is used to control whether the mask layer is displayed, an maskShow variable of 200 + will be generated in a more complex project.
(3) maskShow is too much and integrated into the business. At the same time, the variables of maskShow are often written in the callback function of the interface, which often forgets to change the variables, resulting in logical errors in the mask layer.
(4) The project is often debugged locally, but the real operation is online, and (3) the problems in it are often unverifiable locally. Because these problems often occur in a poor online network environment. For example, after pressing a button, you need to wait for the interface to return before clicking again. However, because the return speed is fast locally, if you forget to add a mask layer, there will be no problem. However, if it is an online environment with network problems, it is easy to appear, and once the problem appears, it is difficult to locate, which greatly affects work efficiency.

2. Problem analysis

According to the above background, it makes sense to add a common mask layer component to the actual project for management. After analysis, the following problems need to be solved:
(1) When the mask layer appears and closes.
(2) Mask component design.
(3) How the component is elegantly introduced into the project without coupling.
(4) How to replace the original maskShow gradually in the existing project, so as not to cause large-scale problems.
(5) Details

3. Component design

1. When the mask layer appears and closes

This problem is determined according to different business requirements, but the author thinks that the appearance and closure of most masks mainly depend on the request and return of interfaces. One interface displays the mask layer when requesting pending, and all interfaces close the mask when returning. This article mainly addresses the interface request masking problem, written using ts, and does not list all the details.

2. Mask component design

The Mask component is an class, shielding details inside the class.
(1) class internal most important function is to add and remove the mask layer, the transmission of the current request interface of url.


class Mask {
 //  Display mask layer 
 appendMask(url: string): void{}

 //  Delete mask layer 
 removeMaskl(url: string): void{}
}

(2) Add a mask layer function, call the function when requested, and pass in the current interface url. The function maintains a listening object to listen for the request of whether there is pending status at present. The value of this object is the number of pending states of this interface. By assuming that the mask view component has been mounted on the Vue prototype chain, if not, it can be introduced above the component.


//  Listening object data type definition 
interface HTTPDictInterface {
 [index: string]: number;
}

appendMask(url: string): void{ 

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

 //  If there is a listening interface, the mask layer is displayed 
 if(!this.mask && Object.keys(this.monitorHTTPDict).length){

 //  In body Add a mask layer style to the, $Mask Style components for the mask layer 
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

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

(3) Delete the mask layer function, which will be called after each request. When the request listening object is found to be empty, delete the mask layer. If there is no interface in pending status, delete the docked key. If the object is empty and there is a mask layer, delete the mask layer.


removeMask(url: string): void{

 //  Upon successful return 
 if (this.monitorHTTPDict[monitorUrl]) {
 this.monitorHTTPDict[monitorUrl] -= 1;
 if (this.monitorHTTPDict[monitorUrl] <= 0) {
 delete this.monitorHTTPDict[monitorUrl];
 }
 }

 // hasMask Is used to detect whether there is a mask tag element on the page 
 if (this.mask && this.hasMask() && !Object.keys(this.monitorHTTPDict).length) {
 document.body.removeChild(this.mask.$el);
 this.mask = null;
 }

 this.timer = null;
}

3. How this component is elegantly introduced into the project without coupling.

Using this component, you need to call the appendMask function before all requests are initiated and the removeMask function after all requests are completed. There are two ways to call this.
(1) Use the callback of axios and other components to complete the function call. However, this approach does not separate the code of the Mask component from the project, it relies on the API of the specific interface framework.


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

 //  Add a mask layer 
 mask.appendMask(config.url);

 return config;
});

(2) Add the init function to inject callbacks directly into the native XMLHttpRequest object. Change the native XMLHttpRequest function and inject callbacks into the events' loadstart'and 'loadend'. It should be noted that there is no currently requested url in the pass received by loadstart, so it is necessary to rewrite the open function and mount the url received by open on the new xhr object. Use this method with caution. Because it is dangerous to change the native API in a 10-point way, it is prohibited in many coding specifications. If everyone rewrites the native API, when these frameworks are introduced at the same time, there will be conflicts and unpredictable consequences.


//  Decide whether to use this method by passing parameters 

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

//  New xmlhttprequest Type 
interface NewXhrInterface extends XMLHttpRequest{
 requestUrl?: string
}

//  Primary injection 
initRequestMonitor(): void{

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

 // @ts-ignore The coding specification is not allowed to be modified 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);

 //  Open the mask 
 maskClass.appendMask(url);
 });

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

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

 //  Remove Mask 
 maskClass.removeMask(url);
 });

 return realXHR;
 };
}

(3) Inject the usage mode and call init directly. In this way, all requests to change the project will go through Mask.


new Mask().init()

4. How to gradually replace the original maskShow in the existing projects, so as not to cause large-scale problems.

If it is directly used in the whole project, the area involved will become very wide, and problems will arise in a large area, but the loss outweighs the gain. Therefore, a gradual replacement method should be adopted to achieve smooth transition. The main idea is to decide which pages are introduced into this component by configuring pages and blacklists, so that each team member can modify it himself. After all, the person in charge of the page is the one who knows the current page business best. As for how to blacklist or whitelist, it is determined by the specific business of the project.


// key The routing page that you want to listen to, value For 1 Array, the interface filled in the array is blacklist, and there is no need to listen to the interface 
const PAGE_ONE = `/home`;
const PAGE_TWO = `/login`;
const HTTO_ONE = `xxx`

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

The appendMask method filters blacklists and pages that are not configured. maskUrlList for the control object, first check the page route, and then check whether there is a blacklist.


appendMask(url: string): void{

 //  Object for the current page path Gets the page path, according to the hash And history Patterns are distinguished 
 const monitorPath: string = this.getMonitorPath();

 // maskUrlList For configuration items, check the page route first, and then check whether there is a blacklist 
 if (this.maskUrlList[monitorPath]
 && !this.maskUrlList[monitorPath].includes(url)) {
 if (this.monitorHTTPDict[url] === undefined) {
 this.monitorHTTPDict[url] = 0;
 }
 this.monitorHTTPDict[monitorUrl] += 1;
 }

 //  Add a mask layer 
 if (!this.mask && this.hasMonitorUrl()) {
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

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

5. Details

(1) Close the mask layer after rendering, and put the logic of actually deleting the mask layer into the timer. The asynchronous rendering of Vue adopts promise, so if it is closed after rendering, it needs to be put into setTimeout. This involves the knowledge of event loops. When the interface returns, if the page needs to be rendered, one Promise will be executed asynchronously, Promise is a micro task and setTimeout is a macro task. When the main thread is executed, the micro task will be executed first, and then the asynchronous macro task setTimeout will be executed.


//  Clean the mask layer 
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) Filter interface '?' and '#' in hash mode,


//  Object of the requesting interface url
getMonitorUrl(url: string): string{
 const urlIndex: number = url.indexOf(`?`);
 let monitorUrl: string = url;
 if (urlIndex !== -1) {
 monitorUrl = url.substring(0, urlIndex);
 }
 return monitorUrl;
}
//  Get the current route 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);
 }

 //  Screenshot path, delete request parameter 
 const hashIndex: number = monitorPath.indexOf(`?`);

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

 return monitorPath;
}

(3) Interface filtering baseUrl. If you are careful, you will find that when using the interface of axios, you will decide whether to bring baseUrl, because axios will filter differently when requesting. There are two different ways to use axios if the usage is not well defined in the early stage of the project. Then, baseUrl needs to be filtered.


//  Listening object data type definition 
interface HTTPDictInterface {
 [index: string]: number;
}

appendMask(url: string): void{ 

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

 //  If there is a listening interface, the mask layer is displayed 
 if(!this.mask && Object.keys(this.monitorHTTPDict).length){

 //  In body Add a mask layer style to the, $Mask Style components for the mask layer 
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

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

0

(4) Component initialization, through the way of passing params, the object is instantiated.


//  Listening object data type definition 
interface HTTPDictInterface {
 [index: string]: number;
}

appendMask(url: string): void{ 

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

 //  If there is a listening interface, the mask layer is displayed 
 if(!this.mask && Object.keys(this.monitorHTTPDict).length){

 //  In body Add a mask layer style to the, $Mask Style components for the mask layer 
 const Constructor = Vue.extend(Vue.prototype.$Mask);
 this.mask = new Constructor().$mount();

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

1

4. Summary

This paper introduces the background, problems and design scheme of the mask layer of Uni-1. However, all the details are not listed, which needs to be selected according to the actual business. But the general plan has been listed:
(1) The mask layer should be displayed when some interfaces pending are installed, and all interfaces will be automatically closed after returning. The interface here refers to the interface that needs to be listened to
(2) The two most important functions of the component add the mask layer for appendMask and remove the mask layer for removeMask.
(3) If you want Mask to be completely independent and do not want to rely on the callback of the third-party library (axios), you can rewrite XMLHttpRequest directly, but this is very risky and not recommended.
(4) The way that the members of Component Replacement System 1 configure routing and monitoring interfaces by themselves. The logic here can be decided by itself, if you want to listen to more interfaces, you can use blacklist, otherwise, whitelist.
(5) Optimization of rendering, request with parameters, routing mode.


Related articles: