通过 Serverless Components Koa 构建后台服务

2020-07-06 01:13:38 浏览数 (1)

Serverless Components 是支持多个云资源编排和组织的场景化解决方案,主要基于客户的具体场景,如 Express 框架支持、网站部署等。Serverless Components 可以有效简化云资源的配置和管理,将网关、COS 和 CAM 等产品联动起来,让客户更多关注场景和业务。

本文将讲解如何使用 Serverless Components Koa 快速构建一个后台接口服务,并验收新建、查询用户等功能。

项目的框架结构:

代码语言:txt复制
koa-backend-demo
├── config
│   ├── database
│   │   └── mysql.js
│   └── index.js
├── controller.js
├── controllers
│   └── User
│       ├── User.js
│       └── index.js
├── models
│   └── UserModel.js
├── package.json
├── serverless.yml
├── sls.js
└── utils
    ├── ApiResponse
    │   └── ApiResponse.js
    └── utils.js

配置 serverless.yml

代码语言:txt复制
# serverless.yml

component: koa # (required) name of the component. In that case, it's koa.
name: koa-backend-demo # (required) name of your koa component instance.

inputs:
  src:
    src: ./ # (optional) path to the source folder. 
    exclude:
      - .env
  region: ap-guangzhou
  runtime: Nodejs12.16
  apigatewayConf:
    protocols:
      - http
      - https
    environment: release

新建 sls.js

代码语言:txt复制
const Koa = require('koa');
const router = require('koa-router')();
const controller = require('./controller');
const app = new Koa();

// x-response-time 响应时间
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// 配置路由
app.use(controller());

module.exports = app;

新建 controller.js

controller.js 用于遍历 controllers 文件夹下的文件去配置接口路由。

代码语言:txt复制
const fs = require('fs');
const config = require('./config/index.js');

function addMapping(router, mapping) {
	for (let url in mapping) {
		if (url.startsWith('GET ')) {
			let path = config.prefix   url.substring(4);
			router.get(path, mapping[url]);
		} else if (url.startsWith('POST ')) {
			let path = config.prefix   url.substring(5);
			router.post(path, mapping[url]);
		} else {
			console.error(`invalid URL: ${url}`);
		}
	}
}

function addControllers(router, dir) {
  let files = fs.readdirSync(__dirname   '/'   dir);
  let js_files = [];
  for (let i in files) {
    js_files.push(`${files[i]}/index.js`);
  }

	for (let f of js_files) {
		console.log(`process controller: ${f}...`);
		let mapping = require(__dirname   '/'   dir   '/'   f);
		addMapping(router, mapping);
	}
}
module.exports = function (dir) {
	let controllers_dir = dir || 'controllers';
	let router = require('koa-router')();
	addControllers(router, controllers_dir);
	return router.routes();
};

config 配置文件夹

config/index.js

配置接口前缀

代码语言:txt复制
'use strict'

module.exports = {
	prefix: '/api',
}

config/database/mysql.js 数据库配置

代码语言:txt复制
'use strict'

const mysql = require('mysql2');

const MYSQL_CONFIG = {
  HOST: 'xxxxxxxxxxxx.gz.tencentcdb.com', // 腾讯云数据库
  PORT: '3306',
  DATABASE: 'mydata',
  USERNAME: 'root',
  PASSWORD: 'xxxxxxxx'
};

// init mysql connection
const initMysqlPool = () => {
  const promisePool = mysql
    .createPool({
      host: MYSQL_CONFIG.HOST,
      user: MYSQL_CONFIG.USERNAME,
      port: MYSQL_CONFIG.PORT,
      password: MYSQL_CONFIG.PASSWORD,
      database: MYSQL_CONFIG.DATABASE,
      connectionLimit: 1,
    })
    .promise();

  return promisePool;
}

module.exports = {
  MYSQL_CONFIG,
  initMysqlPool,
}

备注:此处需要配置远程连接 MySQL 的配置信息(即 MYSQL_CONFIG)

新建 models 文件夹

通过 UserModel.js 操作数据库

代码语言:txt复制
const { initMysqlPool } = require('./../config/database/mysql');
const pool = initMysqlPool();

class UserModel {
  constructor({
    id = 0,
    name = '',
    email = '',
    site = '',
  }) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.site = site;
  }

  // 注册用户
  insertData({
    name = '',
    email = '',
    site = '',
  }) {
    return pool.query('INSERT into users SET ?', {
      name,
      email,
      site,
    })
  }

  // 查找所有用户
  findAllUsers() {
    return pool.query('SELECT * FROM users');
  }

  // 通过名字查找用户信息
  findDataByName( name ) {
    return pool.query(
      'SELECT * FROM users WHERE name = ?',
      [
        name,
      ]
    )
  }
}

module.exports = UserModel;

controllers 控制器

controllers/User/index.js

用于定义接口路由:

代码语言:txt复制
'use strict'

const User = require('./User');
const user = new User();

module.exports = {
  'GET /user/get-user-by-name': user.getUserByName.bind(user),
  'GET /user/get-user-list': user.getUserList.bind(user),
  'GET /user/create-user': user.createUser.bind(user),
}

备注: /user/create-user 接口建议使用 POST 请求(即 'POST /user/create-user': user.createUser.bind(user) 定义),此处为了演示方便定义为 GET 请求。

controllers/User/User.js

业务逻辑

代码语言:txt复制
'use strict';

const ApiResponse = require('./../../utils/ApiResponse/ApiResponse');
const { awaitWrap } = require('./../../utils/utils');
const UserModel = require('./../../models/UserModel');

const userModel = new UserModel({});

class User {

  async getUserList(ctx, next) {
    const res = new ApiResponse({});

    const [data, err] = await awaitWrap(
      userModel.findAllUsers()
    )
    if (err) {
      res.errDetail = err;
      ctx.body = res;
      return false;
    }

    const [userData] = data;

    res.code = 0;
    res.msg = '请求成功';
    res.data = userData;
    ctx.body = res;
  }

  async createUser(ctx, next) {
    const res = new ApiResponse({});

    const name = ctx.request.query.name || "";
    const email = ctx.request.query.email || "";
    const site = ctx.request.query.site || "";

    const userData = await this._getUserByName(name);
    if (
      userData.code === 0 &&userData.data &&
      userData.data.length > 0
    ) {
      res.code = 0;
      res.msg = '已存在该用户名';
      res.data = userData.data;
      ctx.body = res;
      return true;
    }
    
    const [data, err] = await awaitWrap(
      userModel.insertData({
        name,
        email,
        site,
      })
    );
    if (err) {
      res.errDetail = err;
      ctx.body = res;
      return false;
    }

    const [insertData] = data;

    res.code = 0;
    res.msg = '请求成功';
    res.data = [
      new UserModel({
        id: insertData.insertId,
        name: name,
        email: email,
        site: site,
      })
    ]

    ctx.body = res;
  }

  async _getUserByName(name) {
    const res = new ApiResponse({});

    if (!name) {
      res.msg = '用户姓名不能为空';
      return res;
    }

    const [data, err] = await awaitWrap(
      userModel.findDataByName(name)
    );
    if (err) {
      res.errDetail = err;
      return res;
    }

    const [userData] = data;
    if (userData.length > 0) {
      res.code = 0;
      res.msg = '请求成功';
      res.data = userData;
    } else {
      res.code = 0;
      res.msg = '用户信息不存在';
      res.data = []
    }

    return res;
  }

  async getUserByName(ctx, next) {
    const name = ctx.request.query.name || "";
    const res = await this._getUserByName(name);

    ctx.body = res;
  }
}

module.exports = User;

utils 工具函数

utils/ApiResponse/ApiResponse.js

接口响应的数据结构

代码语言:txt复制
'use strict'

class ApiResponse {
  constructor({
    code = 500,
    data = {},
    msg = '系统异常',
    errDetail = '',
  }) {
		this.code = code;
		this.data = data;
		this.msg = msg;
		this.errDetail = errDetail;
	}
}

module.exports = ApiResponse;

utils/utils.js

提供 awaitWrap 函数捕获 promise 报错

代码语言:txt复制
/**
 * @param {*} promise 
 * @returns [data, err]
 */
const awaitWrap = (promise) => {
  return promise
    .then(data => [data, null])
    .catch(err => [null, err]);
}

module.exports = {
  awaitWrap,
}

package.json

通过 npm init 新建 package.json ,安装项目依赖包:

代码语言:txt复制
npm i --save koa koa-router mysql2

使用 sls deploy 部署

代码语言:txt复制
sls deploy

若未有 .env 配置文件,则通过微信扫码登录授权。

sls deploy 部署sls deploy 部署

如上图所示,kao 接口服务的请求 url 是 https://service-gwlsee6c-1253513412.gz.apigw.tencentcs.com/release

我们刚刚写了三个接口,分别为:

新增用户接口:

https://service-gwlsee6c-1253513412.gz.apigw.tencentcs.com/release/api/user/create-user?name=xiaoming&email=1234566@qq.com&site=F

新增用户接口新增用户接口

通过名字查找用户信息:

https://service-gwlsee6c-1253513412.gz.apigw.tencentcs.com/release/api/user/get-user-by-name?name=xiaoming

查找所有用户:

https://service-gwlsee6c-1253513412.gz.apigw.tencentcs.com/release/api/user/get-user-list

查找所有用户查找所有用户

项目代码:

通过 Serverless Components Koa 构建后台服务

0 人点赞