使用typescript&koa&typeorm開發(fā)后端api服務(wù)器

查看完整項(xiàng)目 請(qǐng)移步至 github

joi, swagger, koa 通過 decorator 的方式連接起來, 這是此項(xiàng)目的開發(fā)初衷.

[x] koa生態(tài)系統(tǒng)
[x] jwt登錄驗(yàn)證
[x] 自動(dòng)生成swagger接口文檔
[x] nodemon 自動(dòng)重啟
[x] 使用 decorator 的方式完成接口參數(shù)驗(yàn)證
[x] controller 錯(cuò)誤自動(dòng)捕捉
[x] typescript 支持
[x] 基于 typeorm 的數(shù)據(jù)庫 orm 支持
[x] mysql 事務(wù)支持(隔離支持)
[ ] 根據(jù)typeorm實(shí)體自動(dòng)生成swagger的definition

開始使用

定義 controller

應(yīng)用中所有請(qǐng)求均為一個(gè) js class, 加上 @controller 即可

import {
  controller,
} from "../decorators";

@controller('/example')
export default class ExampleController {
   /* whatever you like */
}

上述例子即可實(shí)現(xiàn)一個(gè)基礎(chǔ) controller 的定義, @controller 中參數(shù)即為此控制器對(duì)應(yīng)的響應(yīng)路徑前綴.

當(dāng)然要完成一個(gè)簡單請(qǐng)求, 這是不夠的, 因?yàn)檫€沒有定義與之相關(guān)的處理方法

定義處理函數(shù)

應(yīng)用包裝了 @get, @post, @put, @delete, @update 等常用http方法, 我們只需要為上一步中 controllermethod 上面加上對(duì)應(yīng)方法的裝飾器即可.

import {
  get,
  controller,
} from "../decorators";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  async Hello() {
    return 'hello world'
  }
}

現(xiàn)在, 切換到項(xiàng)目根目錄, 執(zhí)行 npm start, 打開瀏覽器在地址欄輸入 http://localhost:3002/example/hello 即可.

處理響應(yīng)

通過包裝 koa 的響應(yīng)方法, 現(xiàn)在我們只需要在 controller 的方法下面直接 return 需要響應(yīng)的值即可, 裝飾器會(huì)接收響應(yīng)參數(shù)并返回到瀏覽器.

需要注意: 這意味著我們必須顯示return一個(gè)值給裝飾器

如以下行為是不被建議的:

import {
  get,
  controller,
} from "../decorators";

export default class ExampleController {
  @get('/hello')
  async Hello() {
     // bad 沒有顯示的return
  }

  @get('/hello')
  async Hello() {
     // bad 沒有return任何有價(jià)值的東西
     return ;
  }

  @get('/hello')
  async Hello() {
     // bad 同上, 沒有return任何有價(jià)值的東西
     return undefined;
  }
  
  @get('/hello')
  async Hello(ctx) {
     // bad 請(qǐng)不要直接使用ctx.body = anything; 這會(huì)被覆蓋
     ctx.body = 'hello';
     return 'world';
  }

  @get('/hello')
  async Hello(ctx) {
     // bad 這樣實(shí)際是可以運(yùn)行的, 但是仍然不推薦使用
     ctx.body = 'hello';
  }

  @get('/hello')
  async Hello() {
     // good! nice!
     return = 'hello';
  }
}

處理請(qǐng)求

api 系統(tǒng)中, 參數(shù)不可避免, 而且在處理方法內(nèi)部對(duì)參數(shù)進(jìn)行校驗(yàn)這實(shí)際會(huì)寫上很多的樣板代碼, 也影響業(yè)務(wù)邏輯.
因此, 我們采用 joi 進(jìn)行參數(shù)驗(yàn)證.

import {
  get,
  parameter,
  controller,
} from "../decorators";
import * as joi from 'joi';
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  @parameter('params', joi.object().keys({
    a: joi.number().required(),
    b: joi.string()
  }), ENUM_PARAM_IN.body)
  @parameter('userId', joi.number().integer().description('用戶id').required(), ENUM_PARAM_IN.path)
  @parameter('param1', joi.string().description('其他參數(shù)').required(), ENUM_PARAM_IN.query)
  async Hello(ctx: IContext) {
       // IContext 為定義的一個(gè)ts類型, 擴(kuò)展了 koa 的 ctx
       // 為ctx加上了 $getParams 方法, 方便獲取驗(yàn)證成功后的請(qǐng)求參數(shù)
       return ctx.$getParams();
  }
}

如上: 通過 @parameter 裝飾器我們可以很方便定義接口參數(shù), 并借助 joi 的魔力對(duì)其進(jìn)行驗(yàn)證
系統(tǒng)可以對(duì) querystring, path, body 進(jìn)行參數(shù)驗(yàn)證
ENUM_PARAM_IN 為系統(tǒng)定義的 enum, 包含 'path' | 'body' | 'query', 默認(rèn) query

處理錯(cuò)誤

每個(gè)控制器均支持錯(cuò)誤 Error Catch 的能力

我們可以直接在應(yīng)用中使用 throw 拋出錯(cuò)誤, 應(yīng)用在方法外層會(huì)自動(dòng)捕捉并返回給前端, 參考以下示例

import {
  get,
  controller,
} from "../decorators";
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @get('/hello')
  async Hello() {
     // 拋出一個(gè)默認(rèn)的 500 錯(cuò)誤, error message會(huì)默認(rèn)發(fā)送給前端
     throw new Error('just an error');
  }
  @get('/hello')
  async Hello(ctx: IContext) {
     // 通過ctx.throw 我們可以拋出一個(gè)自定義狀態(tài)碼的錯(cuò)誤
     ctx.throw(400, 'just an error');
  }
}

生成 swagger

應(yīng)用提供了 @description, @tag, @summary, @response 等裝飾器來處理swagger的情況

import {
  post,
  tag,
  summary,
  response,
  controller,
} from "../decorators";
import * as omit from 'omit.js';
import * as joi from 'joi';
import * as jwt from "jsonwebtoken";
import { User } from '../entity/User';
import { AppConfig } from '../utils/config';
import UserSchema from "../definitions/User";
import { Like } from "typeorm";
import { IContext } from "../decorators/interface";

@controller('/example')
export default class ExampleController {
  @post('/login')
  @parameter(
    'body', 
    joi.object().keys({
      name: joi.string().required().description('用戶名'),
      password: joi.string().required().description('密碼'),
    }), ENUM_PARAM_IN.body
  )
  @description('用戶登錄例子')
  @tag('用戶管理')
  @summary('用戶登錄')
  @response(200, {
    user: { $ref: UserSchema, desc: '用戶信息' },
    token: joi.string().description('token, 需要每次在請(qǐng)求頭或者cookie中帶上'),
  })
  async login(ctx: IContext) {
    const { name, password }: User = ctx.$getParams();
    const user: User = await User.findOne({ name });
    if (!user || user.password !== password) {
      throw new Error('用戶名密碼不匹配');
    }
    const token = jwt.sign({
      data: user.id
    }, AppConfig.appKey, { expiresIn: 60 * 60 });
    ctx.cookies.set('token', token);
    return {
      token,
      user: omit(user, ['password'])
    };
  }
}

現(xiàn)在, 打開瀏覽器, 在地址欄輸入 http://localhost:3002/docs, 即可查看生成的接口swagger文檔

user: { $ref: UserSchema, desc: '用戶信息' }, 這里面的 $ref, 即轉(zhuǎn)換后的 swagger definition, 在./src/definitions目錄下即可定義

使用數(shù)據(jù)庫

應(yīng)用使用 typeorm 來作為數(shù)據(jù)庫的 orm 工具.

  1. 安裝并登錄 mysql 創(chuàng)建一個(gè)數(shù)據(jù)庫
sudo apt install mysql-server mysql-client

mysql -u root -p

> create database test default charset=utf8;
  1. 編輯 ormconfig.js 文件, 修改數(shù)據(jù)庫相關(guān)配置

  2. ./src/entity 目錄下面定義 typeorm 實(shí)體, 并定義實(shí)體的相關(guān)屬性, 詳細(xì)配置可參考 typeorm文檔

  3. controller 中導(dǎo)入上一步中定義的模型, 使用方式參考 ./src/controlls/user.ts


使用事務(wù)

我們?cè)?ctx 中內(nèi)置了 typeormmanager, 在控制器開始前開啟一個(gè) typeorm 的事務(wù), 檢測到應(yīng)用內(nèi)拋出的異常之后, 則自動(dòng)回滾事務(wù), 若應(yīng)用正常被處理, 則自動(dòng)提交事務(wù).

注意: 因?yàn)樾枰獧z測應(yīng)用內(nèi)異常, 所以只能通過throw 方式拋出的異常才能被正確處理, 而不能使用ctx.throw

一個(gè)栗子:

  /**
   * 新增用戶
   */
  @post('/add')
  @tag('用戶管理')
  @parameter(
    'body', 
    joi.object().keys({
      name: joi.string().required().description('用戶名'),
      password: joi.string().required().description('密碼'),
    }), ENUM_PARAM_IN.body
  )
  @summary('添加管理員')
  @login_required()
  @response(200, { $ref: UserSchema })
  async addUser(ctx: IContext) {
    const userInfo: User = ctx.$getParams();
    const lastUser = await User.findOne({ name: userInfo.name });
    if (lastUser) {
      throw new Error('用戶已存在');
    }
    const user = new User(userInfo);
    await ctx.manager.save(user);
    return omit(user, ['password']);
  }

登錄驗(yàn)證

系統(tǒng)提供了 login_required 裝飾器, 使用時(shí)加上即可, 栗子見上面

目錄說明

.
├── ./ormconfig.js  // typeorm配置文件
├── ./package.json 
├── ./package-lock.json
├── ./readme.md // readme
├── ./src // 源碼目錄
│   ├── ./upload // 文件上傳目錄
│   ├── ./src/controllers // 控制器相關(guān)
│   │   ├── ./src/controllers/example.ts 
│   ├── ./src/decorators // decorators相關(guān)
│   │   ├── ./src/decorators/controller.ts
│   │   ├── ./src/decorators/definition.ts
│   │   ├── ./src/decorators/description.ts
│   │   ├── ./src/decorators/index.ts
│   │   ├── ./src/decorators/interface.ts
│   │   ├── ./src/decorators/ischema.ts
│   │   ├── ./src/decorators/login_required.ts
│   │   ├── ./src/decorators/method.ts
│   │   ├── ./src/decorators/parameter.ts
│   │   ├── ./src/decorators/response.ts
│   │   ├── ./src/decorators/summary.ts
│   │   ├── ./src/decorators/tag.ts
│   │   └── ./src/decorators/utils.ts
│   ├── ./src/definitions // 主要用于swagger中模型
│   │   ├── ./src/definitions/BaseSchema.ts
│   │   └── ./src/definitions/User.ts
│   ├── ./src/entity // 數(shù)據(jù)庫表(typeorm實(shí)體)
│   │   ├── ./src/entity/BaseEntity.ts
│   │   └── ./src/entity/User.ts
│   ├── ./src/main.ts // 應(yīng)用入口
│   └── ./src/utils // 工具目錄
│       ├── ./src/utils/config.ts // 應(yīng)用配置
│       ├── ./src/utils/JoiToSwagger.ts 
│       └── ./src/utils/middlewares.ts // koa middleware相關(guān)
├── ./tsconfig.json // ts配置
├── ./tslint.json
├── ./typings.json
└── ./yarn.lock

持續(xù)更新中, 更多功能請(qǐng)關(guān)注此倉庫...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容