uniCloud 云函数路由 for koa

384 阅读6分钟

cmd-cloud-router

基于 koa 的云函数路由中间件,提供 RESTful 资源路由

介绍

简易 koa 洋葱圈模型执行,koa-router 路由风格编写云函数。

  • 每个云函数是一个独立进程,使用路由分为多部分
  • 为支持路由参数使用 path-to-regexp 函数库解析参数
  • 函数需要使用 nodejs12 版本以上
  • 没有封装 uniCloud 函数,需要自己声明引入

提醒

  • 云函数:请求共用会导致其他请求连接超时,所以不建议一个云函数里写太多业务路由,分为按模块资源单函数可避免请求压力。
  • 云函数 URL 化:腾讯云免费服务空间最多只支持配置 10 个云函数 URL 化地址。

uni-app插件市场

Gitee源代码

函数-API

应用对象(Application)

Application 是请求级别的全局应用对象,监听云函数执行。基于洋葱圈模型,请求从第一个中间件开始并按其方式顺序运行。

函数名称说明
use使用中间件使用给定中间件加入程序执行过程
listen监听程序请求将云函数请求数据加入到应用程序对象

客户端调用云函数时传入的参数

参数解释
url必传参数,请求已定义的路由路径
method默认参数"GET",路由路径指定的请求方法
data可选参数,传入可能需要的参数数据

使用示例

const { Application } = require("cmd-cloud-router");
const app = new Application();

// 使用中间件
app.use(async (ctx, next) => {
  const start = Date.now();
  console.log(`开始计时`);

  let sum = 0;
  for (let i = 0; i < 99; i++) {
    for (let j = 0; j < i; j++) {
      sum += j;
    }
  }

  await next();

  // 响应结果要用 ctx.body
  ctx.body = `求和:${sum}`;

  const ms = Date.now() - start;
  console.log(`结束耗时 ==> ${ms}ms`);
});

// 对请求参数监听 返回Promise对象
app
  .listen(
    {
      url: "/index",
      method: "post",
      data: { value: 99 },
    },
    "<传入客户端云函数请求环境>"
  )
  .then(res => {
    console.log("响应ctx.body的结果:", res);
  })
  .catch(err => {
    console.log("错误结果:", e);
  });

上下文 Context

Context 是请求级别的对象,封装了请求的信息,对每一次请求都会重新初始上下文参数属性。

请求上下文对象信息

{
  /**客户端调用云函数时传入的参数 */
  "event": "object",
  /**客户端调用云函数时的系统运行环境参数 */
  "app": "object",
  /**客户端请求传入的参数*/
  "data": "object",
  /**请求资源 */
  "url": "string",
  /**url路径 */
  "path": "string",
  /**请求方法 */
  "method": "string",
  /**url查询字符串 */
  "querystring": "string",
  /**url查询参数 */
  "query": "object",
  /**url路由参数 */
  "params": "object",
  /**抛出异常的函数 */
  "throw": "(message:string,code?:string | number,data?:any) => {}",
  /**响应结果 */
  "body": "?any",
  /**配置参数 */
  "config": "?object"
}
  • throw: 程序异常抛出函数,信息通过 catch 输出信息
  • eventapp : 都是客户端调用云函数是传入的参数
  • body: 程序执行后通过该属性返回结果
  • config: 可选的请求级配置参数

获取云函数调用来源

封装的上下文属性app接管云函数context云函数的入参

通过上下文 app.SOURCE 返回云函数调用来源,它的值域为:

取值解释
client客户端 callFunction 方式调用
http云函数 url 化方式调用
timing定时触发器调用
function由其他云函数 callFunction 调用
server由 uniCloud 管理端调用,HBuilderX 里上传并运行

中间件 Middleware

Middleware 是请求级别的函数,在程序执行过程前或后加入处理逻辑,实现程序拦截过滤等功能。

module.exports = async (ctx, next) => {
  console.time("logger");

  // 中间件放行
  await next();
  // 挂上任意属性参数
  ctx.logger = "日志中间件";

  console.timeEnd("logger");
};
  • 中间件必须是异步函数,确保执行结果符合洋葱模型
  • 可以将中间件内处理的数据挂载到上下文上,传递到下一函数
  • next(): 中间件放行通过,如果没有触发该函数,会终止程序并返回上下文 body 属性的结果

路由对象 Router

Router 路由命名 URL 路由规则风格,支持 http 方法声明路由,可以只针对单个路由使用中间件,处理每一次请求。

函数名称说明
register自定义方法创建并注册路由
getget 方法使用 get 方法注册路由
postpost 方法使用 post 方法注册路由
putput 方法使用 put 方法注册路由
deletedelete 方法使用 delete 方法注册路由
routes路由器中间件中间件调度与请求匹配的路由

云函数调用来源为 timing 定时触发器会将 cron 表达式转换为 PUT 方法路由(hex 编码化)
转换例如 cron:0 0 * * * * => 63726f6e3a302030202a202a202a202a

使用示例

const { Router } = require("cmd-cloud-router");
const router = new Router();

// 使用指定方法路由
// router.["get" | "post" | "put" | "delete"](<URL路径>, <函数>, <中间件1>, <中间件2>);
// router.["get" | "post" | "put" | "delete"](<URL路径>, <函数>, [<中间件1>, <中间件2>]);

router.post(
  "/index",
  async ctx => {
    ctx.body = "成功";
  },
  [
    async (ctx, next) => {
      await next();
    },
  ]
);

// 使用自定义方法路由
// router.register(<请求方法>, <URL路径>, <函数>, <中间件1>, <中间件2>);
// router.register(<请求方法>, <URL路径>, <函数>, [<中间件1>, <中间件2>]);

router.register(
  "post",
  "/index",
  async ctx => {
    ctx.body = "成功";
  },
  async (ctx, next) => {
    await next();
  }
);

// 云函数调用来源为 timing 定时cron表达式转换路由 hex编码化
const cronPath = Buffer.from("cron:0 0 * * * *").toString("hex");
router.put(cronPath, async ctx => {
  console.log("执行 ==> %s - %s <==", ctx.method, ctx.path);
  // 定时任务没有需要响应的 ctx.body
});

// 返回路由中间件,要使用应用程序use注册到中间件里
router.routes();

实战示例

创建云函数

下载到公共模块依赖后,按照官方文档配置模块

公共模块文档uniapp.dcloud.net.cn/uniCloud/cf…

package.json

package.json 文件里不能放注释 //,记得删除干净。

{
  "name": "<你的云函数名>",
  "version": "1.0.0",
  "description": "<你的云函数服务说明>",
  "main": "index.js",
  "author": "https://ask.dcloud.net.cn/people/TsMask",
  "license": "MIT",
  "dependencies": {
    // 引入依赖库 cmd-cloud-router 放公共模块里
    "cmd-cloud-router": "file:../common/cmd-cloud-router"
  },
  // 相关配置查看 https://uniapp.dcloud.net.cn/uniCloud/cf-functions.html#packagejson
  "cloudfunction-config": {
    "memorySize": 512, // 最大可用内存
    "timeout": 30, // 超时时间
    "runtime": "Nodejs12" // 运行环境必须要 Nodejs12
  }
}

index.js

"use strict";
const { Application, Router } = require("cmd-cloud-router");
const app = new Application();
const router = new Router();

// 中间件1
app.use(async (ctx, next) => {
  const start = Date.now();
  console.log(`1开始计时`);

  await next();

  const ms = Date.now() - start;
  console.log(`4结束耗时 ==> ${ms}ms`);
});

// 中间件2
app.use(async (ctx, next) => {
  let sum = 0;
  for (let i = 0; i < 99; i++) {
    for (let j = 0; j < i; j++) {
      sum += j;
    }
  }
  console.log("2循环求和 %d", sum);

  await next();

  let ak = 0;
  for (let i = 1; i <= 4; i++) {
    for (let j = 1; j <= i; j++) {
      ak += j;
    }
  }
  console.log("3循环求和 %d", ak);
});

// 自定义方法 { url: '/user?id=28393&sort=desc', method: 'get', data: {} }
router.register("get", "/user", async ctx => {
  console.log(ctx.querystring);
  console.log(ctx.query);
  await Promise.resolve(() => 1);
  ctx.body = ["1", "2", ctx.query.id];
});

// get方法 { url: '/user/28393', method: 'get', data: {} }
router.get("/user/:id", async ctx => {
  console.log(ctx.params);
  await Promise.resolve(() => 1);
  ctx.body = "查询用户ID:" + ctx.params.id;
});

// post方法 { url: '/user/28393', method: 'post', data: { value: 99 } }
router.post(
  "/user/:id",
  async ctx => {
    console.log("路由参数", ctx.params);
    console.log("请求数据", ctx.data);
    await Promise.resolve(() => 1);
    ctx.body = { post: ctx.path, ...ctx.data };
  },
  async (ctx, next) => {
    console.log("路由前数据处理中间件", ctx.params);

    // 中间件放行
    await next();

    console.log("路由后数据处理中间件", ctx.params);
  }
);

// put方法 { url: '/user/28393/age', method: 'put', data: { value: 99 } }
router.put("/user/:id/:flag(none|avatar|age)", async ctx => {
  console.log(ctx.params);
  console.log(ctx.data);
  await Promise.resolve(() => 1);
  ctx.body = "修改用户ID:" + ctx.params.id;
});

// delete方法  { url: '/user/28393', method: 'delete', data: {} }
router.delete("/user/:id", async ctx => {
  console.log(ctx.params);
  await Promise.resolve(() => 1);
  ctx.body = "删除ID:" + ctx.params.id;
});

// 使用路由中间件
app.use(router.routes());

const config = {
  name: "测试配置",
  value: "1.0.0",
};

exports.main = async (event, context, config) => {
  // 对请求参数监听,返回Promise对象结果直接return
  try {
    const res = await app.listen(event, context);
    console.log("响应ctx.body的结果:", res);
  } catch (e) {
    console.log("错误结果:", e);
  }

  //event为客户端上传的参数
  console.log("event : ", event);

  //返回数据给客户端
  return event;
};

客户端使用

// ...
methods: {
  // 云函数客户端
  fnCall() {
    uniCloud.callFunction({
      name: '<你的云函数名>',
      data: {
        // data里是调用云函数需要的参数
        url: '/user/28393',
        method: 'post',
        data: {
          value: 99
        }
      }
    }).then((res) => {
      console.log(res)
    }).catch((err) => {
      console.error(err)
    })
  },
  // 云函数URL化
  fnUrl() {
    uni.request({
      method: 'POST',
      url: '<你的云函数URL化配置的PATH完整链接地址>',
      data: {
        // data里是调用云函数需要的参数
        url: '/user/28393',
        method: 'post',
        data: {
          value: 99
        }
      },
      success(res) {
        console.log(res);
      },
      fail(err) {
        console.error(err)
      }
    });
  }
}
// ...

推荐阅读