The two-month-long project refactoring will go live next week, so spend the weekend writing a summary of the refactoring. This article is based on the small program project as an example, but its ideas can be used for reference in other front-end projects. Some design patterns such as singleton pattern factory pattern and some algorithms are used in the reconstruction, which can also be regarded as the solution to the problem of how to apply the design pattern in the development.Copy the code

Project Main Contents

. ├ ─ ─ app. Js ├ ─ ─ app. Json ├ ─ ─ app. WXSS ├ ─ ─ pages ├ ─ ─ service │ ├ ─ ─ const. Js │ ├ ─ ─ env. Js │ └ ─ ─ HTTP │ ├ ─ ─ AppDataRequest. Js │ ├ ─ ─ cacheManager. Js │ ├ ─ ─ HTTP. Js │ └ ─ ─ loginManager. Js └ ─ ─ utils └ ─ ─ utils. Js...Copy the code

There are four modules that call each other

  1. Request methods encapsulate modules
  2. The login module
  3. The cache module
  4. Interface request module

1. Request method encapsulates module

Technical point: Factory mode

Because different domain name interface request header data is different, so in this module to distinguish encapsulation, the use of factory mode, convenient interface request module only care about the interface call, not repeated processing request header related logic in the following demo specific distinction: POST and GET requests need headers and do not need headers four methodsCopy the code
  • Concrete implementation code
import env from '.. /.. /service/env.js'// Environment variable/domain address import loginManager from'.. /.. /service/http/loginManager.js'Import msgUtil from'.. /.. /utils/msgUtil.js'// The singleton implementation popup prompts import cacheManager from'.. /.. /service/http/cacheManager.js'// Cache module /** * encapsulates wechat request request, and is responsible for assembly of general interface parameters * assemble authorization information/verification information for different service background according to different serverType... * For the error message returned by the server, the unified processing of traffic limiting/login failure error */exportDefault class HTTP {constructor(params) {// Server type, A or B or C // different service background, Have different token information and parameter calibration method of enclosing serverType = params. ServerType | |'A'; } /** ** Try to access cached information, if available, directly complete the request * @method tryCachedData * @return {bool}  true or false:trueIndicates that cache * @param is usedtypeDistinguish between callback 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 {
          returncachedData.data; }}}return false; } /** ** HTTP header without token authorization, set the related fields * @method getHeader * @ according to serverTypereturn{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;
    }

    returnheader; } /** * 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); }})} /** * HTTP header that requires token authorization, set the authorization parameter * @method getHeaderWithToken * @ according to serverTypereturn{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;
    }

    returnheader; } /** * 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); }})} /** * handleTrafficLimit * @methodreturn{bool} Whether handleTrafficLimit has been processed */ handleTrafficLimit(res = {}) {if (res.statusCode == 503 || (res.header && res.header['Ec-Over-Limit'] == 503)) {
      msgUtil.getInstance().showTrafficLimitMsg();
      return true;
    }

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

    return false; }}Copy the code

2. Login module

Technical point: Singleton pattern publish subscriber pattern

1: singleton mode ensures unified global login status and avoids repeated calls to the login information in the cache. If you need to use the login information, you only need to read the data in the memory of the singleton 2: Publish subscriber mode to ensure the consistency of user operation actions. If the user needs login status for operation, and the user is not logged in, the action to be executed will be added to the subscriber queue. When the login status changes, publish the latest login status to carry out the consistency of user operationCopy the code
  • Concrete implementation code
import HTTP from 'http'; // The enclosed request method import env from'.. /env.js'// Environment variable import msgUtil from'.. /.. /utils/msgUtil.js'// singleton implementation of global unique prompt /** * login authorization management module, responsible for user registration/login/update token operations * manage authentication and authorization for multiple platforms in the background */exportdefault class loginManager { static instance; /** * [getInstance] * @method getInstance * @return {object} 
   */
  static getInstance() {
    if (false === this.instance instanceof this) {
      this.instance = new this;
    }
    return this.instance;
  }

  constructorThis. LoginCbs = {}; // Temporary callback method variable this.tmpLoginCb = null; // Cache tag this.tag ='LOGIN'This. ABCHttp = new HTTP({serverType:'ABC'}); . AccessToken = this.accesstoken =' '; . //token Updates the flag. This. CheckingToken =false; This.restoretokeninfo (); // Initialize user information this.restoreTokenInfo(); } /** * Check the current login status according to the token time * Login status check method */isLogined() {
    let ts = new Date().getTime() / 1000;
    let logined = false;
    if (this.accessToken && ts < this.atExpiredAt) {
      logined = true;
    }
    returnlogined; } // Specific methods to read and manipulate 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) {// The login successfully executed the callback params.success && params.success(res) // Save user information and handle this._processUserTokenInfo(res) after login. }elseParams.fail && params.fail(res)}}, fail: (res) => {params.fail && params.fail(res)}, complete: Params.com plete})} _processUserTokenInfo(res) {processUserTokenInfo(res) {if(! res || ! res.data || ! res.data.token)return; this.accessToken = res.data.token.access_token; . // Synchronize user information to storage this.savetokenInfo (); This.notifyloginstatus (); . } // Log outlogout(params = {}) { ...... // Clear the cached user information this.clearTokenInfo(); This.notifyloginstatus (); Params.success && params.success()} /** * Recover 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 the user information cache */clearTokenInfo() {
    this.accessToken = this.refreshToken = ' ';
    wx.removeStorage({
      key: 'access_token'}); . } /** * Register to listen for login status changes */ onLoginStatus(key, fn) {if(key && fn) this.loginCbs[key] = fn; } /** * cancel listening for login status change */ offLoginStatus(key) {if (key) delete this.loginCbs[key];
  }

  notifyLoginStatus() {
    let logined = this.isLogined();
    for (let key in this.loginCbs) {
      letfn = this.loginCbs[key]; Fn &&fn (logined)}} /** * addTmpLoginCb(fn) {this.tmplogincb = fn; }removeTmpLoginCb() {
    this.tmpLoginCb = ' '; } /** * Check whether token * force needs to be updated: Whether to 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(! Logined) {// Invalid login confirmation, prompt to re-log in this.cleartokenInfo ();if (force) {
            msgUtil.getInstance().showLoginPrompt();
          } else{... } } this.notifyLoginStatus(); }); }else {
      this.checkingToken = false; }} /** * Whether the current token information needs to be updated ** the token validity period is updated within 1 hour ** @returnWhether 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 informationif (res.data.access_token && res.data.refresh_token) {
          ......

          this.saveTokenInfo();

          

          cb && cb(true)}else {
          cb && cb(false)}}})}Copy the code

3. Cache module

Technical point: singleton LRU

Scenario: In order to reduce CND requests and reduce server pressure, some interfaces are cached using singleton mode to achieve global memory data sharing, and LRU algorithm processes cache logic. Two storage methods are distinguished: memory data and cache data are stored and the cache time is processed. Permanent cache and time-limited cache adopt timer to periodically clear expired cache dataCopy the code
/**
 * 二级数据缓存管理
 * 1. 仅存储在内存
 * 2. 同时存放至内存和storage, 持久化保持
 */

//  最大缓存数据量
const MAX_LEN = 250;

exportDefault Class cacheManager {/** * [instance Current instance] * @type{this} */ static instance; /** * [getInstance] * @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) // Periodically clear expired data in the memory to avoid excessive memory usagesetInterval(() => { 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)} /** ** Keeps data to memory, does not persist * @param duration Duration, seconds. When the validity period expires, the device is automatically cleared. -1 indicates that the device is not cleared. * /setData(key, d, duration = -1) {
    if(key) { this.data[key] = { requestTime: parseInt(new Date().getTime() / 1000), duration, data: d } this.sortKey(key); * @param duration Specifies the duration for which data is stored in memory and storage. When the validity period expires, the device is automatically cleared. -1 indicates that the device is not 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) {
    letindex = this.keys.indexOf(key); // Put the hotkey at the end of the queueif (index >= 0) {
      let array = this.keys.splice(index, 1);
      this.keys.push(array[0]);
    } else{ this.keys.push(key); } // Delete the least frequently used data in the header when the cache number is exceededif (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(); } /** ** Obtain cached data within a specified period * @param key * @param duration Validity period, in secondsreturn{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 ' '; }}Copy the code

4. Interface request module

Technical point: Singleton pattern

Unified processing of interface request, combined with cache module, request interception, no sense of UI layer, reduce the number of interface requests compatible with callback and PROMISE two writing methods take GET without special request header as an example:Copy the code
  • Concrete 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 * can be broken down by business module */exportDefault class ecDataRequest {/** * [instance current instance] * @type{this} */ static instance; /** * [getInstance] * @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'}); } @method getDataTimestamp */ getDataTimestamp(params = {}) {// Set the stamp of the cachelet key = `${this.tag}_getDataTimestamp`; // Cache validity timelet duration = 60 * 60;
    returnNew Promise((resolve, rejects) => {const requsetRes = this.http. TryCachedData (params, key, duration, 1);if (requsetRes) {
        resolve(requsetRes);
        return; }; This.http. GET({url: env.serverImageAPI + 'interface address', SUCCESS: (res) = > {/ / request successfully set/update cache cacheMgr setPersistanceData (key, res, duration). resolve(res) }, fail: (res) => { rejects(res) } }) }) } }Copy the code

For specific business pages

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 loginif(loginMgr.islogined ()){this.requestData()}elseApp.loginifneed ((islogin)=> {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 => {})
    }
  },

})
Copy the code

loginIfNeed

The global unique access to the login page entry method uses the cache module to place the callback into memory after successful loginCopy the code
LoginIfNeed: loginIfNeed:function(complete) {
    loginMgr.removeTmpLoginCb();
    if (loginMgr.isLogined()) {
      complete && complete(true);
    } else {
      complete && loginMgr.addTmpLoginCb(complete);
      wx.navigateTo({
        url: 'Login page',}}}),Copy the code

Thank you for watching and I hope you can comment on it

For more personal learning summaries of native JS, check out star

Design pattern personal learning summary, click open surprise oh