I. Business background

The use of a mask layer to shield users from abnormal operations is often used in the front end. However, in some projects, there is no unified management of Mask layer, which will cause the following problems: (1) All business components need to introduce Mask layer component, that is, every. Vue business component introduces Mask component in template. Components exist in every corner of the project, which is unmanageable, and the code is extremely redundant. (2) Mask components are scattered to all corners of the business, so the variables that control whether the Mask layer is displayed are scattered in the business components. For example, when maskShow is used to control whether or not a mask layer is displayed, 200+ maskShow variables can be generated in a complex project. (3) maskShow is too much and integrated into the business. At the same time, variables of maskShow are often written in the callback function of the interface, and variables are often forgotten to change, resulting in logical errors in the display and display of the mask layer. (4) Projects are often debugged locally while the actual operation is online, and problems in (3) are often not verified locally. Because these problems often occur online in a poor network environment. For example, after a button is pressed, it needs to wait for the interface to return before it can be clicked again. However, because the local return speed is faster, if you forget to add a mask layer, there will be no problem. However, if the network is faulty online environment, it is easy to occur, and once the problem occurs, it is difficult to locate, greatly affecting the work efficiency.

Ii. Problem analysis

With this background, it makes sense to add a common mask layer component for management in real projects. After analysis, specific problems need to be solved as follows: (1) the time when the mask layer appears and closes. (2) Mask component design. (3) How this component is gracefully introduced into the project without coupling. (4) How to gradually replace the original maskShow in existing projects, so as not to cause large-scale problems. (5) Details

Three, component design

1. The time when the mask layer appears and closes

This problem depends on different business requirements, but the author believes that the emergence and closure of most masks mainly depends on the request and return of interfaces. When an interface is in the pending state of request, the mask layer is displayed, and when all interfaces return, the mask is closed. This article addresses the interface request mask problem, is written in TS, and does not cover all the details.

2. Mask component design

The Mask component is a class that hides details inside the class. (1) The main function of class is to add and delete the mask layer and transfer the URL of the current request interface.

AppendMask (URL: string): void{} appendMask(URL: string): void{}}Copy the code

(2) Add the mask layer function, which is called when requested, passing in the current interface URL. The function maintains a listener that listens for requests that are currently pending. The value of this object is the number of pending states for the interface. By assuming that the mask view component is already mounted on the Vue prototype chain, or if not, by introducing it above the component.

Interface HTTPDictInterface {[index: string]: number; } appendMask(url: string): void{ if(! this.monitorHTTPDict[url]){ this.monitorHTTPDict[url] = 0; } this.monitorHTTPDict[url] += 1; // Display mask layer if(! This.mask && object.keys (this.monitorHttpdict).length){// Add a mask layer style to the body, Constructor = vue.extend (vue.prototype.$Mask); this.mask = new Constructor().$mount(); document.body.appendChild(this.mask.$el); }}Copy the code

(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 no interface is in the pending state, delete the key. Delete the mask layer if the object is empty and has a mask layer.

RemoveMask (url: string): void{// Return if (this.monitorHTTPDict[monitorUrl]) {this.monitorHTTPDict[monitorUrl] -= 1; if (this.monitorHTTPDict[monitorUrl] <= 0) { delete this.monitorHTTPDict[monitorUrl]; If (this.mask && this.hasmask () &&! Object.keys(this.monitorHTTPDict).length) { document.body.removeChild(this.mask.$el); this.mask = null; } this.timer = null; }Copy the code

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

With this component, the appendMask function is called before all requests are initiated and removeMask is called 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 make the code for the Mask component project-independent; it relies on the API of the specific interface framework.

The instance. The interceptors. Request. Use ((config) = > {/ / add the mask layer mask. AppendMask (config. Url); return config; });Copy the code

(2) Add init functions to inject callbacks directly into native XMLHttpRequest objects. Change the native XMLHttpRequest function to inject callbacks into the events’ loadStart ‘and’ loadEnd ‘. Note that loadStart does not receive the url of the current request, so you need to rewrite the open function. Mount the URL that open receives the parameter to the new XHR object. Use this method with caution. Because changing native apis in such a way is dangerous and forbidden in many coding specifications, if everyone were to rewrite native apis, introducing these frameworks at the same time would create conflicts with unintended consequences.

Init (){if (this.automonitoring){this.initRequestMonitor(); Interface NewXhrInterface extends XMLHttprequest {requestUrl? InitRequestMonitor (): void{let OldXHR = window.xmlHttprequest; let maskClass: Mask = this; // @ts-ignore, encoding specification does not allow modification 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 maskClass.appendMask(url); }); realXHR.addEventListener(`loadend`, () => { const responseURL: string = (realXHR as XMLHttpRequest).responseURL; const url: string = maskClass.cleanBaseUrl(responseURL); // Remove the mask maskclass.removemask (url); }); return realXHR; }; }Copy the code

(3) Injection use mode, directly call init. All requests to change the project then go through the Mask.

new Mask().init()
Copy the code

4. How to gradually replace the original maskShow in 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 there will be problems in a large area, but the gain outweighs the loss. Therefore, a gradual replacement should be adopted to achieve a smooth transition. The main idea is to configure the page and blacklist the way to determine which pages to introduce this component, so that each team member can modify their own, after all, the person in charge of the page is the most aware of the current page business. As for the blacklist or whitelist, it depends on the specific business of the project.

// key Specifies the route page to listen on. Value is an array of blacklisted interfaces that do not need to listen on const PAGE_ONE = '/home'; const PAGE_TWO = `/login`; const HTTP_ONE = `xxx` export const maskUrlList = { [PAGE_ONE]: [HTTP_ONE], [PAGE_TWO]: [], };Copy the code

The appendMask method filters blacklist and unconfigured pages. MaskUrlList is the controlled object. It checks the page route first and then the blacklist.

AppendMask (URL: string): void{// Get the path of the current page, get the page path, differentiate based on hash and history modes const monitorPath: string = this.getMonitorPath(); 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); }}Copy the code

5. Details

(1) Close the mask layer after rendering, and put the actual deletion logic of the mask layer into the timer. Vue’s asynchronous rendering uses promise, so if it is closed after rendering, it needs to put it into setTimeout. Knowledge of event loops is involved here. When the interface returns, if the page needs to be rendered, a Promise will be asynchronously executed. The Promise is a microtask, and setTimeout is a macro task. After the execution of the main thread, the microtask will be executed first, and then the asynchronous macro task setTimeout will be executed.

// Clear 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); }Copy the code

(2) Filter the ‘? ‘, and ‘#’ in hash mode,

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); } // Capture path, delete request parameter const hashIndex: number = monitorPath.indexof ('? `); if (hashIndex ! == -1) { monitorPath = monitorPath.substring(0, hashIndex); } return monitorPath; }Copy the code

(3) Interface filtering baseUrl. If you’re careful, you’ll notice that when you use axios’ interface, you decide whether to bring baseUrl, because AXIos does discriminating filtering on requests. There are two different ways to use Axios if the usage is not well defined at the beginning of the project. Then, you need to filter the baseUrl.

BaseUrl cleanBaseUrl(fullUrl: string): string {const baseUrlLength: number = this.baseurl.length; return fullUrl.substring(baseUrlLength); }Copy the code

(4) Component initialization, through the way of passing params, instantiate the object out.

New Mask({modeType, // hash or history autoMonitoring, // whether to write the native XMLHttpRequest object maskUrlList, // configure the imported page and interface baseUrl, // Current project baseUrl... }).init()Copy the code

Four,

This paper introduces the background, problems and design scheme of unified mask layer. However, not all the details are listed, which needs to be selected according to the actual business. (1) The mask layer should be displayed when some interfaces are pending, and automatically closed when all interfaces return. (2) The two most important functions of the component add a mask layer to appendMask and remove a mask layer to removeMask. (3) If you want your Mask to be completely independent and don’t want to rely on third-party library (AXIos) callbacks, you can rewrite XMLHttpRequest directly, but doing so is risky and not recommended. (4) Components change the way of configuring routes and monitoring interfaces for team members. The logic here can decide by itself. If there are many interfaces to listen on, the blacklist can be used; otherwise, the whitelist can be used. (5) Optimization of rendering, request with parameters, routing mode was optimized.