小程序云開(kāi)發(fā)教程之云函數(shù)路由高級(jí)玩法
在掘金開(kāi)發(fā)者大會(huì)上,在推薦實(shí)踐那里,我有提到一種云函數(shù)的用法,我們可以將相同的一些操作,比如用戶管理、支付邏輯,按照業(yè)務(wù)的相似性,歸類到一個(gè)云函數(shù)里,這樣比較方便管理、排查問(wèn)題以及邏輯的共享。甚至如果你的小程序的后臺(tái)邏輯不復(fù)雜,請(qǐng)求量不是特別大,完全可以在云函數(shù)里面做一個(gè)單一的微服務(wù),根據(jù)路由來(lái)處理任務(wù)。
tcb-router 介紹及用法
為了方便大家試用,咱們騰訊云 Tencent Cloud Base 團(tuán)隊(duì)開(kāi)發(fā)了 tcb-router,云函數(shù)路由管理庫(kù)方便大家使用。
那具體怎么使用 tcb-router 去實(shí)現(xiàn)上面提到的架構(gòu)呢?下面我會(huì)逐一舉例子。
架構(gòu)一:一個(gè)云函數(shù)處理一個(gè)任務(wù)
這種架構(gòu)下,其實(shí)不需要用到 tcb-router,像普通那樣寫好云函數(shù),然后在小程序端調(diào)用就可以了。
- 云函數(shù)
// 函數(shù) router exports.main = (event, context) => { return { code: 0, message: 'success' }; };
- 小程序端
wx.cloud.callFunction({ name: 'router', data: { name: 'tcb', company: 'Tencent' } }).then((res) => { console.log(res); }).catch((e) => { console.log(e); });
架構(gòu)二: 按請(qǐng)求給云函數(shù)歸類
此類架構(gòu)就是將相似的請(qǐng)求歸類到同一個(gè)云函數(shù)處理,比如可以分為用戶管理、支付等等的云函數(shù)。
- 云函數(shù)
// 函數(shù) user const TcbRouter = require('tcb-router'); exports.main = async (event, context) => { const app = new TcbRouter({ event }); app.router('register', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'register success' } }); app.router('login', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'login success' } }); return app.serve(); }; // 函數(shù) pay const TcbRouter = require('tcb-router'); exports.main = async (event, context) => { const app = new TcbRouter({ event }); app.router('makeOrder', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'make order success' } }); app.router('pay', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'pay success' } }); return app.serve(); };
- 小程序端
// 注冊(cè)用戶 wx.cloud.callFunction({ name: 'user', data: { $url: 'register', name: 'tcb', password: '09876' } }).then((res) => { console.log(res); }).catch((e) => { console.log(e); }); // 下單商品 wx.cloud.callFunction({ name: 'pay', data: { $url: 'makeOrder', id: 'xxxx', amount: '3' } }).then((res) => { console.log(res); }).catch((e) => { console.log(e); });
架構(gòu)三: 由一個(gè)云函數(shù)處理所有服務(wù)
- 云函數(shù)
// 函數(shù) router const TcbRouter = require('tcb-router'); exports.main = async (event, context) => { const app = new TcbRouter({ event }); app.router('user/register', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'register success' } }); app.router('user/login', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'login success' } }); app.router('pay/makeOrder', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'make order success' } }); app.router('pay/pay', async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx) => { ctx.body = { code: 0, message: 'pay success' } }); return app.serve(); };
- 小程序端
// 注冊(cè)用戶 wx.cloud.callFunction({ name: 'router', data: { $url: 'user/register', name: 'tcb', password: '09876' } }).then((res) => { console.log(res); }).catch((e) => { console.log(e); }); // 下單商品 wx.cloud.callFunction({ name: 'router', data: { $url: 'pay/makeOrder', id: 'xxxx', amount: '3' } }).then((res) => { console.log(res); }).catch((e) => { console.log(e); });
借鑒 Koa2 的中間件機(jī)制實(shí)現(xiàn)云函數(shù)的路由管理
小程序·云開(kāi)發(fā)的云函數(shù)目前更推薦 async/await 的玩法來(lái)處理異步操作,因此這里也參考了同樣是基于 async/await 的 Koa2 的中間件實(shí)現(xiàn)機(jī)制。
從上面的一些例子我們可以看出,主要是通過(guò) use 和 router 兩種方法傳入路由以及相關(guān)處理的中間件。
use 只能傳入一個(gè)中間件,路由也只能是字符串,通常用于 use 一些所有路由都得使用的中間件
// 不寫路由表示該中間件應(yīng)用于所有的路由 app.use(async (ctx, next) => { }); app.use('router', async (ctx, next) => { });
router 可以傳一個(gè)或多個(gè)中間件,路由也可以傳入一個(gè)或者多個(gè)。
app.router('router', async (ctx, next) => { }); app.router(['router', 'timer'], async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx, next) => { });
不過(guò),無(wú)論是 use 還是 router,都只是將路由和中間件信息,通過(guò) _addMiddleware 和 _addRoute 兩個(gè)方法,錄入到 _routerMiddlewares 該對(duì)象中,用于后續(xù)調(diào)用 serve 的時(shí)候,層層去執(zhí)行中間件。
最重要的運(yùn)行中間件邏輯,則是在 serve 和 compose 兩個(gè)方法里。
serve 里主要的作用是做路由的匹配以及將中間件組合好之后,通過(guò) compose 進(jìn)行下一步的操作。比如以下這段節(jié)選的代碼,其實(shí)是將匹配到的路由的中間件,以及 * 這個(gè)通配路由的中間件合并到一起,最后依次執(zhí)行。
let middlewares = (_routerMiddlewares[url]) ? _routerMiddlewares[url].middlewares : []; // put * path middlewares on the queue head if (_routerMiddlewares['*']) { middlewares = [].concat(_routerMiddlewares['*'].middlewares, middlewares); }
組合好中間件后,執(zhí)行這一段,將中間件 compose 后并返回一個(gè)函數(shù),傳入上下文 this 后,最后將 this.body 的值 resolve,即一般在最后一個(gè)中間件里,通過(guò)對(duì) ctx.body 的賦值,實(shí)現(xiàn)云函數(shù)的對(duì)小程序端的返回:
const fn = compose(middlewares); return new Promise((resolve, reject) => { fn(this).then((res) => { resolve(this.body); }).catch(reject); });
那么 compose 是怎么組合好這些中間件的呢?這里截取部份代碼進(jìn)行分析
function compose(middleware) { /** * ... 其它代碼 */ return function (context, next) { // 這里的 next,如果是在主流程里,一般 next 都是空。 let index = -1; // 在這里開(kāi)始處理處理第一個(gè)中間件 return dispatch(0); // dispatch 是核心的方法,通過(guò)不斷地調(diào)用 dispatch 來(lái)處理所有的中間件 function dispatch(i) { if (i <= index) { return Promise.reject(new Error('next() called multiple times')); } index = i; // 獲取中間件函數(shù) let handler = middleware[i]; // 處理完最后一個(gè)中間件,返回 Proimse.resolve if (i === middleware.length) { handler = next; } if (!handler) { return Promise.resolve(); } try { // 在這里不斷地調(diào)用 dispatch, 同時(shí)增加 i 的數(shù)值處理中間件 return Promise.resolve(handler(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } } }
看完這里的代碼,其實(shí)有點(diǎn)疑惑,怎么通過(guò) Promise.resolve(handler(xxxx)) 這樣的代碼邏輯可以推進(jìn)中間件的調(diào)用呢?
首先,我們知道,handler 其實(shí)就是一個(gè) async function,next,就是 dispatch.bind(null, i + 1) 比如這個(gè):
async (ctx, next) => { await next(); }
而我們知道,dispatch 是返回一個(gè) Promise.resolve 或者一個(gè) Promise.reject,因此在 async function 里執(zhí)行 await next(),就相當(dāng)于觸發(fā)下一個(gè)中間件的調(diào)用。
當(dāng) compose 完成后,還是會(huì)返回一個(gè) function (context, next),于是就走到下面這個(gè)邏輯,執(zhí)行 fn 并傳入上下文 this 后,再將在中間件中賦值的 this.body resolve 出來(lái),最終就成為云函數(shù)數(shù)要返回的值。
const fn = compose(middlewares); return new Promise((resolve, reject) => { fn(this).then((res) => { resolve(this.body); }).catch(reject); });
看到 Promise.resolve 一個(gè) async function,許多人都會(huì)很困惑。其實(shí)撇除 next 這個(gè)往下調(diào)用中間件的邏輯,我們可以很好地將邏輯簡(jiǎn)化成下面這段示例:
let a = async () => { console.log(1); }; let b = async () => { console.log(2); return 3; }; let fn = async () => { await a(); return b(); }; Promise.resolve(fn()).then((res) => { console.log(res); }); // 輸出 // 1 // 2 // 3
第二部分:如何開(kāi)通一個(gè)小商店