天天看點

使用 TypeScript 快速開發 Serverless REST APIor預設展示 base64 編碼之後的資料base64 解碼日志

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

本文你将學習到

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 操作

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。

安裝

npm install serverless-offline -D

添加 serverless-offline 到 serverless.yml 檔案

plugins:

serverless-plugin-typescript 插件

零配置 TypeScript 支援的 ServerLess 插件,Github serverless-plugin-typescript

npm install -D serverless-plugin-typescript typescript

添加 serverless-plugin-typescript 到 serverless.yml 檔案,確定其位于 serverless-offline 之前

  • serverless-plugin-typescript

多配置環境管理

實際業務中,都會存在多套環境配置,例如:測試、預發、生産,那麼在 Serverless 中如何做環境切換呢?

為雲函數配置環境變量

修改 serverless.yml 檔案為雲函數配置環境變量,例如設定變量 NODE_ENV = dev

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';

config/.env.${process.env.NODE_ENV}

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) {

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;

constructor(books: Model) {

this.books = books;           
  • @param params

protected async createBook (params: CreateBookDTO): Promise

{
try {
  const result = await this.books.create({
    name: params.name,
    id: params.id,
  });

  // Do something

  return result;
} catch (err) {
  console.error(err);

  throw err;
}           
  • @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

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('books', booksSchema, process.env.DB_BOOKS_COLLECTION);

單元測試

安裝插件

這些插件都有什麼用途,下面會介紹。

npm i @types/lambda-tester @types/chai chai @types/mocha mocha ts-node -D

lambda-tester

以前我們可以使用 supertest 做

買QQ靓号平台

接口測試,但是現在我們使用 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 -

GET -

DELETE -

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:

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 管理控制台檢視

打開 CloudWatch 控制台的日志頁面。

選擇您的函數 (/aws/lambda/function-name) 的日志組。

選擇清單中的日志流。

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 化,經過包裝進行調用。