使用 TypeScript 快速開發 Serverless REST API

這是一個對於 AWS Lambda Functions 的簡單 REST API 項目,使用 TypeScript 語言編寫,數據存儲採用 MongoDB Atlas 雲數據庫,從編碼到 AWS Lambda 下的單元測試,再到部署、日誌調試完整的介紹瞭如何快速編寫一個 FaaS 函數。html

本文你將學習到

  • REST API with typescript
  • MongoDB Atlas data storage
  • Multi-environment management under Serverless
  • Mocha unit tests and lambda-tester interface test
  • AWS lambda function log view

REST API 規劃

如下是咱們將要完成的 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 相關插件

serverless-offline

使用這個 serverless-offline 插件能夠在本地啓動一個 HTTP 服務器模擬 API Gateway。git

安裝github

npm install serverless-offline -D
複製代碼

添加 serverless-offline 到 serverless.yml 文件typescript

plugins:
 - serverless-offline
複製代碼

serverless-plugin-typescript 插件

零配置 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
複製代碼

配置文件上傳時的 incldue 和 exclude

修改 serverless.yml 文件,新增 exclude 和 incldue 配置,實現僅上傳對應配置文件

  • exclude: 要忽略的配置文件
  • include: 指定的配置文件會被上傳
package:
 exclude:
 - config/.env.stg
 - config/.env.pro
 include:
 - config/.env.dev
複製代碼

注:由於 TS 最終編譯只會編譯 .ts 結尾的文件,默認狀況下 config 裏面指定的配置文件是不會上傳的

Dotenv 模塊

默認狀況若是咱們設置了 .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 環境變量配置參考

  • github.com/motdotla/dotenv
  • serverlesscloud.cn/best-practice/2020-03-10-serverless-env

編碼實踐核心講解

路由指定

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
複製代碼

handler 入口函數處理

入口函數,利用函數的執行上下文重用,啓動環境執行代碼時初始化咱們的數據庫連接、加載環境變量。

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);
};
...
複製代碼

Controller 控制器層

經過路由指定和 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);
    }
  }
  ...
}
複製代碼

Service 服務層

爲了保證 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 });
  }
  ...
}
複製代碼

Model 數據層

這一層連接咱們的 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
複製代碼

lambda-tester

之前咱們可使用 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) => {
    ...
  });
複製代碼

sinon

例如,咱們請求一個接口,接口內部依賴於 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 函數。

一些經常使用方法

  • s.atLeast(1) 最少調用一次。
  • s.atMost(3) 最多調用三次。
  • s.verify() 用來驗證 findOne 這個方法是否知足上面的條件。
  • s.restore() 使用後復原該函數,適合於對某個函數的屬性進行屢次 stub 操做。

測試覆蓋率

單元測試用來驗證代碼,測試覆蓋率則能夠驗證測試用例,這裏咱們選擇使用 nyc

安裝

npm i nyc -D
複製代碼

.nycrc.json 配置文件

{
  "all": true, // 檢測全部文件
  "report-dir": "./coverage", // 報告文件存放位置
  "extension": [".ts"], // 除了 .js 以外應嘗試的擴展列表
  "exclude": [ // 排除的一些文件
    "coverage",
    "tests"
  ]
}
複製代碼

測試報告

下圖是對本項目作的一個測試用例覆蓋率報告。

圖片描述

Deploy And Usage

本地部署測試

  • 運行 npm install 安裝須要的依賴
  • 運行 npm run local 實際使用的是 serverless offline 在本地開啓測試。

在 AWS 上的部署, 運行:

$ 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.
複製代碼

Usage

使用 curl 之類的工具直接向端點發送一個 HTTP 請求。

curl https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/books
複製代碼

AWS Lambda 查看 Serverless 函很多天志

服務上線以後不免有時會須要經過日誌來排查問題,AWS 中咱們能夠經過管理控制檯和 CLI 本地化兩種方式查看。

AWS 管理控制檯查看

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

本示例項目,你能夠在 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

相關文章
相關標籤/搜索