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

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

历时两个多月的项目重构任务下个星期就要上线了,

利用周末时间写一下本次重构的一些总结。

本文是以程序项目" title="小程序项目">小程序项目为例展开的,

不过其思路其他的前端项目都可以借鉴。

重构中使用了单例模式工厂模式等一些设计模式及一些算法,

也算是对设计模式如何在开发中应用这个问题做出了解答。

.

├── 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.缓存模块

场景:为了减少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<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();

}

/**

* 获取特定时间内的缓存数据

* @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

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

以上是 【小程序】项目重构模块化封装总结(小程序项目为例) 的全部内容, 来源链接: utcz.com/a/103821.html

回到顶部