李成熙,騰訊云高級工程師。2014年度畢業加入騰訊AlloyTeam,先后負責過QQ群、花樣直播、騰訊文檔等項目。2018年加入騰訊云云開發團隊。專注于性能優化、工程化和小程序服務。微博 | 知乎 | Github
概念回顧
在(zai)掘金(jin)開發者大會上,在(zai)推薦實踐那里,我有提到一(yi)種云(yun)函數(shu)的用法,我們可以(yi)將(jiang)相同的一(yi)些操作,比如(ru)用戶管理(li)、支付邏(luo)(luo)輯,按照業(ye)務(wu)的相似(si)性,歸類到一(yi)個云(yun)函數(shu)里,這樣比較(jiao)方便(bian)管理(li)、排查問題(ti)以(yi)及邏(luo)(luo)輯的共(gong)享。甚(shen)至如(ru)果你(ni)的小程序的后臺邏(luo)(luo)輯不(bu)復雜,請求量不(bu)是特(te)別(bie)大,完全(quan)可以(yi)在(zai)云(yun)函數(shu)里面做一(yi)個單一(yi)的微服務(wu),根據路由(you)來處理(li)任務(wu)。
用下面三幅圖可(ke)以概括,我們來回顧一(yi)下:




比如(ru)這里(li)就是傳統的云函(han)數用法,一個云函(han)數處理一個任務,高度解(jie)耦。

第二(er)幅架構圖就是嘗試將請(qing)求歸(gui)類(lei),一個云函數處(chu)理某(mou)一類(lei)的(de)請(qing)求,比如有(you)專(zhuan)(zhuan)門負(fu)責處(chu)理用戶的(de),或者專(zhuan)(zhuan)門處(chu)理支(zhi)付的(de)云函數。

最后一(yi)幅圖顯示這里只有(you)一(yi)個云(yun)函數(shu),云(yun)函數(shu)里有(you)一(yi)個分派任務的(de)(de)路由管理,將不(bu)(bu)同的(de)(de)任務分配(pei)給不(bu)(bu)同的(de)(de)本地函數(shu)處理。
tcb-router 介紹及用法
為了(le)方便大家(jia)試用,咱們(men)騰訊云(yun) Tencent Cloud Base 團隊開發了(le) tcb-router,云(yun)函數路由(you)管理庫方便大家(jia)使用。
那具(ju)體怎(zen)么使(shi)用 tcb-router 去實現上面提到的架構呢(ni)?下面我會逐(zhu)一舉例子。
架構一:一個云函數處理一個任務
這種(zhong)架構下,其實不(bu)需要(yao)用到 tcb-router,像普(pu)通(tong)那樣寫好(hao)云函(han)數,然(ran)后在小程序端調用就可(ke)以了。
// 函數 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);
});
|
架構二: 按請求給云函數歸類
此類(lei)架構就是將相似的請求歸類(lei)到同一(yi)個云(yun)函數(shu)處理(li),比如(ru)可以分為(wei)用戶管理(li)、支付(fu)等(deng)等(deng)的云(yun)函數(shu)。
// 函數 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();
};
|
// 注冊用戶
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);
});
|
架構(gou)三: 由一個云(yun)函數(shu)處理所有服務(wu)
// 函數 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();
};
|
// 注冊用戶
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 的中間件機制實現云函數的路由管理
小程序·云(yun)開發的云(yun)函數目(mu)前更推薦(jian) async/await 的玩(wan)法來處理異(yi)步操作,因此這里也(ye)參考了同(tong)樣是基于 async/await 的 Koa2 的中間(jian)件實現(xian)機制。
從(cong)上面的(de)一些(xie)例(li)子我(wo)們可(ke)以看出,主要是通過 use 和 router 兩種(zhong)方法(fa)傳入(ru)路由以及(ji)相關(guan)處理的(de)中間件(jian)。
use 只能(neng)(neng)傳入一個中間件(jian),路由也只能(neng)(neng)是(shi)字(zi)符串,通常用于 use 一些所有路由都得(de)使用的中間件(jian)
// 不寫路由表示該中間件應用于所有的路由
app.use(async (ctx, next) => {
});
app.use('router', async (ctx, next) => {
});
router 可以傳一個或多個中間件,路由也可以傳入一個或者多個。
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,都只(zhi)是將路由和(he)中間件信息,通過 _addMiddleware 和(he) _addRoute 兩(liang)個方法,錄入到 _routerMiddlewares 該對象(xiang)中,用(yong)于后續(xu)調用(yong) serve 的時(shi)候(hou),層(ceng)層(ceng)去執行中間件。
最(zui)重要的運行中間件邏(luo)輯,則是(shi)在 serve 和 compose 兩個方法里。
serve 里主(zhu)要(yao)的(de)作用是做路(lu)由(you)的(de)匹配(pei)以及將中(zhong)間(jian)件(jian)組合好之后(hou)(hou),通(tong)(tong)過(guo) compose 進行下(xia)(xia)一步的(de)操(cao)作。比如以下(xia)(xia)這段(duan)節(jie)選(xuan)的(de)代碼,其實是將匹配(pei)到(dao)的(de)路(lu)由(you)的(de)中(zhong)間(jian)件(jian),以及 * 這個通(tong)(tong)配(pei)路(lu)由(you)的(de)中(zhong)間(jian)件(jian)合并到(dao)一起(qi),最(zui)后(hou)(hou)依次執(zhi)行。
let middlewares = (_routerMiddlewares[url]) ? _routerMiddlewares[url].middlewares : [];
// put * path middlewares on the queue head
if (_routerMiddlewares['*']) {
middlewares = [].concat(_routerMiddlewares['*'].middlewares, middlewares);
}
組合(he)好(hao)中間(jian)件后,執行這一(yi)(yi)段,將(jiang)中間(jian)件 compose 后并返(fan)回(hui)一(yi)(yi)個函數,傳(chuan)入(ru)上下文 this 后,最(zui)后將(jiang) this.body 的值(zhi) resolve,即一(yi)(yi)般(ban)在最(zui)后一(yi)(yi)個中間(jian)件里,通過對 ctx.body 的賦值(zhi),實現云函數的對小程(cheng)序端的返(fan)回(hui):
const fn = compose(middlewares);
return new Promise((resolve, reject) => {
fn(this).then((res) => {
resolve(this.body);
}).catch(reject);
});
那(nei)么 compose 是怎么組合(he)好這(zhe)些中(zhong)間件的呢?這(zhe)里截(jie)取(qu)部份代碼進行分析(xi)
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);
}
}
}
}
|
看完(wan)這(zhe)里的代碼,其實有點疑惑,怎么通過 Promise.resolve(handler(xxxx)) 這(zhe)樣的代碼邏(luo)輯可(ke)以推進(jin)中間件的調用呢(ni)?
首(shou)先(xian),我(wo)們(men)知道,handler 其實就(jiu)(jiu)是(shi)一個 async function,next,就(jiu)(jiu)是(shi) dispatch.bind(null, i + 1) 比如這(zhe)個:
async (ctx, next) => {
await next();
}
而我(wo)們知道(dao),dispatch 是返(fan)回一(yi)個(ge) Promise.resolve 或者(zhe)一(yi)個(ge) Promise.reject,因(yin)此在(zai) async function 里執行(xing) await next(),就相當(dang)于(yu)觸發下一(yi)個(ge)中(zhong)間件的(de)調用。
當 compose 完(wan)成(cheng)后(hou),還是會(hui)返回一個 function (context, next),于(yu)是就走到下面這個邏輯,執行 fn 并傳入上下文 this 后(hou),再將在中(zhong)間件中(zhong)賦值的 this.body resolve 出來,最(zui)終(zhong)就成(cheng)為云(yun)函數數要(yao)返回的值。
const fn = compose(middlewares);
return new Promise((resolve, reject) => {
fn(this).then((res) => {
resolve(this.body);
}).catch(reject);
});
|
看(kan)到 Promise.resolve 一個 async function,許多人(ren)都會很困(kun)惑。其實撇除 next 這(zhe)個往下調用中間件(jian)的(de)邏輯,我們(men)可以(yi)很好地(di)將邏輯簡化成下面這(zhe)段示例:
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
|
|