# uni-cloud-router

UniCloud cloud function routing library based on koa style, supports both uniCloud client and URL access

Source code repository: https://gitee.com/dcloud/uni-cloud-router

# Cloud function side

# Install

Imported from Plugin Market

  1. Visit the plugin market uni-cloud-router, click on the right to import the plugin using HBuilderX
  2. Right-click Manage Common Module Dependencies in the cloud function directory where you want to use uni-cloud-router (for example: uniCloud/cloudfunctions/router), select uni-cloud-router and confirm

Install using npm

npm install --save uni-cloud-router

# introduce

# Directory Structure

├── package.json
├── index.js // 云函数入口文件
├── config.js // 用于配置 router 应用根目录、中间件等
├── controller // 用于解析用户的输入,处理后返回相应的结果
|   ├── user.js
├── service (可选) //用于编写业务逻辑层,建议使用
|   ├── user.js

# Quick start

In order to get started quickly, a simple demo example is provided, taking creating a hello-uni-cloud-router cloud function as an example, to demonstrate how to organize code through uni-cloud-router:

1. Add entry file

// index.js (usually without changes) do
const Router = require("uni-cloud-router").Router; // 引入 Router
const router = new Router(require("./config.js")); // 根据 config 初始化 Router
exports.main = async (event, context) => {
  return router.serve(event, context); // 由 Router 接管云函数
};

2. Add configuration file

// config.js
module.exports = {
  debug: true, // 调试模式时,将返回 stack 错误堆栈
  baseDir: __dirname, // 必选,应用根目录
  middleware: [], // 自定义中间件
};

3. Create a hello.js in the controller folder

create a controller

const { Controller } = require("uni-cloud-router");
module.exports = class HelloController extends Controller {
  sayHello() {
    return this.service.hello.sayHello();
  }
};

4. Create a hello.js in the service folder

create a service

const { Service } = require("uni-cloud-router");
module.exports = class HelloService extends Service {
  sayHello() {
    return {
      data: "welcome to uni-cloud-router!",
    };
  }
};

At this point, a hello-uni-cloud-router cloud function has been created (note: the front-end can only be accessed after uploading the cloud function).

5. Call the cloud function in the page

In the page, access sayHello under hello (controller) through callFunction:

sayHello() {
  uniCloud.callFunction({
		name: 'hello-uni-cloud-router',
		data: {
			action: 'hello/sayHello',
			data: {}
		}
  })
  .then(res => {
    this.title = res.data
  });
}

The above code is only an example, it is recommended to click [Import sample project using HBuilderX] on the right to try.

# Deep learning

# Controller

It is responsible for parsing the user's input and returning the corresponding result after processing.

It is recommended that the Controller layer mainly process (check and convert) the user's request parameters, and then call the corresponding service method to process the business, encapsulate and return the business result after obtaining the business result:

  1. Get the request parameters passed by the user.
  2. Check and assemble parameters.
  3. Call the Service for business processing, and if necessary, process and convert the returned result of the Service to adapt it to the needs of the user.
  4. Respond to the user with the result.

# How to write Controller

All Controller files must be placed in the controller directory, which can support multi-level directories, and can be accessed by cascading directory names when accessing.

// controller/post.js
const Controller = require("uni-cloud-router").Controller;
// Must inherit from Controller class
module.exports = class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    // Verify parameters, note: uni-cloud-router itself does not contain a validate method, this method needs to be implemented by the user. It is recommended to mount it in the middleware
    ctx.validate({
      title: { type: "string" },
      content: { type: "string" },
    });
    // Assembly parameters, ctx.auth.uid is mounted on ctx by the user's own auth middleware
    const author = ctx.auth.uid;
    const post = Object.assign(ctx.data, { author });
    // Call Service for business processing
    return service.post.create(post);
  }
};

The defined Controller class will instantiate a brand new object every time an access request is made, and the following properties will be attached to this.

  • this.ctx: The instance of the context object of the current request, through which we can get various convenient properties and methods.
  • this.service: The service defined by the application, through which we can access the abstracted business layer, which is equivalent to this.ctx.service.
  • this.db: equivalent to uniCloud.database().
  • this.curl: equivalent to uniCloud.httpclient.request.
  • this.throw: throw exception information, equivalent to this.ctx.throw.

# Get request parameters

Obtain the parameters sent by the request through the data attribute of the Context instance bound on the Controller

class PostController extends Controller {
  async listPosts() {
    const data = this.ctx.data;
    // {
    //   username: 'demo',
    //   password: 'demo',
    // }
  }
}

# call Service

The encapsulation of business logic through the Service layer can not only improve the reusability of the code, but also allow the business logic to be better tested.

The Controller can call any method on any Service, and the Service is lazy loaded, and it will be instantiated only when it is accessed.

class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const author = ctx.auth.uid;
    const post = Object.assign(ctx.data, { author });
    // Call service for business processing
    return service.post.create(post);
  }
}

For the specific writing of Service, please refer to the Service chapter.

# Customize the status code returned by URLization

class PostController extends Controller {
  async create() {
    // set the status code to 201
    this.ctx.status = 201; // 仅当使用 HTTP/HTTPS 请求时生效
  }
}

# Customized URL-based response headers

class PostController extends Controller {
  async create() {
    this.ctx.headers = {
			'location': 'http://www.baidu.com'
		}
  }
}

Notice

  • Please use all lowercase for each field in the response header, for example: 'Content-Type' ×, 'content-type' √

# URLized return response body

// return javascript content
class GetController extends Controller {
  async create() {
    ctx.headers = {'content-type':'application/javascript'}
    ctx.body = 'console.log("abc")'
  }
}
// return image
class GetController extends Controller {
  async create() {
    ctx.isBase64Encoded = true
    ctx.headers = {'content-type': 'image/png'}
    ctx.body = '图片Buffer对应的base64内容'
  }
}

# Use of cookies

In some scenarios, cookies still play an important role, for example, in the case of URLization of cloud functions, to obtain the status of the client

Using cookies in cloud functions needs to rely on the cookie library npm page address, which can be installed through npm install cookie

'use strict';

//Introduce cookies
const cookie = require('cookie')

module.exports = class Instance extends Controller 
{
    async foo(ctx)
    {
	//event is the parameter uploaded by the client
	//If the client has a cookie, the cookie will be carried to the server with the request and placed in ctx.event.headers.cookie, assuming the cookie is "[jwt=self-encrypting base64; app=uniCloud]"
	console.log('event : ', ctx.event)
    
        //parse the cookie
        const cookieData = cookie.parse( ctx.event.headers.cookie||'' )
        console.log(cookieData)//输出结果为:{jwt:"自加密base64", app:"uniCloud" }

        //Set the cookie to the client

        const cookieOptions = {
            //Please refer to https://www.npmjs.com/package/cookie for specific parameters
            maxAge: 60 * 60 * 24 * 7,//一周
            path:"/"
        }
        const setCookieData = cookie.serialize('app', 'appName', cookieOptions)
        ctx.headers['set-cookie'] = setCookieData
        

        //...other operations
    }
	
};

# Service

An abstraction layer for business logic encapsulation, which has the following benefits:

  • Keep logic in Controller more concise.
  • To maintain the independence of business logic, the abstracted Service can be called repeatedly by multiple Controllers.
  • Separation of logic and presentation makes it easier to write test cases.

# scenes to be used

  • Processing of complex data, for example, the information to be displayed needs to be obtained from the database, and must be calculated by certain rules before it can be returned to the user for display. Or after the calculation is completed, update to the database.
  • Invocation of third-party services, such as WeChat template message push, etc.

# How to write Service

All Service files must be placed in the service directory, which can support multi-level directories, and can be accessed by cascading directory names when accessing.

// service/post.js
const Service = require("uni-cloud-router").Service;
// must inherit from Service
module.exports = class PostService extends Service {
  async create(data) {
    return this.db.add(data);
  }
};

The defined Service class is lazy loaded, it will be instantiated only when it is accessed, and there will be the following properties hanging on this.

  • this.ctx: The instance of the context object of the current request, through which we can get various convenient properties and methods.
  • this.service: The service defined by the application, through which we can access the abstracted business layer, which is equivalent to this.ctx.service.
  • this.db: equivalent to uniCloud.database().
  • this.curl: equivalent to uniCloud.httpclient.request.
  • this.throw: throw exception information, equivalent to this.ctx.throw.

# Using Service

Calling Service in Controller

# Middleware

Add processing logic before and after routing requests to implement some specific functions, such as: user login, permission verification, etc.

# Development middleware

Consistent with koa, reference: koa middleware

// middleware/auth.js
const uniID = require("uni-id");
module.exports = () => {
  // return middleware function
  return async function auth(ctx, next) {
    // check token
    const auth = await uniID.checkToken(ctx.event.uniIdToken);
    if (auth.code) {
      // Failed to verify, throw error message
      throw { code: auth.code, message: auth.message };
    }
    ctx.auth = auth; // 设置当前请求的 auth 对象
    await next(); // 执行后续中间件
  };
};

Example:

# Using middleware

  1. Configure via config.js
const auth = require('./middleware/auth.js') // 引入 auth 中间件
module.exports = {
  debug: true, // 调试模式时,将返回 stack 错误堆栈
  baseDir: __dirname, // 指定应用根目录
  middleware: [
    [
      //Array format, the first element is the middleware, and the second element is the middleware effective rule configuration
      auth(), // 注册中间件
      { enable: true, ignore: /\/login$/ }, // 配置当前中间件生效规则,该规则表示以`/login`结尾的路由不会执行 auth 中间件校验 token
    ],
  ],
}
  1. Middleware configuration items
  • enable controls whether the middleware is enabled.

  • match Set only requests that match certain rules will go through this middleware.

    Support type:

    • String: When the parameter is a string type, an action prefix is configured, and all actions prefixed with this string will match.
    • Regular: When the parameter is regular, directly match the action that satisfies the regular verification.
    • Function: When the parameter is a function, the request context will be passed to the function, and the match will be determined according to the function result (true/false).
    • Array: It can be composed of strings, regular expressions, and functions, and any one of them can be matched
  • ignore Set requests that match certain rules to not go through this middleware.

    Support type: same as match

# Context

Context is a request-level object. Every time a user request is received, a Context object will be instantiated. This object encapsulates the information requested by the user and provides many convenient methods to obtain request parameters or set response information. . The framework will mount all Services to the Context instance

# method of obtaining

The most common way to get Context instance is in Middleware, [Controller](#%E6%8E%A7%E5%88% B6%E5%99%A8controller) and Service.

// Get the Context instance through this.ctx in the Controller
module.exports = class UserController extends Controller {
  async login() {
    const data = this.ctx.data // 从 Context 实例上获取请求参数
  }
}
// Get the Context instance in Service through this.ctx
module.exports = class PostService extends Service {
  async create(data) {
    const auth = this.ctx.auth // 从 Context 实例上获取 auth(需要启用 uni-id 中间件)
  }
}
// Get the Context instance through the ctx parameter in Middleware
module.exports = (options) => {
  // return middleware function
  return async function auth(ctx, next) {
    const data = ctx.data // 从 Context 实例上获取请求参数
    await next()
  }
}

# Client uses cloud functions

# send request

// use uniCloud access
uniCloud.callFunction({
  name: 'router', // 要调用的云函数名称
  data: {
    action: 'user/login', // 路由地址,对应 controller 下 user.js 的 login 方法
    // parameter list
    data: {
      // controller gets through this.ctx.data
      username: 'demo',
      password: 'demo',
    },
  },
})
// use URLized request access
uni.request({
  url: 'xxxxx/router/user/login', // 路由地址,对应 controller 下 user.js 的 login 方法
  data: {
    // controller gets through this.ctx.data
    username: 'demo',
    password: 'demo',
  },
})

# return result

{
  "code": "", // 异常 code,如:"INVOKE_FUNCTION_FAILED"
  "message": "", // 异常信息
  "stack": "" // 当 config.js 中配置 debug 为 true 时,返回发生异常的堆栈信息
  // other information
}