李成熙,騰訊云高級工程師。2014年度畢業(yè)加入騰訊AlloyTeam,先后負責過QQ群、花樣直播、騰訊文檔等項目。2018年加入騰訊云云開發(fā)團隊。專注于性能優(yōu)化、工程化和小程序服務。微博 | 知乎 | Github
概念回顧
在掘金開發(fā)者大會上,在推薦實踐那里,我有提到一種云函數(shù)的用法,我們可以將相同的一些操作,比如用戶管理、支付邏輯,按照業(yè)務的相似性,歸類到一個云函數(shù)里,這樣比較方便管理、排查問題以及邏輯的共享。甚至如果你的小程序的后臺邏輯不復雜,請求量不是特別大,完全可以在云函數(shù)里面做一個單一的微服務,根據(jù)路由來處理任務。
用下面三幅圖可以概括,我們來回顧一下:
比如這里就是傳統(tǒng)的云函數(shù)用法,一個云函數(shù)處理一個任務,高度解耦。
第二幅架構圖就是嘗試將請求歸類,一個云函數(shù)處理某一類的請求,比如有專門負責處理用戶的,或者專門處理支付的云函數(shù)。
最后一幅圖顯示這里只有一個云函數(shù),云函數(shù)里有一個分派任務的路由管理,將不同的任務分配給不同的本地函數(shù)處理。
tcb-router 介紹及用法
為了方便大家試用,咱們騰訊云 Tencent Cloud Base 團隊開發(fā)了 tcb-router,云函數(shù)路由管理庫方便大家使用。
那具體怎么使用 tcb-router 去實現(xiàn)上面提到的架構呢?下面我會逐一舉例子。
架構一:一個云函數(shù)處理一個任務
這種架構下,其實不需要用到 tcb-router,像普通那樣寫好云函數(shù),然后在小程序端調(diào)用就可以了。
// 函數(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);
});
|
架構二: 按請求給云函數(shù)歸類
此類架構就是將相似的請求歸類到同一個云函數(shù)處理,比如可以分為用戶管理、支付等等的云函數(shù)。
// 函數(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();
};
|
// 注冊用戶
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);
});
|
架構三: 由一個云函數(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();
};
|
// 注冊用戶
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 的中間件機制實現(xiàn)云函數(shù)的路由管理
小程序·云開發(fā)的云函數(shù)目前更推薦 async/await 的玩法來處理異步操作,因此這里也參考了同樣是基于 async/await 的 Koa2 的中間件實現(xiàn)機制。
從上面的一些例子我們可以看出,主要是通過 use 和 router 兩種方法傳入路由以及相關處理的中間件。
use 只能傳入一個中間件,路由也只能是字符串,通常用于 use 一些所有路由都得使用的中間件
// 不寫路由表示該中間件應用于所有的路由
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,都只是將路由和中間件信息,通過 _addMiddleware 和 _addRoute 兩個方法,錄入到 _routerMiddlewares 該對象中,用于后續(xù)調(diào)用 serve 的時候,層層去執(zhí)行中間件。
最重要的運行中間件邏輯,則是在 serve 和 compose 兩個方法里。
serve 里主要的作用是做路由的匹配以及將中間件組合好之后,通過 compose 進行下一步的操作。比如以下這段節(jié)選的代碼,其實是將匹配到的路由的中間件,以及 * 這個通配路由的中間件合并到一起,最后依次執(zhí)行。
let middlewares = (_routerMiddlewares[url]) ? _routerMiddlewares[url].middlewares : [];
// put * path middlewares on the queue head
if (_routerMiddlewares['*']) {
middlewares = [].concat(_routerMiddlewares['*'].middlewares, middlewares);
}
組合好中間件后,執(zhí)行這一段,將中間件 compose 后并返回一個函數(shù),傳入上下文 this 后,最后將 this.body 的值 resolve,即一般在最后一個中間件里,通過對 ctx.body 的賦值,實現(xiàn)云函數(shù)的對小程序端的返回:
const fn = compose(middlewares);
return new Promise((resolve, reject) => {
fn(this).then((res) => {
resolve(this.body);
}).catch(reject);
});
那么 compose 是怎么組合好這些中間件的呢?這里截取部份代碼進行分析
function compose(middleware) {
/**
* ... 其它代碼
*/
return function (context, next) {
// 這里的 next,如果是在主流程里,一般 next 都是空。
let index = -1;
// 在這里開始處理處理第一個中間件
return dispatch(0);
// dispatch 是核心的方法,通過不斷地調(diào)用 dispatch 來處理所有的中間件
function dispatch(i) {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
// 獲取中間件函數(shù)
let handler = middleware[i];
// 處理完最后一個中間件,返回 Proimse.resolve
if (i === middleware.length) {
handler = next;
}
if (!handler) {
return Promise.resolve();
}
try {
// 在這里不斷地調(diào)用 dispatch, 同時增加 i 的數(shù)值處理中間件
return Promise.resolve(handler(context, dispatch.bind(null, i + 1)));
}
catch (err) {
return Promise.reject(err);
}
}
}
}
|
看完這里的代碼,其實有點疑惑,怎么通過 Promise.resolve(handler(xxxx)) 這樣的代碼邏輯可以推進中間件的調(diào)用呢?
首先,我們知道,handler 其實就是一個 async function,next,就是 dispatch.bind(null, i + 1) 比如這個:
async (ctx, next) => {
await next();
}
而我們知道,dispatch 是返回一個 Promise.resolve 或者一個 Promise.reject,因此在 async function 里執(zhí)行 await next(),就相當于觸發(fā)下一個中間件的調(diào)用。
當 compose 完成后,還是會返回一個 function (context, next),于是就走到下面這個邏輯,執(zhí)行 fn 并傳入上下文 this 后,再將在中間件中賦值的 this.body resolve 出來,最終就成為云函數(shù)數(shù)要返回的值。
const fn = compose(middlewares);
return new Promise((resolve, reject) => {
fn(this).then((res) => {
resolve(this.body);
}).catch(reject);
});
|
看到 Promise.resolve 一個 async function,許多人都會很困惑。其實撇除 next 這個往下調(diào)用中間件的邏輯,我們可以很好地將邏輯簡化成下面這段示例:
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
|
|