概念回顧
在掘金開發者大會上,在推薦實踐那裡,我有提到一種雲函式的用法,我們可以將相同的一些操作,比如使用者管理、支付邏輯,按照業務的相似性,歸類到一個雲函式裡,這樣比較方便管理、排查問題以及邏輯的共享。甚至如果你的小程式的後臺邏輯不復雜,請求量不是特別大,完全可以在雲函式裡面做一個單一的微服務,根據路由來處理任務。
用下面三幅圖可以概括,我們來回顧一下:
比如這裡就是傳統的雲函式用法,一個雲函式處理一個任務,高度解耦。
第二幅架構圖就是嘗試將請求歸類,一個雲函式處理某一類的請求,比如有專門負責處理使用者的,或者專門處理支付的雲函式。
最後一幅圖顯示這裡只有一個雲函式,雲函式裡有一個分派任務的路由管理,將不同的任務分配給不同的本地函式處理。
tcb-router
介紹及用法
為了方便大家試用,我們們騰訊雲 Tencent Cloud Base 團隊開發了 tcb-router,雲函式路由管理庫方便大家使用。
那具體怎麼使用 tcb-router
去實現上面提到的架構呢?下面我會逐一舉例子。
架構一:一個雲函式處理一個任務
這種架構下,其實不需要用到 tcb-router
,像普通那樣寫好雲函式,然後在小程式端呼叫就可以了。
- 雲函式
1234567// 函式 routerexports.main = (event, context) => {return {code: 0,message: 'success'};}; - 小程式端
1234567891011wx.cloud.callFunction({name: 'router',data: {name: 'tcb',company: 'Tencent'}}).then((res) => {console.log(res);}).catch((e) => {console.log(e);});
架構二: 按請求給雲函式歸類
此類架構就是將相似的請求歸類到同一個雲函式處理,比如可以分為使用者管理、支付等等的雲函式。
- 雲函式
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061// 函式 userconst 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();};// 函式 payconst 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();}; - 小程式端
123456789101112131415161718192021222324252627// 註冊使用者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);});
架構三: 由一個雲函式處理所有服務
- 雲函式
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152// 函式 routerconst 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();}; - 小程式端
123456789101112131415161718192021222324252627// 註冊使用者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 的中介軟體機制實現雲函式的路由管理
小程式·雲開發的雲函式目前更推薦 async/await
的玩法來處理非同步操作,因此這裡也參考了同樣是基於 async/await
的 Koa2 的中介軟體實現機制。
從上面的一些例子我們可以看出,主要是通過 use
和 router
兩種方法傳入路由以及相關處理的中介軟體。
use
只能傳入一箇中介軟體,路由也只能是字串,通常用於 use 一些所有路由都得使用的中介軟體
1 2 3 4 5 6 7 8 |
// 不寫路由表示該中介軟體應用於所有的路由 app.use(async (ctx, next) => { }); app.use('router', async (ctx, next) => { }); |
router
可以傳一個或多箇中介軟體,路由也可以傳入一個或者多個。
1 2 3 4 5 6 7 8 9 10 11 |
app.router('router', async (ctx, next) => { }); app.router(['router', 'timer'], async (ctx, next) => { await next(); }, async (ctx, next) => { await next(); }, async (ctx, next) => { }); |
不過,無論是 use
還是 router
,都只是將路由和中介軟體資訊,通過 _addMiddleware
和 _addRoute
兩個方法,錄入到 _routerMiddlewares
該物件中,用於後續呼叫 serve
的時候,層層去執行中介軟體。
最重要的執行中介軟體邏輯,則是在 serve
和 compose
兩個方法裡。
serve
裡主要的作用是做路由的匹配以及將中介軟體組合好之後,通過 compose
進行下一步的操作。比如以下這段節選的程式碼,其實是將匹配到的路由的中介軟體,以及 *
這個通配路由的中介軟體合併到一起,最後依次執行。
1 2 3 4 5 |
let middlewares = (_routerMiddlewares[url]) ? _routerMiddlewares[url].middlewares : []; // put * path middlewares on the queue head if (_routerMiddlewares['*']) { middlewares = [].concat(_routerMiddlewares['*'].middlewares, middlewares); } |
組合好中介軟體後,執行這一段,將中介軟體 compose
後並返回一個函式,傳入上下文 this
後,最後將 this.body
的值 resolve
,即一般在最後一箇中介軟體裡,通過對 ctx.body
的賦值,實現雲函式的對小程式端的返回:
1 2 3 4 5 6 7 |
const fn = compose(middlewares); return new Promise((resolve, reject) => { fn(this).then((res) => { resolve(this.body); }).catch(reject); }); |
那麼 compose
是怎麼組合好這些中介軟體的呢?這裡擷取部份程式碼進行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
function compose(middleware) { /** * ... 其它程式碼 */ return function (context, next) { // 這裡的 next,如果是在主流程裡,一般 next 都是空。 let index = -1; // 在這裡開始處理處理第一個中介軟體 return dispatch(0); // dispatch 是核心的方法,通過不斷地呼叫 dispatch 來處理所有的中介軟體 function dispatch(i) { if (i <= index) { return Promise.reject(new Error('next() called multiple times')); } index = i; // 獲取中介軟體函式 let handler = middleware[i]; // 處理完最後一箇中介軟體,返回 Proimse.resolve if (i === middleware.length) { handler = next; } if (!handler) { return Promise.resolve(); } try { // 在這裡不斷地呼叫 dispatch, 同時增加 i 的數值處理中介軟體 return Promise.resolve(handler(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } } } |
看完這裡的程式碼,其實有點疑惑,怎麼通過 Promise.resolve(handler(xxxx))
這樣的程式碼邏輯可以推進中介軟體的呼叫呢?
首先,我們知道,handler
其實就是一個 async function
,next
,就是 dispatch.bind(null, i + 1)
比如這個:
1 2 3 |
async (ctx, next) => { await next(); } |
而我們知道,dispatch
是返回一個 Promise.resolve
或者一個 Promise.reject
,因此在 async function
裡執行 await next()
,就相當於觸發下一個中介軟體的呼叫。
當 compose
完成後,還是會返回一個 function (context, next)
,於是就走到下面這個邏輯,執行 fn
並傳入上下文 this
後,再將在中介軟體中賦值的 this.body
resolve
出來,最終就成為雲函式數要返回的值。
1 2 3 4 5 6 7 |
const fn = compose(middlewares); return new Promise((resolve, reject) => { fn(this).then((res) => { resolve(this.body); }).catch(reject); }); |
看到 Promise.resolve
一個 async function
,許多人都會很困惑。其實撇除 next
這個往下呼叫中介軟體的邏輯,我們可以很好地將邏輯簡化成下面這段示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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 |