ez-wxlite是一套小程序开发模板,旨在设计一套简洁、高效、可维护的开发框架。
本套模板总体上分为三部分:
client部分是框架的核心,设计上分为:
框架核心代码都包含在client/framework
文件夹内,在app.js中一次性引入:
// app.js
require('./framework/index.js').patch();
App({});
调用patch方法会直接完成App
、Page
、Component
这三个全局方法的代理,并完成相应的注入,所以上面的App({})
其实已经是被代理之后的App
,在这一实例中我们可以获取到注入的options
数据,通过this.$opts
获取到:
App({
onLaunch() {
console.log(this.$opts);
},
onShow() {
console.log(this.$opts);
},
});
原小程序的App
方法只能在onLaunch
中获取到options
,代理过后的App
,通过将options
挂载在实例上,我们可以在所有生命周期里访问到,方便使用,Page
同理。
config为全局配置信息,大家一定有过跨页面共享数据的需求,比如有十几个页面都是用的同一个转发标题、转发描述,以及转发图片,那么我们就可以将通用的转发信息shareData都保存在一个文件里,作为配置文件让所有页面都能访问:
// pages/index/index.js
const { config } = require('../../framework/index.js');
Page({
onShareAppMessage() {
return config.shareData;
},
});
以上,我们将通用的分享信息存放在config.shareData
里,这只是config
的一个示例用法。
另外我们也发现了,config是集成在client/framework/index.js
里的,事实上,之前我们提到的req、router、utils也都是集成在client/framework/index.js
里的,使用的时候不需要一个个单独引入,这么做的目的是减少模板代码,方便维护。
req是wx.request
的高级封装,用于发起ajax请求以及文件上传。
wx.request
是一个底层api,使用的不便之处在于:
- 返回结果比较底层,需要处理statusCode,而开发者往往更关注业务相关的data部分;
- 登录机制繁琐,设计上甚至有些反人类;
- 不具备良好的接口管理功能,可维护性差;
……
综上所述,wx.request
需要一层高级的封装来简化操作,因此有了req
,req代理了wx.request
,并在这基础上做了一些设计工作,以提供良好的维护性:
- promisify:支持promise,替代callback的方式;
- 简化respone:简化返回的数据信息,只保留业务数据;
- method替代url:使用js api的书写方式来替代直接书写url的方式;
- 接口缓存:支持便捷的接口前端缓存;
- 自动登录:登录态过期自动重新登录,过程对开发者透明。
涉及到ajax就避不开异步编程,谈到异步,怎么少得了promise,所以我们第一时间考虑将其promise化:
// url方式
wx.request({
url: 'https://api.jack-lo.com/ez-wxlite/user/getInfo?id=123',
success(res) {
console.log(res);
},
});
// method方式
req.user.getInfo({
id: '123',
})
.then((res) => {
console.log(res);
});
既然都已经promise了,你当然也可以方便地使用async/await。
为了更加通用,wx.request
的返回值包含了完整的respone
内容,但在大部分情况下,开发者关注的只有respone.data
部分,于是我们做了一层过滤,req的请求返回结果就是respone.data
,至于异常的statusCode(指的是除了(statusCode >= 200 && statusCode < 300) || statusCode === 304;
以外的情况),我们将它归为了fail
的范畴,也就是promise的catch通道。
我们还是以上面的示例代码来介绍,wx.request
返回的res
结构为:
{
"data": {
"name": "Jack",
"age": 18,
"gender": 1
},
"statusCode": 200,
"header": {}
}
如果我们想要读取data的内容,首先要判断statusCode
是否为“正常”的http状态码,也就是诸如200之类的,而如果是404,我们还得弹个窗报个错什么的:
// url方式
wx.request({
url: 'https://api.jack-lo.com/ez-wxlite/user/getInfo?id=123',
success(res) {
if (res.statusCode === 200) {
// 读取res.data
} else {
// 处理异常
}
},
fail(err) {
// 处理异常
},
});
一般来说,我们的res.data
里面还有业务层面的错误信息,这样的话,除了处理wx.request
fail的错误,以及success
里异常的statusCode
错误,我们还要再处理业务逻辑的res.data.code
(这里假设你的数据结构是{code: number, data: object, msg: string}
)错误。。。
真是丧心病狂。。。
而req.user.getInfo
返回的res
则仅为:
{
"code": 0,
"data": {
"name": "Jack",
"age": 18,
"gender": 1
},
"msg": "success"
}
只关注业务部分的json,而fail和“bad statusCode”则一概交给catch通道去处理:
// method方式
req.user.getInfo({
id: '123',
})
.then((res) => {
console.log(res);
if (res.code === 0) {
// 请求成功
} else {
// 请求失败
}
})
.catch((err) => {
console.log(err);
});
此时,err有可能是fail或者“bad statusCode”产生的,而这两种情况产生的err结构并不一样,如果你想弹窗显示错误信息,你可能需要对err
进行识别和提炼,为此我们内置了两个方法req.err.picker
和req.err.show
,前者用于提炼错误信息文本,请放心,req.err.picker
囊括了常见的error,能够很好地结合框架工作,而后者更方便,直接就是将error提炼并弹窗显示:
req.user.getInfo({
id: '123',
})
.then((res) => {
console.log(res);
if (res.code === 0) {
// 请求成功
} else {
// 请求失败
req.err.show(res.msg);
}
})
.catch((err) => {
req.err.show(err); // 弹窗显示错误信息
console.log(req.err.picker(res)); // 打印错误信息
});
直接使用wx.request
必然要面临手写url的问题,一方面书写不方便,另一方面难以维护。想象一下,一旦需要更换某个使用频率较高的接口,你可能要把每个调用的地方都修改一遍,而且由于url的拼接方式可能各不相同,使用find&replace功能可能会有纰漏。
req将接口进行统一管理,对外暴露的都是method式的api,我们举个栗子,比如获取用户信息:
// url方式
wx.request({
url: 'https://api.jack-lo.com/ez-wxlite/user/getInfo?id=123',
});
// method方式
req.user.getInfo({
id: '123',
});
看到这里你可能会说,咦,url跑哪去了?当然,url需要在某处被集中定义,进入client/req/api
目录,我们新建一个js文件user.js
,并做如下定义:
/**
* apiUrl是定义在config中的常量,
* 它是当前使用接口的前缀,
* 用于切换不同的环境
* 这里的示例值为 https://api.jack-lo.com/ez-wxlite
*/
const { apiUrl } = require('../../config/index.js');
module.exports = {
install(R, req) {
R.user = {
// 获取用户信息
getInfo(data) {
const url = `${apiUrl}/user/getInfo`;
return req({ url, data });
},
};
},
};
然后在client/req/index.js
中进行挂载,req使用的是类似插件的方式,插件需要实现一个install
方法,然后req使用use
api来进行安装:
const R = require('./prototype.js');
const userApi = require('./api/user.js');
R.use(userApi);
module.exports = R;
通过以上操作,我们将url转化为js api,这样的好处是方便调用和维护,同时,我们将接口做了一个归类,比如获取用户信息属于user
类,将来user
类也会继续添加其他一些接口,这样的接口更加的语义化,同时也起到命名空间的作用。
某些接口使用频率高但是变动又少,比如“获取当前用户的个人信息”、“获取省市区数据”,我们可以在前端通过缓存来提高性能,为此我们提供了如下几个api来控制接口缓存:
api | 参数 | 返回值 | 示例 | 描述 |
---|---|---|---|---|
req.cachify | [string]req api | [function]cachifyFn | req.cachify('user.getMyInfo')() | 调用接口并缓存数据 |
req.clearCache | [string]req api, [string]id(optional) | undefined | req.clearCache('user.getMyInfo') | 清除某个接口的缓存:接受两个参数,第一参数为req api 名,第二参数为id (选填),也就是接口的唯一标识,这一般用在分页接口,默认可不填 |
req.clearAllCache | [string]req api(optional) | undefined | req.clearAllCache('user.getMyInfo')() | 清除所有缓存:接受一个参数req api (选填),当传值时,清除指定api的缓存,不传则清除所有api的缓存 |
我们假设你已经定义好了 “获取当前用户的个人信息”这一接口req.user.getMyInfo
,我们要对这一接口进行调用后缓存,那么调用方式应该为:
req.cachify('user.getMyInfo')()
.then((res) => {
if (res.code === 0) {
// res.data
} else {
req.err.show(res.msg);
}
})
.catch((err) => {
req.err.show(err);
});
req.cachify
接受的第一参数为一个req api名,并返回一个函数,这个函数入参同被定义的req api一致。
那么,什么时候清除缓存?当我对我的个人信息进行编辑并提交成功以后,我就需要清除缓存,以便获取最新的数据,假设已经定义好的“更新我的信息接口”为req.user.updateMyInfo
:
req.user.updateMyInfo()
.then((res) => {
if (res.code === 0) {
req.clearCache('user.getMyInfo');
} else {
req.err.show(res.msg);
}
})
.catch((err) => {
req.err.show(err);
});
注意:接口缓存是基于已定义接口的前提下,没有定义的接口是无法直接使用
req.cachify
调用的。
简单来理解,小程序登录其实就是一个用code换取session_key的过程。
当session_key过期了,我重新用新code换取新的session_key,再去发请求。
那么,session_key过期我们怎么知道?有些开发者可能会用wx.checkSession
去定时检查,但是定多长的时间呢?不知道,因为微信不会把 session_key 的有效期告知开发者,而是根据用户使用小程序的行为对 session_key 进行续期,用户越频繁使用小程序,session_key 有效期越长,因此,定时刷新是个不好的实践,因为你把握不了时机,会造成资源浪费并且增加不确定性。
事实上,只有在需要跟微信(后端)接口打交道的时候,才需要有效的session_key,那么后端肯定知道什么时候过期了,因为微信后端会告诉我们,所以我们把过期的判断交给后端,只要后端被告知过期了,接口就返回一个固定的状态,比如code=3000
,前端收到这一状态之后,便重新走一遍登录流程,获取到新的session_key
,再重新发起请求。
大多数时候我们只停留在自己的业务里,并不需要跟微信打交道,我们可以约定自己的会话有效期,并且放宽一些,比如1天,只要是不需要跟微信打交道,这个时效性就会宽松的多,性能也会得到提高。
req的自动登录就是这么实现的,约定好登录过期状态(默认是res.code === 3000
,请根据实际情况自行修改),req会自动调用wx.login
重新获取js code
,再用js code
去调用登录接口换取新的sessionId
,最后再发起一遍上次的请求。
这让开发者可以更加专注在业务开发上,而不必关心登录过期的问题。
页面的跳转存在哪些问题呢?
- 与接口的调用一样面临url的管理问题;
- 参数类型单一,只支持string。
第一个问题很好解决,我们就跟req一样,做一个集中管理。
第二个问题的情况是,当我们传递的参数argument不是string
,而是number
或者boolean
时,也只能在下个页面得到一个argument.toString()
值:
// pages/index/index.js
wx.navigateTo({
url: '/pages/a/index?a=true',
});
// pages/a/index.js
Page({
onLoad(options) {
console.log(options.a); // => "true"
console.log(typeof options.a); // => "string"
console.log(options.a === true); // => false
},
});
上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。
我们的解决方案是:利用JSON.stringify+encodeURIComponent
和decodeURIComponent+JSON.parse
的方式让参数保真。
顺手也替换掉那不好记的navigate api
,于是就出现了如下方式:
// pages/pageA/index.js
const { router } = require('../../framework/index.js');
Page({
onReady() {
router.push({
name: 'home',
});
},
});
// pages/index/index.js
Page({
onLoad() {
console.log(this.$opts); // { id: '123', type: 1 }
},
});
当然,上面的name: 'home'
肯定也是事先配置好的,要不然鬼知道home到底跳转到哪里。
在client/route/routes.js
中我们可以看到:
module.exports = {
// 主页
home: {
type: 'tab',
path: '/pages/index/index',
},
};
很明显,home
其实就是/pages/index/index
的一个别名,同时因为它是一个tab页面,所以我们也顺便指定了type: 'tab'
,默认是type: 'page'
。
除了支持别名之外,name也支持直接寻址,比如跳转home还可以写成这样:
router.push({
name: 'index', // => /pages/index/index
});
router.push({
name: 'userCenter', // => /pages/user_center/index
});
router.push({
name: 'userCenter.phone', // => /pages/user_center/phone/index
});
router.push({
name: 'test.debug', // => /pages/test/debug/index
});
注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如
pages/index
下面会存放pages/index/index.js
、pages/index/index.wxml
、pages/index/index.wxss
、pages/index/index.json
,这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如pages/index/pageB/index.js
、pages/index/pageB/index.wxml
、pages/index/pageB/index.wxss
、pages/index/pageB/index.json
。
router支持微信路由的所有方法,映射关系如下:
router.push => wx.navigateTo
router.replace => wx.redirectTo
router.pop => wx.navigateBack
router.relaunch => wx.reLaunch
可能你会发现这里少了一个wx.switchTab
,这不是遗漏,而是被集成到了router.push
当中去了,因为我们认为,跳转一个页面到底是page
的方式还是tab
的方式这类事情,根本与业务无关,它应该被透明化。
你可能会记得上面我们的home
在路由配置的时候就已经指定了type: 'tab'
的属性,这样一来我们便可以尽管调用router.push({name: 'home'})
,至于具体是wx.navigateTo
还是wx.switchTab
,程序会自动帮我们处理的。
这里还有一个好处就是,当一个页面需要从tab
转变为page
的时候,我们只需要改一下routes
的定义就可以了,完全不需要去一个个修改业务中的代码。
(内容待补充)
由于精力有限,本应该在一开始就写好的文档,一直到现在都还只是陆陆续续在更新,感谢支持,如果有什么宝贵意见可以直接通过邮箱(jack-lo@foxmail.com)联系我,谢谢!