项目重构模块化封装总结(小程序项目为例)

历时两个多月的项目重构任务下个星期就要上线了,
利用周末时间写一下本次重构的一些总结。
本文是以小程序项目为例展开的,
不过其思路其他的前端项目都可以借鉴。

重构中使用了单例模式工厂模式等一些设计模式及一些算法,
也算是对设计模式如何在开发中应用这个问题做出了解答。
项目主要目录
.
├── app.js
├── app.json
├── app.wxss
├── pages
├── service
│   ├── const.js
│   ├── env.js
│   └── http
│       ├── appDataRequest.js
│       ├── cacheManager.js
│       ├── http.js
│       └── loginManager.js
└── utils
    └── utils.js
...

一共四个模块,进行相互调用工作
  1. 请求方法封装模块
  2. 登录模块
  3. 缓存模块
  4. 接口请求模块

1.请求方法封装模块

技术点:工厂模式
因为不同的域名接口请求头数据不同,因此在此模块中进行区分封装,
使用工厂模式,方便接口请求模块只关心 接口调用,不在重复处理请求头相关逻辑

下面demo中具体区分了:
post 和 get 请求需要请求头和不需要请求头的四种方法
  • 具体实现代码
import env from '../../service/env.js' // 环境变量/域名地址
import loginManager from '../../service/http/loginManager.js' //登陆模块
import msgUtil from '../../utils/msgUtil.js' // 单例实现的弹窗提示
import cacheManager from '../../service/http/cacheManager.js' // 缓存模块


/**
 * 封装微信request请求,负责接口通用参数的装配
 * 根据serverType不同, 为不同的服务后台装配授权信息/校验信息等...
 * 对服务端返回的错误信息, 统一处理限流/登陆失效等错误
 */
export default class http {

  constructor(params) {
    //服务器类型, A or B or C
    //不同的服务后台, 有不同的token信息与参数校验方法
    this.serverType = params.serverType || 'A';
  }

  /**
   * 尝试访问缓存信息, 如可用,直接完成请求
   * @method tryCachedData
   * @return {bool}  true or false: true表示已使用缓存
   * @param type 区分回调方式和Promise请求
   */
  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;
  }

  /**
   * 无需token授权的http header, 根据serverType设置相关字段
   * @method getHeader
   * @return {object} http header信息
   */
  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请求封装, 无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请求封装,无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);
      }
    })
  }

  /**
   * 需token授权的http header, 根据serverType设置对于的授权参数
   * @method getHeaderWithToken
   * @return {object} http header信息
   */
  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请求封装, 带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请求封装, 带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);
      }
    })
  }

  

  /**
   * 限流处理
   * @method handleTrafficLimit
   * @return {bool} 是否已处理
   */
  handleTrafficLimit(res = {}) {
    if (res.statusCode == 503 || (res.header && res.header['Ec-Over-Limit'] == 503)) {
      msgUtil.getInstance().showTrafficLimitMsg();
      return true;
    }

    return false;
  }

  /**
   * 
   * token失效处理
   * @method handleTokenError
   * @return {bool} 是否已处理
   */
  handleTokenError(res = {}) {
    if (res.statusCode == 401 || (res.data && res.data.result == 10000)) {
      loginManager.getInstance().checkTokenInfo(true);
      return true;
    }

    return false;
  }
}

2.登录模块

技术点:单例模式 发布订阅者模式
1:单例模式
保证全局登陆状态统一,避免重复调用缓存中的登陆信息,如需使用登陆信息只需要读取单例中内存中的数据
2:发布订阅者模式
保证用户操作动作连贯性,如果用户操作需要用到登陆状态,且现在未登录时,
将需要执行的动作加入订阅者队列,当登陆状态发生改变  发布最新的登陆状态,进行用户连贯性操作
  • 具体实现代码
import HTTP from 'http';  // 封装的请求方法
import env from '../env.js' // 环境变量
import msgUtil from '../../utils/msgUtil.js' // 单例实现的全局唯一的提示

/**
 * 登录授权管理模块, 负责用户注册/登录/更新token的操作
 * 管理对后台多个平台的认证授权
 */
export default class loginManager {

  static instance;

  /**
   * [getInstance 获取单例]
   * @method getInstance
   * @return {object} 
   */
  static getInstance() {
    if (false === this.instance instanceof this) {
      this.instance = new this;
    }
    return this.instance;
  }

  constructor() {
    // 登录监听函数注册
    this.loginCbs = {};
    // 临时回调方法变量
    this.tmpLoginCb = null;
    // 缓存tag
    this.tag = 'LOGIN'
    
    // 不同接口域名请求方法实例化
    this.ABCHttp = new HTTP({
      serverType: 'ABC'
    });
    .....
    
    //登陆后的一些信息
    this.accessToken = '';
    .....
    
    //token更新flag 本次业务需要,和架构思路无关
    this.checkingToken = false;
    
    // 初始化用户信息
    this.restoreTokenInfo();

  }

  /**
   * 根据token时间判断当前登录状态
   * 登陆状态判断方法
   */
  isLogined() {
    let ts = new Date().getTime() / 1000;
    let logined = false;
    if (this.accessToken && ts < this.atExpiredAt) {
      logined = true;
    }
    return logined;
  }
  // 具体的读取和操作用户信息的方法    
  ......

  /**
   * ABC登录
   * }
   */
   doLogin(params = {}) {
    this.ecHttp.POST({
      url: `登陆请求地址`,
      data: params.data,
      success: (res) => {
        if (res && res.data && res.data.result == 0 && res.data.token) {
          // 登陆成功执行回调
          params.success && params.success(res)
          // 登陆成功后保存用户信息及对应处理
          this._processUserTokenInfo(res);

        } else {
          // 登陆失败的回调
          params.fail && params.fail(res)
        }
      },
      fail: (res) => {
        params.fail && params.fail(res)
      },
      complete: params.complete
    })
  }
  // 解析登录/注册后的用户登陆的信息等
  _processUserTokenInfo(res) {
    if (!res || !res.data || !res.data.token) return;

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

    // 同步用户信息到storage
    this.saveTokenInfo();
    

    //通知成功登录状态
    this.notifyLoginStatus();
    
    ......
  }

  // 退出登录
  logout(params = {}) {
    ......
    // 清楚缓存的用户信息
    this.clearTokenInfo();
    // 执行订阅的方法 告知已经退出登陆
    this.notifyLoginStatus();

    params.success && params.success()

  }

  /**
   * 从storage恢复用户信息
   */
  restoreTokenInfo() {
    this.accessToken = wx.getStorageSync('access_token');
    ......
  }
  /**
   * 保存用户信息至storage
   */
  saveTokenInfo() {
    wx.setStorage({
      key: 'access_token',
      data: this.accessToken,
    })
    ......
  }
  /**
   * 清除用户信息缓存
   */
  clearTokenInfo() {
    this.accessToken = this.refreshToken = '';
    wx.removeStorage({
      key: 'access_token',
    });
    ......
  }

  /**
   * 注册监听登录状态变化
   * 必须与offLoginStatus配合使用
   */
  onLoginStatus(key, fn) {
    if (key && fn) this.loginCbs[key] = fn;
  }

  /**
   * 取消监听登录状态变化
   */
  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)
    }
  }

  /**
   * 调用app.loginIfNeed时设置的临时回调函数
   */
  addTmpLoginCb(fn) {
    this.tmpLoginCb = fn;
  }

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

  /**
   * 检查是否需要更新token
   * force: 是否强制更新
   */
  checkTokenInfo(force = false) {
    if (this.checkingToken) return;

    this.checkingToken = true;

    //check token
    if (force || this._shouldRefreshToken()) {
      this._refreshToken((logined) => {
        this.checkingToken = false;
        if (!logined) { //登录确认无效, 提示重新登录
          this.clearTokenInfo();
          if (force) {
            msgUtil.getInstance().showLoginPrompt();
          } else {
            ....
          }
        }

        this.notifyLoginStatus();

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

  }

  /**
   * 当前token信息是否需要更新token
   * 有效时间1小时内则更新
   * 
   * @return 是否需要刷新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: '请求刷新',
      data: {
        refreshToken
      },
      success: (res) => {
        //处理新的token信息
        if (res.data.access_token && res.data.refresh_token) {
          ......

          this.saveTokenInfo();

          

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

3.缓存模块

技术点:单例模式 LRU
场景:为了减少cnd请求,减少服务器压力,对一些接口进行缓存处理
使用单例模式实现内存数据全局共享,LRU算法处理缓存逻辑;

区分了两种存储方式:
存储内存数据和存储缓存数据
同时对缓存时间做了处理,区分了永久缓存和限时缓存
采用定时器进行定时清除过期了的缓存数据
/**
 * 二级数据缓存管理
 * 1. 仅存储在内存
 * 2. 同时存放至内存和storage, 持久化保持
 */

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

export default class cacheManager {
  /**
   * [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)
    //定时清理内存中过期数据, 避免内存使用过多
    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)

  }

  /**
   * 保持数据至内存中,不做持久化处理
   * @param duration 有效时间,秒。 超过有效时间会被自动清除, -1代表不清除。
   */
  setData(key, d, duration = -1) {
    if (key) {
      this.data[key] = {
        requestTime: parseInt(new Date().getTime() / 1000),
        duration,
        data: d
      }
      this.sortKey(key);
    }
  }

  /**
   * 保持数据至内存和storage中, 持久化保存
   * @param duration 有效时间,秒。 超过有效时间会被自动清除, -1代表不清除。
   */
  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);
    // 热键放至队列尾部
    if (index >= 0) {
      let array = this.keys.splice(index, 1);
      this.keys.push(array[0]);
    } else {
      this.keys.push(key);
    }
    // 超出缓存数量,删除头部最不常用的数据
    if (this.keys.length > MAX_LEN) {
      let keys = this.keys.splice(0, this.keys.length - MAX_LEN)
      for (let i=0; i= 0) this.keys.splice(index, 1);
  }

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

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

  /**
   * 获取特定时间内的缓存数据
   * @param key
   * @param duration  有效时间, 秒. -1表示不检查有效时间
   * @return {Object} 缓存数据
   */
  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. 接口请求模块

技术点:单例模式
接口请求统一处理,结合缓存模块,进行请求拦截,ui层无感,减少接口请求次数
兼容回调和promise两种写法
以get不需要特殊请求头的请求为例:
  • 具体代码实现
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服务API
 * 可根据业务模块,更细的划分
 */
export default class ecDataRequest {
  /**
   * [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 = {}) {
    // 设置缓存标识
    let key = `${this.tag}_getDataTimestamp`;
    //缓存有效时间
    let duration = 60 * 60;
    return new Promise((resolve, rejects) => {
      // 拦截请求读取缓存
      const requsetRes = this.http.tryCachedData(params, key, duration, 1);
      if (requsetRes) {
        resolve(requsetRes);
        return;
      };
      this.http.GET({
        url: env.ServerImageAPI + `接口地址`,
        success: (res) => {
          // 请求成功设置/更新缓存
          cacheMgr.setPersistanceData(key, res, duration);
          resolve(res)
        },
        fail: (res) => {
          rejects(res)
        }
      })
    })
  }
}

具体业务页面使用

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) {
   // 验证是否登陆
   if(loginMgr.isLogined()){
      // 具体业务操作
      this.requestData()
   }else {
       // 见下方具体实现
       app.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 => {})
    }
  },

})

loginIfNeed

全局唯一进入登录页面入口方法
利用缓存模块将回调放入内存中
登陆成功后执行
// 封装登录判断,还未登录则完成登录
  loginIfNeed: function(complete) {
    loginMgr.removeTmpLoginCb();
    if (loginMgr.isLogined()) {
      complete && complete(true);
    } else {
      complete && loginMgr.addTmpLoginCb(complete);
      wx.navigateTo({
        url: '登录页',
      })
    }
  },

感谢您的观看,希望大佬点评指教

更多原生js的个人学习总结欢迎查看 star

设计模式个人学习总结,点开有惊喜哦

你可能感兴趣的:(前端,javascript,es6,小程序,前端架构)