1. 程式人生 > >這樣玩雲函數路由,讓你看起來很高級

這樣玩雲函數路由,讓你看起來很高級

開發者 實戰 技術分享 ctr login 發布 以及 new t github

歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐幹貨哦~

本文由李成熙heyli發表於雲+社區專欄

概念回顧

在掘金開發者大會上,在推薦實踐那裏,我有提到一種雲函數的用法,我們可以將相同的一些操作,比如用戶管理、支付邏輯,按照業務的相似性,歸類到一個雲函數裏,這樣比較方便管理、排查問題以及邏輯的共享。甚至如果你的小程序的後臺邏輯不復雜,請求量不是特別大,完全可以在雲函數裏面做一個單一的微服務,根據路由來處理任務。

用下面三幅圖可以概括,我們來回顧一下:

技術分享圖片

比如這裏就是傳統的雲函數用法,一個雲函數處理一個任務,高度解耦。

技術分享圖片

第二幅架構圖就是嘗試將請求歸類,一個雲函數處理某一類的請求,比如有專門負責處理用戶的,或者專門處理支付的雲函數。

技術分享圖片

最後一幅圖顯示這裏只有一個雲函數,雲函數裏有一個分派任務的路由管理,將不同的任務分配給不同的本地函數處理。

tcb-router 介紹及用法

為了方便大家試用,咱們騰訊雲 Tencent Cloud Base 團隊開發了 tcb-router,雲函數路由管理庫方便大家使用。

那具體怎麽使用 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); });

借鑒 Koa2 的中間件機制實現雲函數的路由管理

小程序·雲開發的雲函數目前更推薦 async/await 的玩法來處理異步操作,因此這裏也參考了同樣是基於 async/await 的 Koa2 的中間件實現機制。

從上面的一些例子我們可以看出,主要是通過 userouter 兩種方法傳入路由以及相關處理的中間件。

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 的時候,層層去執行中間件。

最重要的運行中間件邏輯,則是在 servecompose 兩個方法裏。

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);

        // 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 functionnext,就是 dispatch.bind(null, i + 1) 比如這個:

async (ctx, next) => {
    await next();
}

而我們知道,dispatch 是返回一個 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

相關閱讀
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由作者授權騰訊雲+社區發布,更多原文請點擊

搜索關註公眾號「雲加社區」,第一時間獲取技術幹貨,關註後回復1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區!

這樣玩雲函數路由,讓你看起來很高級