V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lcxfs1991
V2EX  ›  推广

小程序·云开发的云函数路由高级玩法

  •  
  •   lcxfs1991 · 2018-09-23 11:27:15 +08:00 · 3639 次点击
    这是一个创建于 2252 天前的主题,其中的信息可能已经有所发展或是发生改变。

    李成熙,腾讯云高级工程师。2014 年度毕业加入腾讯 AlloyTeam,先后负责过 QQ 群、花样直播、腾讯文档等项目。2018 年加入腾讯云云开发团队。专注于性能优化、工程化和小程序服务。微博 | 知乎 | Github

    概念回顾

    在掘金开发者大会上,在推荐实践那里,我有提到一种云函数的用法,我们可以将相同的一些操作,比如用户管理、支付逻辑,按照业务的相似性,归类到一个云函数里,这样比较方便管理、排查问题以及逻辑的共享。甚至如果你的小程序的后台逻辑不复杂,请求量不是特别大,完全可以在云函数里面做一个单一的微服务,根据路由来处理任务。

    用下面三幅图可以概括,我们来回顾一下:

    比如这里就是传统的云函数用法,一个云函数处理一个任务,高度解耦。

    第二幅架构图就是尝试将请求归类,一个云函数处理某一类的请求,比如有专门负责处理用户的,或者专门处理支付的云函数。

    最后一幅图显示这里只有一个云函数,云函数里有一个分派任务的路由管理,将不同的任务分配给不同的本地函数处理。

    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
    
    2 条回复    2018-09-23 13:42:25 +08:00
    anyele
        1
    anyele  
       2018-09-23 11:53:12 +08:00 via Android
    什么鬼东西
    otakustay
        2
    otakustay  
       2018-09-23 13:42:25 +08:00
    有种在时代倒退的既视感
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2881 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 02:57 · PVG 10:57 · LAX 18:57 · JFK 21:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.