历时两个多月的项目重构任务下个星期就要上线了,
利用周末时间写一下本次重构的一些总结。
本文是以小程序项目为例展开的,
不过其思路其他的前端项目都可以借鉴。
重构中使用了单例模式工厂模式等一些设计模式及一些算法,
也算是对设计模式如何在开发中应用这个问题做出了解答。
项目主要目录
.
├── 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.请求方法封装模块
技术点:工厂模式
因为不同的域名接口请求头数据不同,因此在此模块中进行区分封装,
使用工厂模式,方便接口请求模块只关心 接口调用,不在重复处理请求头相关逻辑
下面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: '登录页',
})
}
},
感谢您的观看,希望大佬点评指教