这是一个对于 AWS Lambda Functions 的简单 REST API 项目,使用 TypeScript 语言编写,数据存储采用 MongoDB Atlas 云数据库,从编码到 AWS Lambda 下的单元测试,再到部署、日志调试完整的介绍了如何快速编写一个 FaaS 函数。html
如下是咱们将要完成的 REST API 规划,包含四个 CRUD 操做node
CRUD | API Routes | Description |
---|---|---|
POST | /books | 增长一本书 |
GET | /books | 获取全部书籍列表 |
PUT | /books/:id | 根据 id 更新指定编号书籍 |
DELETE | /books/:id | 根据 id 删除指定编号书籍 |
├── app
│ ├── contrller # 控制层,解析用户输入数据,处理结果返回
│ ├── model # 数据库模型
│ ├── service # 业务逻辑层
│ └── utils # 工具类
├── config # 环境变量和配置相关
├── docs # 文档
├── tests # 单元测试
├── tsconfig.json # 指定 TypeScript 编译的参数信息
└── tslint.json # 指定 TypeScript 代码规范
├── .editorconfig # 约定编辑器的代码风格
├── .gitignore # git 提交忽略指定文件
├── .nycrc.json
├── package.json # package.json
├── serverless.yml # Serverless 配置文件
├── README.md
复制代码
使用这个 serverless-offline 插件能够在本地启动一个 HTTP 服务器模拟 API Gateway。git
安装github
npm install serverless-offline -D
复制代码
添加 serverless-offline 到 serverless.yml 文件typescript
plugins:
- serverless-offline
复制代码
零配置 TypeScript 支持的 ServerLess 插件,Github serverless-plugin-typescript数据库
安装npm
npm install -D serverless-plugin-typescript typescript
复制代码
添加 serverless-plugin-typescript 到 serverless.yml 文件,确保其位于 serverless-offline 以前json
plugins:
- serverless-plugin-typescript
- serverless-offline
复制代码
实际业务中,都会存在多套环境配置,例如:测试、预发、生产,那么在 Serverless 中如何作环境切换呢?api
修改 serverless.yml 文件为云函数配置环境变量,例如设置变量 NODE_ENV = devbash
provider:
environment:
NODE_ENV: dev
复制代码
修改 serverless.yml 文件,新增 exclude 和 incldue 配置,实现仅上传对应配置文件
package:
exclude:
- config/.env.stg
- config/.env.pro
include:
- config/.env.dev
复制代码
注:由于 TS 最终编译只会编译 .ts 结尾的文件,默认状况下 config 里面指定的配置文件是不会上传的
默认状况若是咱们设置了 .env 文件,dotenv 能够将此文件里设置的环境变量注入到 process.env 对象中,若是你有本身个性化定义的 .env 文件,在 dotenv 加载时指定 path 也可。
安装
npm i dotenv -S
npm i @types/dotenv-safe -D
复制代码
项目中使用
经过提取上面云函数中设置的环境变量 NODE_ENV,拼接路径 path 为 .env 指定文件路径
import dotenv from 'dotenv';
import path from 'path';
// 具体路径根据本身的项目配置来
const dotenvPath = path.join(__dirname, '../', `config/.env.${process.env.NODE_ENV}`);
dotenv.config({
path: dotenvPath,
});
复制代码
dotenv 环境变量配置参考
Serverless.yml 文件中经过 handler 指定函数的访问路径,http.path 指定访问的路由,method 指定函数的请求方法。
至关于传统应用开发中,咱们这样来定义 router.get('books/:id', () => { ... })
一个路由
functions:
create:
handler: app/handler.create
events:
- http:
path: books
method: post
findOne:
handler: app/handler.findOne
events:
- http:
path: books/{id}
method: get
复制代码
入口函数,利用函数的执行上下文重用,启动环境执行代码时初始化咱们的数据库连接、加载环境变量。
event、context 这些参数由 FaaS 平台提供,从 aws-lambda 中能够找到 Handler、Context 的声明,可是并无找到关于 event 的。
// app/handler.ts
import { Handler, Context } from 'aws-lambda';
import dotenv from 'dotenv';
import path from 'path';
const dotenvPath = path.join(__dirname, '../', `config/.env.${process.env.NODE_ENV}`);
dotenv.config({
path: dotenvPath,
});
import { books } from './model';
import { BooksController } from './contrller/books';
const booksController = new BooksController(books);
export const create: Handler = (event: any, context: Context) => {
return booksController.create(event, context);
};
export const findOne: Handler = (event: any, context: Context) => {
return booksController.findOne(event, context);
};
...
复制代码
经过路由指定和 handler 入口函数的处理,将用户的请求基于 Path 和 Method 分发至相应 Controller 层,解析用户的输入,处理后返回。
这一层不该存在任何形式的 “SQL 查询”,若有须要它应该调用 Service 层处理业务,而后封装结果返回。
// app/controller/books.ts
...
export class BooksController extends BooksService {
constructor (books: Model<any>) {
super(books);
}
/** * Create book * @param {*} event */
async create (event: any, context?: Context) {
console.log('functionName', context.functionName);
const params: CreateBookDTO = JSON.parse(event.body);
try {
const result = await this.createBook({
name: params.name,
id: params.id,
});
return MessageUtil.success(result);
} catch (err) {
console.error(err);
return MessageUtil.error(err.code, err.message);
}
}
/** * Query book by id * @param event */
async findOne (event: any, context: Context) {
// The amount of memory allocated for the function
console.log('memoryLimitInMB: ', context.memoryLimitInMB);
const id: number = Number(event.pathParameters.id);
try {
const result = await this.findOneBookById(id);
return MessageUtil.success(result);
} catch (err) {
console.error(err);
return MessageUtil.error(err.code, err.message);
}
}
...
}
复制代码
为了保证 Controller 层逻辑更加简洁,针对复杂的业务逻辑能够抽象出来作一个服务层,作到独立性、可复用性(能够被多个 Controller 层调用),这样也更有利于单元测试的编写。
// app/service/books.ts
...
export class BooksService {
private books: Model<any>;
constructor(books: Model<any>) {
this.books = books;
}
/** * Create book * @param params */
protected async createBook (params: CreateBookDTO): Promise<object> {
try {
const result = await this.books.create({
name: params.name,
id: params.id,
});
// Do something
return result;
} catch (err) {
console.error(err);
throw err;
}
}
/** * Query book by id * @param id */
protected findOneBookById (id: number) {
return this.books.findOne({ id });
}
...
}
复制代码
这一层连接咱们的 DB,定义咱们须要的 Schema,每一个 Schema 都会映射到一个 MongoDB Collection 中。
// app/model/mongoose-db.ts
import mongoose from 'mongoose';
export default mongoose.connect(process.env.DB_URL, {
dbName: process.env.DB_NAME,
useUnifiedTopology: true,
useNewUrlParser: true,
});
复制代码
// app/model/books.ts
import mongoose from 'mongoose';
export type BooksDocument = mongoose.Document & {
name: string,
id: number,
description: string,
createdAt: Date,
};
const booksSchema = new mongoose.Schema({
name: String,
id: { type: Number, index: true, unique: true },
description: String,
createdAt: { type: Date, default: Date.now },
});
// Note: OverwriteModelError: Cannot overwrite `Books` model once compiled. error
export const books = mongoose.models.books || mongoose.model<BooksDocument>('books', booksSchema, process.env.DB_BOOKS_COLLECTION);
复制代码
这些插件都有什么用途,下面会介绍。
npm i @types/lambda-tester @types/chai chai @types/mocha mocha ts-node -D
复制代码
之前咱们可使用 supertest 作接口测试,可是如今咱们使用 AWS Lambda 编写的 FaaS 函数则不能够这样作,例如请求中的 event、context 是与云厂商是有关联的,这里推荐一个 lambda-tester 能够实现咱们须要的接口测试。
安装
npm i lambda-tester @types/lambda-tester -D
复制代码
一个简单的应用示例
在接口的路径(path)上传入参数 id
lambdaTester(findOne)
.event({ pathParameters: { id: 25768396 } })
.expectResult((result: any) => {
...
});
复制代码
例如,咱们请求一个接口,接口内部依赖于 DB 获取数据,可是在作单元测试中咱们若是不须要获取实际的对象,就须要使用 Stub/Mock 对咱们的代码进行模拟操做。
安装
npm i sinon @types/sinon -D
复制代码
示例
如下例子中,我会作一个接口测试,经过 sinon 来模拟 mongoose 的各类方法操做。
const s = sinon
.mock(BooksModel);
s.expects('findOne')
.atLeast(1)
.atMost(3)
.resolves(booksMock.findOne);
// .rejects(booksMock.findOneError);
return lambdaTester(findOne)
.event({ pathParameters: { id: 25768396 } })
.expectResult((result: any) => {
// ...
});
复制代码
以上对 booksMock 的 findOne 作了数据返回 Mock 操做,使用 s.resolves 方法模拟了 fulfilled 成功态,如需测试 rejected 失败态需指定 s.rejects 函数。
一些经常使用方法
单元测试用来验证代码,测试覆盖率则能够验证测试用例,这里咱们选择使用 nyc。
安装
npm i nyc -D
复制代码
.nycrc.json 配置文件
{
"all": true, // 检测全部文件
"report-dir": "./coverage", // 报告文件存放位置
"extension": [".ts"], // 除了 .js 以外应尝试的扩展列表
"exclude": [ // 排除的一些文件
"coverage",
"tests"
]
}
复制代码
测试报告
下图是对本项目作的一个测试用例覆盖率报告。
npm install
安装须要的依赖npm run local
实际使用的是 serverless offline 在本地开启测试。$ npm run deploy
# or
$ serverless deploy
复制代码
指望的结果应该以下所示:
Serverless: Compiling with Typescript...
Serverless: Using local tsconfig.json
Serverless: Typescript compiled.
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service aws-node-rest-api-typescript.zip file to S3 (1.86 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
......................................
Serverless: Stack update finished...
Service Information
service: aws-node-rest-api-typescript
stage: dev
region: us-east-1
stack: aws-node-rest-api-typescript-dev
resources: 32
api keys:
None
endpoints:
POST - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
PUT - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
DELETE - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books/{id}
functions:
create: aws-node-rest-api-typescript-dev-create
update: aws-node-rest-api-typescript-dev-update
find: aws-node-rest-api-typescript-dev-find
findOne: aws-node-rest-api-typescript-dev-findOne
deleteOne: aws-node-rest-api-typescript-dev-deleteOne
layers:
None
Serverless: Removing old service artifacts from S3...
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
复制代码
使用 curl 之类的工具直接向端点发送一个 HTTP 请求。
curl https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
复制代码
服务上线以后不免有时会须要经过日志来排查问题,AWS 中咱们能够经过管理控制台和 CLI 本地化两种方式查看。
docs.aws.amazon.com/cli/latest/…
which aws 或 aws --version 命令检测是否安装成功,相似如下结果,安装成功
$ which aws
/usr/local/bin/aws
$ aws --version
aws-cli/2.0.12 Python/3.7.4 Darwin/19.3.0 botocore/2.0.0dev16
复制代码
安装成功,需先执行 aws configure 命令配置 aws-cli 和凭据
$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-1
Default output format [None]:
复制代码
区域名称 region 必定要配置,若是不知道的,当时 serverless deploy 的时候也有显示,能够留意下。
# 默认展现 base64 编码以后的数据
$ aws lambda invoke --function-name aws-node-rest-api-typescript-dev-find out-logger.json --log-type Tail
# base64 解码日志
$ aws lambda invoke --function-name aws-node-rest-api-typescript-dev-find out-logger.json --log-type Tail --query 'LogResult' --output text | base64 -d
复制代码
本示例项目,你能够在 Github 找到 Clone 下来进行学习。
仓库:github.com/Q-Angelo/aw… <-- 戳戳 Star
Serverless 下的云函数开发,可使咱们更关注于业务自己,从上面示例中也能够看到咱们的业务代码并无什么区别,更多的是避免了运维、后期的扩所容等一些成本问题,还有一点不一样的是入口函数,传统的应用开发咱们能够经过 HTTP 的 Request、Response 作处理和响应,例如在 AWS Lambda 下咱们则是经过 event、context 来处理请求和一些上下文信息。
FaaS 这一层应尽量的轻量,更多的是业务逻辑的处理,对于数据库这种是很难作到动态化、自动伸缩,可是若是每次冷启动都去建立连接对于数据库自己也会形成压力,一方面能够选择云平台提供的,另外一方面也能够本身数据库 BaaS 化,通过包装进行调用。
做者简介:五月君,Nodejs Developer,慕课网认证做者,热爱技术、喜欢分享的 90 后青年,欢迎关注Github 开源项目 www.nodejs.red