查看完整項(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方法, 我們只需要為上一步中 controller 的 method 上面加上對(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 工具.
- 安裝并登錄
mysql創(chuàng)建一個(gè)數(shù)據(jù)庫
sudo apt install mysql-server mysql-client
mysql -u root -p
> create database test default charset=utf8;
編輯
ormconfig.js文件, 修改數(shù)據(jù)庫相關(guān)配置在
./src/entity目錄下面定義typeorm實(shí)體, 并定義實(shí)體的相關(guān)屬性, 詳細(xì)配置可參考 typeorm文檔在
controller中導(dǎo)入上一步中定義的模型, 使用方式參考./src/controlls/user.ts
使用事務(wù)
我們?cè)?ctx 中內(nèi)置了 typeorm 的 manager, 在控制器開始前開啟一個(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)注此倉庫...