- 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)装饰模式[图片] 可能有朋友会好奇装饰器和我们接下来要做的原生函数扩展有什么关系? 先看下装饰模式的定义:在不改变原函数的基础上,附加一些额外的功能 方便更好的理解 创建一个函数,然后将存在originF这个变量里,并对它重新赋值,最后执行,最后的执行直接结果是什么? hello world!! 这样做的好处是什么,我们对函数f添加了一个新的功能,但是我们没有对原函数进行任何的修改 那么对于小程序的应用 照猫画虎一下,同样的将小程序原生的Page函数存起来,再重新赋值,看下结果,发现Page执行的时候每次都会console这句话,我这里有两个未分包的页面,就执行了2次 [图片] 回归到刚开始需要解决的问题,“多个页面pv,uv如何监控,不可能每个页面都要手动收集”, pv怎么算,pv的意思是页面浏览量,也就是需要页面生成时,对应到小程序生命周期就是onLoad 刚才我们做到了每个Page执行时添加功能,但是怎么在onLoad时进行数据统计呢? 同样的可以用装饰函数修饰page里面的函数方法 [图1] 这个data就是我们实际在pages下写的业务逻辑对象,我们先拿到该页面的key名来进行遍历,首先排除掉非函数,拿到onLoad函数,这时候对它进行扩展,这时候每一个页面执行onLoad的时候都会console一次字符串,当然也可以替换为你想要的任何功能 [图2] 其实我们可以再优化一下,比如抽出一个对象,将你想要装饰的函数写入其中,如果原函数存在则进行装饰,如果不存在则直接赋值,这个抽出来的对象其实可以算是另一种继承方式的实现 它的意义在于[图片] 帮助我们开辟了一块公有空间,帮助我们扩展Page对象,并且可以劫持任意方法,在不修改原业务函数代码的情况下增加新的逻辑 其次也是最重要的是,我们完全不需要处理历史包袱 一个应用场景[图片] 左边是微信原生的分享方法 遇到的一些问题: 不便于扩展,多个分享策略时,代码块过大,并且分享的通用数据需要每个页面单独处理,例如统计回流信息需要的分享页面信息,事件信息 右边是依赖于我们刚刚开辟的公有空间里填写的公共方法,函数很简单,获得参数,获得页面分享策略,执行,顺便还做了obj转url的处理,避免了纯url书写长路径时的尴尬 思考[图片] 既然能扩展Page对象,那么App,Component甚至wx全局对象下的方法呢? 有兴趣的朋友可以下来想一下 跨页面 / 多页面事件通知[图片] 我这边简单提一下跨页面通知的问题,这个应该算是很多小程序开发者遇到的通用问题,我问过的一些人,大部分是使用下面这两种方法,一种是getCurrentPages 页面栈队列,二种onShow upData global里的存储的数据,不管哪一种在业务复杂的情况下都会引起一定的问题,比如第一种的多级入口,第二种的话属于无效判断和及时性 小打卡这边用的简版的event Bus,一个只有80行代码的订阅方法, 左图是一个简单的示例,大致上就是分两种角色,订阅者和发布者,中间依靠任务队列联系,每次发布者推送消息,订阅者都会收到通知和相应的数据 [图片] 其实单纯的event bus也有很多的问题,比如:业务复杂情况下过于频繁,对业务代码侵入频繁,可以想像一下到处都是on,off, emit场景 解绑需要树立规范,但是人总是会犯错,容易绑定后忘记解绑或重复绑定的问题,比较浪费内存,对性能也有消耗 结合公共空间我们已知可以对生命周期进行扩展的时候怎么去解决这个问题 其实订阅必然是和页面结合,因为在页面不存在的情况下,发送通知也不会有反馈, 如何证明页面存在自然是onLoad 和 onUnload 按照这个逻辑我们只需要在onLoad和onShow做些处理即可 [图1] 先看右侧业务代码,按照策略注册了一个函数集合,在执行onLoad时,自动将业务内的onRss函数遍历,自定绑定订阅,并推到一个缓存数组内 onUnload时遍历缓存,自动解绑 这时订阅与页面的生命周期强绑定,我们不再需要处理解绑事件,也不需要在业务内插入订阅代码,只需要管理好当前页面的订阅策略即可 技术选型[图片] 没有哪一种一定正确的方案,在选型的时候可能需要考虑到上面大致6种元素, 总之选择最适合自己团队方案非常重要 一些小贴士:开发要尽量避免过度设计, 应根据实际需求, 比如之前在写现在这个简版的event bus库的时候设计了很多奇奇怪怪的 功能,现在2年过去还没用到 小程序和浏览器,node本质上是相通的,可以多借鉴和参考 The End今天我的分享就到此结束,算是这段时间对小程序开发上的一点心得体会,谢谢 [图片]
2019-04-22 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)
[图片] 大家好,我是小打卡的前端负责人金轩正,今天分享的主题是如何基于微信原生构建应用级小程序底层架构,这个命题看上去好像有些大,不过不要紧,这次分享我把它拆一下,大致从 小程序原生开发面临的问题 小打卡整体架构演进 开发中摸索与实践 这三个方面来看这个讲一下 [图片] 小程序原生开发面临的问题[图片] ok,首先第一个方面原生开发遇到的问题 小程序从17年诞生2年来一直处于互联网风口,不过对于开发者而言的整个开发体验不是特别友好,在17-18年之间我和很多开发小程序的小伙伴们聊过,大多数的反馈可能分为下面大致几类,当然还有更多: 没有父类,无法使用继承挂载全局方法,扩展生命周期没有父类,无法使用继承挂载全局方法,扩展生命周期 不支持跨页面/多页面通讯 setData的性能瓶颈 代码包大小限制 1/2/4/8 M,没有npm包 代码发布流程繁琐 其根本原因是将刚刚诞生的小程序与已经非常成熟的React,vue,angular作对比,而没有将小程序作为一个新的生态来看待,当然这个是一种看待事物的进步,并不是倒退,我在这里说这句话的意思是有更多的问题需要我们开发者主动去解决问题,推动整个生态的前进与发展 [图片] 其实这里可能有些朋友会问,已经有很多优秀的框架已经解决了这些问题,那么为什么还要使用原生开发? 确实在这段时间内出现了很多优秀的解决方案,我们不用并不是因为情怀哈(当然还是有那么一丢丢) 更多的是下面几点: 历史包袱,改造成本过高 小打卡在小程序刚出现的时候就进入开发了,当时框架还不成熟,而且对创业公司来说时间和迭代效率高于一切,在人手不足,业务模式尚未形成,还处于探索阶段的情况下花费大量时间去做对产品影响较小, 甚至delay迭代速度事情不是很赚 减少与第三方沟通成本 高速迭代的情况下,将时间尽可能的覆盖于业务上,避免在整个开发-上线闭环上增加节点 避免开发黑盒,控制风险 虽然整个社区是非常活跃的,fixed一个问题同样是需要花费一定时间,但是很多时候需求是不会等你bug fixed 如非必要,勿增实体 即“简单有效原理”,这句话还是我去年刚来公司的时候和阿赖聊他所说过的 放在项目开发上我的理解是在架构层面要做的尽可能的薄,避免过度设计 这样才有足够的扩展性,灵活性,容错性 这些框架虽好,但是对我们当前业务来说可能过于复杂,比如跨端在之前的阶段还没有这方面需求,而像组件化小程序已经支持,自动化构建我们自己也是可以搭建的并不复杂 相信微信小程序团队 是真正的想把这件事情做好,而且做的是一个生态,不论是小程序对于反馈响应速度,和迭代速度非常给力,还是对开发者社区运营,比如是社区活跃与审核速度挂钩,社区周刊,优质个人和优质企业 对齐web标准,并且更加开放 [图片] 小打卡整体架构演进其实小打卡整个架构并非一蹴而就的,就像前面所说的如非必要,勿增实体,而是大量的实际开发中遇到的共同问题解决方案的集合题 [图片] 常规架构这个是微信小程序给出的快速开发模版的一个开发模式: server模块提供数据,App作为全局对象直连所有的业务模块,工具函数提供api处理业务模块的需求 优点: 整个模型非常简单,上手快,学习成本 低结构清晰,在业务不复杂的情况下可以快速开发 不瞒大家其实小打卡在最初的半年内基本都是这套模式。 当然是在业务不复杂的情况下,复杂情况下会出现哪些问题呢? App作为全局对象在有大量业务模块连接的情况下,代码很容易膨胀,在多人开发的时候问题非常明显,无论是fixed bug还是正常的业务开发都会造成麻烦 页面之间独立,缺少公共模块,唯一的工具函数又要尽可能保持单一职责来提供服务(小打卡当时就是因为这个问题导致很多工具函数内部存储直接修改外部状态,导致大量强耦函数合无法拆分) 业务层直连server层,未拆分数据层的情况下,基本不存在复用性 上面所述的问题,从我接手这个项目到真正的调整持续了挺长一段时间,主要是缺乏一个契机来进行优化 优化的转折点 [图片] 然后突然有一天产品同学跑过来说: 我们要有自己的核心数据仓库,我们要看实时数据 ok,涉及到数据采集的问题了,我这边从浅到深大概列了几项: 最基础的多个页面pv,uv如何监控,不可能每个页面都要手动收集 为了统计页面和事件的分享和回流的数据,需要在分享事件携带大量的参数 微信的wx.previewImage, wx.chooseImage 等api对于用户session的收集造成很大麻烦 我们先解决第一个问题,如何收集页面pv,uv 容易陷入的误区 [图片] 在解决问题之前,我们先说一下开发小程序容易进入的误区 App 和 Page 等函数工厂是微信原生提供,不可修改 小程序项目结构是基于App, Page, 工具函数三个模块构建的 小程序的全局存储只有globalData和本地缓存 其实产生这些误区最根本的原因是小程序没有提供在复杂业务逻辑下的开发范式,比如vue,react有自己的通用开发模版 如果保持这些观念来进行开发的话,很容易将路子走窄,并且难以解决一些实际上的问题, 其实不论小程序和传统web有多少不同, 本质上还是在js环境下开发 小打卡架构图解 [图片] 为了更好的方便理解后面的具体实现,我提前放了一张目前小打卡的架构图 首先很熟悉的server这一边垫了一个数据层,主要将数据层和业务层解耦,提高复用性,并且提供一些通用功能,比如返回格式化数据问题,参数校验,日志监控... 在App对象和业务层同样增加了一个全局模块,提供独立于业务和工具类,只提供api之间双向通讯的渠道 工具模块的话其实就是对业务层的增强,比如常见的请求模块,上传模块,路由拦截等等 业务模块的话基本除了增加Component和中间层外没有太大变化 这个图上可能有两块可能大家觉得比较怪异,一个是global里面的函数重载,还有一个是业务模块的中间层是什么? 函数重载其实就是修改微信提供的App, Page, Component函数,使其更符合我们的业务场景, 业务模块的中间层就是依赖于函数重载的扩展 其实小打卡的整套架构都是基于这两个模块,这两个模块赋予了更多的可能性,然而实现却十分的简单 点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
2019-04-22