李成熙,騰訊雲高級工程師。2014年度畢業加入騰訊AlloyTeam,前後負責過QQ羣、花樣直播、騰訊文檔等項目。2018年加入騰訊云云開發團隊。專一於性能優化、工程化和小程序服務。微博 | 知乎 | Githubgit
原文連接github
在掘金開發者大會上,在推薦實踐那裏,我有提到一種雲函數的用法,咱們能夠將相同的一些操做,好比用戶管理、支付邏輯,按照業務的類似性,歸類到一個雲函數裏,這樣比較方便管理、排查問題以及邏輯的共享。甚至若是你的小程序的後臺邏輯不復雜,請求量不是特別大,徹底能夠在雲函數裏面作一個單一的微服務,根據路由來處理任務。小程序
用下面三幅圖能夠歸納,咱們來回顧一下:性能優化
好比這裏就是傳統的雲函數用法,一個雲函數處理一個任務,高度解耦。架構
第二幅架構圖就是嘗試將請求歸類,一個雲函數處理某一類的請求,好比有專門負責處理用戶的,或者專門處理支付的雲函數。app
最後一幅圖顯示這裏只有一個雲函數,雲函數裏有一個分派任務的路由管理,將不一樣的任務分配給不一樣的本地函數處理。異步
tcb-router
介紹及用法爲了方便你們試用,我們騰訊雲 Tencent Cloud Base 團隊開發了 tcb-router,雲函數路由管理庫方便你們使用。async
那具體怎麼使用 tcb-router
去實現上面提到的架構呢?下面我會逐一舉例子。函數
架構一:一個雲函數處理一個任務 這種架構下,其實不須要用到 tcb-router
,像普通那樣寫好雲函數,而後在小程序端調用就能夠了。微服務
// 函數 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);
});
複製代碼
架構二: 按請求給雲函數歸類 此類架構就是將類似的請求歸類到同一個雲函數處理,好比能夠分爲用戶管理、支付等等的雲函數。
// 函數 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();
};
// 函數 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);
});
複製代碼
架構三: 由一個雲函數處理全部服務
// 函數 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);
});
複製代碼
小程序·雲開發的雲函數目前更推薦 async/await
的玩法來處理異步操做,所以這裏也參考了一樣是基於 async/await
的 Koa2 的中間件實現機制。
從上面的一些例子咱們能夠看出,主要是經過 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
該對象中,用於後續調用 serve
的時候,層層去執行中間件。
最重要的運行中間件邏輯,則是在 serve
和 compose
兩個方法裏。
serve
裏主要的做爲是作路由的匹配以及將中間件組合好以後,經過 compose
進行下一步的操做。好比如下這段節選的代碼,實際上是將匹配到的路由的中間件,以及 *
這個通配路由的中間件合併到一塊兒,最後依次執行。
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
,實現雲函數的對小程序端的返回:
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);
// dispath 是核心的方法,經過不斷地調用 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)
好比這個:
async (ctx, next) => {
await next();
}
複製代碼
而咱們知道,dispath
是返回一個 Promise.resolve
或者一個 Promise.reject
,所以在 async function
裏執行 await next()
,就至關於觸發下一個中間件的調用。
當 compose
完成後,仍是會返回一個 function (context, next)
,因而就走到下面這個邏輯,執行 fn
並傳入上下文 this
後,再將在中間件中賦值的 this.body
resolve
出來,最終就成爲雲函數數要返回的值。
const fn = compose(middlewares);
return new Promise((resolve, reject) => {
fn(this).then((res) => {
resolve(this.body);
}).catch(reject);
});
複製代碼
看到 Promise.resolve
一個 async function
,許多人都會很困惑。其實撇除 next
這個往下調用中間件的邏輯,咱們能夠很好地將邏輯簡化成下面這段示例:
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
複製代碼