Project reconfiguration modular encapsulation summary (applet project as an example)

Time:2021-8-14

Project reconfiguration modular encapsulation summary (applet project as an example)

The project reconstruction task, which lasted more than two months, will go online next week,
Use the weekend to write a summary of this refactoring.
This paper takes the applet project as an example,
However, its ideas can be used for reference by other front-end projects.

Some design patterns and algorithms such as singleton pattern and factory pattern are used in the reconstruction,
It also answers the question of how to apply design patterns in development.

Project main directory

.
├── app.js
├── app.json
├── app.wxss
├── pages
├── service
│   ├── const.js
│   ├── env.js
│   └── http
│       ├── appDataRequest.js
│       ├── cacheManager.js
│       ├── http.js
│       └── loginManager.js
└── utils
    └── utils.js
...

There are four modules to call each other

  1. Request method encapsulation module
  2. Login module
  3. Cache module
  4. Interface request module

1. Request method encapsulation module

Technical point: factory mode

Because the request header data of different domain name interfaces are different, differential encapsulation is carried out in this module,
The factory mode is used to facilitate the interface request module to only care about the interface call and do not repeatedly process the logic related to the request header

The following demo specifically distinguishes:
Post and get requests require request headers and do not require request headers
  • Specific implementation code
Import env from '.. /.. / service / env. JS' // environment variable / domain name address
Import loginmanager from '.. /.. / service / HTTP / loginmanager. JS' // login module
Import msgutil from '.. /.. / utils / msgutil. JS' // pop up prompt for single instance implementation
Import CacheManager from '.. /.. / service / HTTP / CacheManager. JS' // cache module


/**
 *Encapsulate wechat request and be responsible for the assembly of general interface parameters
 *Assemble authorization information / verification information for different service background according to different servertypes
 *For the error information returned by the server, the current limit / login failure and other errors shall be handled uniformly
 */
export default class http {

  constructor(params) {
    //Server type, a or B or C
    //Different service backgrounds have different token information and parameter verification methods
    this.serverType = params.serverType || 'A';
  }

  /**
   *Try to access the cached information and complete the request directly if available
   * @method tryCachedData
   *@ return {bool} true or false: true indicates that the cache has been used
   *@ param type distinguishes between callback mode and promise request
   */
  tryCachedData(params = {}, key, sec, type = 0) {
    if (!params.ignoreCache) {
      let cacheMgr = cacheManager.getInstance();

      let cachedData = cacheMgr.getValidData(key, sec);
      if (cachedData) {
        if(!type) {
          params.success && params.success(cachedData.data);
          params.complete && params.complete();
          return true
        }else {
          return cachedData.data;
        }
      }
    }

    return false;
  }

  /**
   *For HTTP headers without token authorization, set relevant fields according to servertype
   * @method getHeader
   *@ return {object} HTTP header information
   */
  getHeader() {

    let header = {
      'Content-Type': 'application/json'
    }

    switch (this.serverType) {
      case 'A':
        {
          header.d = env.d;
          header.h = env.h;
        }
        break;
      case 'B':
        {
          header.a = env.a;
          header.b = env.b;
          header.c = env.c;
            .......
        
          
        }
        break;
    }

    return header;
  }

  /**
   *Post request encapsulation, no token
   * @method POST
   * @return
   */
  POST(params = {}) {
    let header = this.getHeader();
    wx.request({
      url: params.url,
      header,
      data: params.data,
      method: "POST",
      success: (res) => {
        params.success && params.success(res);
      },
      fail: (res) => {
        this.handleTrafficLimit(res);
        params.fail && params.fail(res);
      },
      complete: (res) => {
        params.complete && params.complete(res);
      }
    })
  }

  /**
   *Get request encapsulation, no token
   * @method GET
   * @return
   */
  GET(params = {}) {
    let header = this.getHeader();
    wx.request({
      url: params.url,
      header,
      data: params.data,
      method: "GET",
      success: (res) => {
        params.success && params.success(res);
      },
      fail: (res) => {
        this.handleTrafficLimit(res);
        params.fail && params.fail(res);
      },
      complete: (res) => {
        params.complete && params.complete(res);
      }
    })
  }

  /**
   *For the HTTP header requiring token authorization, set the authorization parameters for according to the servertype
   * @method getHeaderWithToken
   *@ return {object} HTTP header information
   */
  getHeaderWithToken() {
    let header = this.getHeader();

    const loginMgr = loginManager.getInstance();
    switch (this.serverType) {
      case 'EC':
        {
          let userToken = loginMgr.getIToken();
          if (userToken) header.Authorization = userToken;
        }
        break;
      case 'MApp':
        {
          let sid = loginMgr.getUserId();
          if (sid) header.sid = sid;
        }
        break;
    }

    return header;
  }

  /**
   *Post request encapsulation with token
   * @method POSTWithToken
   * @return
   */
  POSTWithToken(params = {}) {
    let header = this.getHeaderWithToken();
    wx.request({
      url: params.url,
      header,
      data: params.data,
      method: "POST",
      success: (res) => {
        if (!this.handleTokenError(res)) {
          params.success && params.success(res);;
        } else {
          params.fail && params.fail(res);
        }
      },
      fail: (res) => {
        if (!this.handleTrafficLimit(res)) {
          this.handleTokenError(res);
        }
        params.fail && params.fail(res);
      },
      complete: (res) => {
        params.complete && params.complete(res);
      }
    })
  }

  /**
   *Get request encapsulation with token
   * @method GETWithToken
   * @return
   */
  GETWithToken(params = {}) {
    let header = this.getHeaderWithToken();
    wx.request({
      url: params.url,
      header,
      data: params.data,
      method: "GET",
      success: (res) => {
        if (!this.handleTokenError(res)) {
          params.success && params.success(res);;
        } else {
          params.fail && params.fail(res);
        }
      },
      fail: (res) => {
        if (!this.handleTrafficLimit(res)) {
          this.handleTokenError(res);
        }
        params.fail && params.fail(res);
      },
      complete: (res) => {
        params.complete && params.complete(res);
      }
    })
  }

  

  /**
   *Current limiting treatment
   * @method handleTrafficLimit
   *@ return {bool} processed
   */
  handleTrafficLimit(res = {}) {
    if (res.statusCode == 503 || (res.header && res.header['Ec-Over-Limit'] == 503)) {
      msgUtil.getInstance().showTrafficLimitMsg();
      return true;
    }

    return false;
  }

  /**
   * 
   *Token invalidation handling
   * @method handleTokenError
   *@ return {bool} processed
   */
  handleTokenError(res = {}) {
    if (res.statusCode == 401 || (res.data && res.data.result == 10000)) {
      loginManager.getInstance().checkTokenInfo(true);
      return true;
    }

    return false;
  }
}

2. Login module

Technical point: Singleton mode publishing subscriber mode

1: Singleton mode
Ensure that the global login status is unified and avoid calling the login information in the cache repeatedly. If you need to use the login information, you only need to read the data in the memory of the single instance
2: Publish subscriber mode
Ensure the consistency of user actions. If the user needs to log in and is not logged in now,
Add the actions to be executed into the subscriber queue. When the login status changes, publish the latest login status and perform user consistency operations
  • Specific implementation code
import HTTP from 'http';  //  Encapsulated request method
Import env from '.. / env. JS' // environment variable
Import msgutil from '.. /.. / utils / msgutil. JS' // globally unique prompt for single instance implementation

/**
 *The login authorization management module is responsible for the operation of user registration / login / update token
 *Manage the authentication and authorization of multiple platforms in the background
 */
export default class loginManager {

  static instance;

  /**
   *[getInstance get singleton]
   * @method getInstance
   * @return {object} 
   */
  static getInstance() {
    if (false === this.instance instanceof this) {
      this.instance = new this;
    }
    return this.instance;
  }

  constructor() {
    //Login listening function registration
    this.loginCbs = {};
    //Temporary callback method variable
    this.tmpLoginCb = null;
    //Cache tag
    this.tag = 'LOGIN'
    
    //Domain name request method instantiation of different interfaces
    this.ABCHttp = new HTTP({
      serverType: 'ABC'
    });
    .....
    
    //Some information after login
    this.accessToken = '';
    .....
    
    //The token updates the flag. This business needs nothing to do with the architecture idea
    this.checkingToken = false;
    
    //Initialize user information
    this.restoreTokenInfo();

  }

  /**
   *Judge the current login status according to the token time
   *Login status judgment method
   */
  isLogined() {
    let ts = new Date().getTime() / 1000;
    let logined = false;
    if (this.accessToken && ts < this.atExpiredAt) {
      logined = true;
    }
    return logined;
  }
  //Specific method for reading and operating user information    
  ......

  /**
   *ABC login
   * }
   */
   doLogin(params = {}) {
    this.ecHttp.POST({
      URL: ` login request address',
      data: params.data,
      success: (res) => {
        if (res && res.data && res.data.result == 0 && res.data.token) {
          //Log in successfully and execute callback
          params.success && params.success(res)
          //Save user information and corresponding processing after successful login
          this._processUserTokenInfo(res);

        } else {
          //Callback of login failure
          params.fail && params.fail(res)
        }
      },
      fail: (res) => {
        params.fail && params.fail(res)
      },
      complete: params.complete
    })
  }
  //Analyze the login information of the logged in / registered user, etc
  _processUserTokenInfo(res) {
    if (!res || !res.data || !res.data.token) return;

    this.accessToken = res.data.token.access_token;
    ......

    //Synchronize user information to storage
    this.saveTokenInfo();
    

    //Notify successful login status
    this.notifyLoginStatus();
    
    ......
  }

  //Log out
  logout(params = {}) {
    ......
    //Clear cached user information
    this.clearTokenInfo();
    //The method of executing the subscription informs that the login has been exited
    this.notifyLoginStatus();

    params.success && params.success()

  }

  /**
   *Recovering user information from storage
   */
  restoreTokenInfo() {
    this.accessToken = wx.getStorageSync('access_token');
    ......
  }
  /**
   *Save user information to storage
   */
  saveTokenInfo() {
    wx.setStorage({
      key: 'access_token',
      data: this.accessToken,
    })
    ......
  }
  /**
   *Clear user information cache
   */
  clearTokenInfo() {
    this.accessToken = this.refreshToken = '';
    wx.removeStorage({
      key: 'access_token',
    });
    ......
  }

  /**
   *Register to listen for login status changes
   *Must be used with offloginstatus
   */
  onLoginStatus(key, fn) {
    if (key && fn) this.loginCbs[key] = fn;
  }

  /**
   *Cancel listening for login status changes
   */
  offLoginStatus(key) {
    if (key) delete this.loginCbs[key];
  }

  notifyLoginStatus() {
    let logined = this.isLogined();
    for (let key in this.loginCbs) {
      let fn = this.loginCbs[key];
      fn && fn(logined)
    }
  }

  /**
   *Temporary callback function set when calling app.loginifneed
   */
  addTmpLoginCb(fn) {
    this.tmpLoginCb = fn;
  }

  removeTmpLoginCb() {
    this.tmpLoginCb = '';
  }

  /**
   *Check whether the token needs to be updated
   *Force: force update
   */
  checkTokenInfo(force = false) {
    if (this.checkingToken) return;

    this.checkingToken = true;

    //check token
    if (force || this._shouldRefreshToken()) {
      this._refreshToken((logined) => {
        this.checkingToken = false;
        if (! Logged {// the login confirmation is invalid. You will be prompted to log in again
          this.clearTokenInfo();
          if (force) {
            msgUtil.getInstance().showLoginPrompt();
          } else {
            ....
          }
        }

        this.notifyLoginStatus();

      });
    } else {
      this.checkingToken = false;
    }

  }

  /**
   *Does the current token information need to be updated
   *Update within 1 hour
   * 
   *@ return do you want to refresh the token
   */
  _shouldRefreshToken() {
    let ts = new Date().getTime() / 1000;

    let ret = false;
    if (this.refreshToken) {
      if (this.atExpiredAt - ts < 60 * 60) {
        ret = true;
      }
    } else {
      this.clearTokenInfo();
      this.notifyLoginStatus();
    }
    return ret;
  }

  _refreshToken(cb) {

    this.ecHttp.POST({
      URL: 'request refresh',
      data: {
        refreshToken
      },
      success: (res) => {
        //Process new token information
        if (res.data.access_token && res.data.refresh_token) {
          ......

          this.saveTokenInfo();

          

          cb && cb(true)
        } else {
          cb && cb(false)
        }
      }
    })
  }

3. Cache module

Technical point: single case mode LRU

Scenario: to reduce CND requests and server pressure, some interfaces are cached
The single instance mode is used to realize the global sharing of memory data, and the LRU algorithm handles the cache logic;

Two storage methods are distinguished:
Store memory data and store cache data
At the same time, the cache time is processed to distinguish between permanent cache and time limited cache
Timer is used to regularly clear expired cache data
/**
 *Secondary data cache management
 *1. Only stored in memory
 *2. Store it in memory and storage at the same time, and persist it
 */

//Maximum cached data
const MAX_LEN = 250;

export default class cacheManager {
  /**
   *[instance current instance]
   * @type {this}
   */
  static instance;

  /**
   *[getInstance get singleton]
   * @method getInstance
   * @return
   */
  static getInstance() {
    if (false === this.instance instanceof this) {
      this.instance = new this;
    }
    return this.instance;
  }

  constructor() {
    this.data = {};
    this.keys = [];
    
  }

  enableAutoClear() {
    if (this.timer) clearInterval(this.timer)
    //Regularly clean up expired data in memory to avoid excessive memory use
    this.timer = setInterval(() => {
      this.clearExpiredData();
    }, 10 * 1000)
  }

  clearExpiredData() {
    // console.log("[CacheMgr] cached key number before clear: " + this.keys.length)
    // console.log("try to clear expired cache ...")

    let t = parseInt(new Date().getTime() / 1000);
    for (let key in this.data) {
      let d = this.data[key];
      if (d.requestTime && d.duration > 0) {
        if (t > d.requestTime + d.duration) {
          // console.log("clear data for key = "+ key)
          this.clearData(key);
        }
      }
    }

    // console.log("[CacheMgr] cached key number after clear: " + this.keys.length)

  }

  /**
   *Keep the data in memory without persistence
   *@ param duration effective time, seconds. If the valid time is exceeded, it will be cleared automatically, - 1 means it will not be cleared.
   */
  setData(key, d, duration = -1) {
    if (key) {
      this.data[key] = {
        requestTime: parseInt(new Date().getTime() / 1000),
        duration,
        data: d
      }
      this.sortKey(key);
    }
  }

  /**
   *Keep the data in memory and storage and persist it
   *@ param duration effective time, seconds. If the valid time is exceeded, it will be cleared automatically, - 1 means it will not be cleared.
   */
  setPersistanceData(key, d, duration = -1) {
    if (key) {
      this.data[key] = {
        requestTime: parseInt(new Date().getTime() / 1000),
        duration,
        data: d
      }
      wx.setStorage({
        key,
        data: this.data[key]
      })
      this.sortKey(key);
    }
  }

  sortKey(key) {
    let index = this.keys.indexOf(key);
    //Place hotkeys at the end of the queue
    if (index >= 0) {
      let array = this.keys.splice(index, 1);
      this.keys.push(array[0]);
    } else {
      this.keys.push(key);
    }
    //If the number of caches is exceeded, delete the least commonly used data in the header
    if (this.keys.length > MAX_LEN) {
      let keys = this.keys.splice(0, this.keys.length - MAX_LEN)
      for (let i=0; i<keys.length; i++) {
        this.clearData(keys[i])
      }
    }
  }

  getData(key) {
    let d = this.data[key];
    if (!d) {
      d = wx.getStorageSync(key);
      if (d) this.data[key] = d;
    }
    if (d) this.sortKey(key);
    return d;
  }

  clearData(key) {
    delete this.data[key];
    wx.removeStorage({
      key
    })
    let index = this.keys.indexOf(key);
    if (index >= 0) this.keys.splice(index, 1);
  }

  clearAllDataInMemory() {
    this.data = {}
    this.keys = []
  }

  clearAllCache() {
    this.data = {};
    wx.clearStorage();
  }

  /**
   *Get cached data in a specific time
   * @param key
   *@ param duration valid time, seconds. - 1 means that the valid time is not checked
   *@ return {object} cache data
   */
  getValidData(key, duration = -1) {
    let cachedData = this.getData(key);
    if (cachedData && (duration < 0 || (cachedData.requestTime && parseInt(new Date().getTime() / 1000) - cachedData.requestTime <= duration))) {
      return cachedData;
    }

    return '';
  }

}

4. Interface request module

Technical point: Singleton mode

Interface requests are processed uniformly, and requests are intercepted in combination with the cache module. The UI layer is insensitive to reduce the number of interface requests
Compatible with callback and promise
Take the get request that does not require a special request header as an example:
  • Specific code implementation
import HTTP from 'http';
import env from '../env.js'
import cacheManager from './cacheManager.js'
import loginManager from './loginManager.js'



const cacheMgr = cacheManager.getInstance();
const loginMgr = loginManager.getInstance();

/**
 *ABC service API
 *It can be divided in more detail according to the business module
 */
export default class ecDataRequest {
  /**
   *[instance current instance]
   * @type {this}
   */
  static instance;

  /**
   *[getInstance get singleton]
   * @method getInstance
   * @return
   */
  static getInstance() {
    if (false === this.instance instanceof this) {
      this.instance = new this;
    }
    return this.instance;
  }

  constructor() {
    this.tag = 'ABC'
    this.http = new HTTP({
      serverType: 'ABC'
    });
  }

  /**
   *Get data
   * @method getDataTimestamp
   *Settable cache
   */
  getDataTimestamp(params = {}) {
    //Set cache ID
    let key = `${this.tag}_getDataTimestamp`;
    //Cache effective time
    let duration = 60 * 60;
    return new Promise((resolve, rejects) => {
      //Intercept request read cache
      const requsetRes = this.http.tryCachedData(params, key, duration, 1);
      if (requsetRes) {
        resolve(requsetRes);
        return;
      };
      this.http.GET({
        URL: env.serverimageapi + ` interface address',
        success: (res) => {
          //The request successfully set / update the cache
          cacheMgr.setPersistanceData(key, res, duration);
          resolve(res)
        },
        fail: (res) => {
          rejects(res)
        }
      })
    })
  }
}

Specific business page usage

import appDataRequest from '../service/http/appDataRequest.js';
import loginManager from "../service/http/loginManager.js";
const appRequest = appDataRequest.getInstance();
const loginMgr = loginManager.getInstance()
const app = getApp();

Page({
  data: {

  },
  onLoad: function(e) {
   //Verify whether to log in
   if(loginMgr.isLogined()){
      //Specific business operations
      this.requestData()
   }else {
       //See the specific implementation below
       app.loginIfNeed((islogin)=> {
           //Specific business operations
           this.requestData()
           
       })
   }
  },
  requestData: function(str) {
    var that = this;
      appRequest.getDataTimestamp(Number(str))
        .then(res => {
          console.log(res.data);
          if (res.data.success) {
            
          } else {
            
          }
        })
        .catch(err => {})
    }
  },

})

loginIfNeed

Globally unique entry method to login page
Use the cache module to put the callback into memory
Execute after successful login
//Encapsulate the login judgment, and complete the login before logging in
  loginIfNeed: function(complete) {
    loginMgr.removeTmpLoginCb();
    if (loginMgr.isLogined()) {
      complete && complete(true);
    } else {
      complete && loginMgr.addTmpLoginCb(complete);
      wx.navigateTo({
        URL: 'login page',
      })
    }
  },

Thank you for watching. I hope the boss will comment and give advice

More personal learning summaries of native JS welcome to star

Design pattern personal learning summary, click to open a surprise