簡介
fong: A service framework of node gRPC.
github: github.com/xiaozhongli…
fong是一個完全用typescript編寫的node gRPC框架, 可以基於它很方便地編寫gRPC微服務應用. 一般是用來編寫service層應用, 以供bff層或前端層等呼叫.
優點
1.純typescript編寫, typescript的好處不用多說了. 並且使用者使用這個框架框架時, 檢視定義都是ts原始碼, 使用者使用框架感受不到type definition檔案.
2.效仿egg.js的『約定優於配置』原則, 按照統一的約定進行應用開發, 專案風格一致, 開發模式簡單, 上手速度極快. 如果用過egg, 就會發現一切都是那麼熟悉.
對比
目前能找到的開源node gRPC框架很少, 跟其中star稍微多點的mali簡單對比一下:
對比方面 | mali | fong |
---|---|---|
專案風格約定 | √ | |
定義檢視跳轉 | definition | 原始碼 |
編寫語言 | javascript | typescript |
proto檔案載入 | 僅能載入一個 | 按目錄載入多個 |
程式碼生成 | √ | |
中介軟體 | √ | √ |
配置 | √ | |
日誌 | √ | |
controller載入 | √ | |
service載入 | 即將支援, 目前可以自己import即可 | |
util載入 | 即將支援, 目前可以自己import即可 | |
入參校驗 | 即將支援 | |
外掛機制 | 打算支援 | |
更多功能 | TBD |
示例
示例專案
github: github.com/xiaozhongli…
執行服務
使用vscode的話直接進F5除錯typescript.
或者:
npm start
複製程式碼
測試請求
ts-node tester
# 或者:
npm run tsc
node dist/tester.js
複製程式碼
使用
目錄約定
不同型別檔案只要按以下目錄放到相應的資料夾即可自動載入.
root
├── proto
| └── greeter.proto
├── config
| ├── config.default.ts
| ├── config.dev.ts
| ├── config.test.ts
| ├── config.stage.ts
| └── config.prod.ts
├── midware
| └── logger.ts
├── controller
| └── greeter.ts
├── service
| └── sample.ts
├── util
| └── sample.ts
└── typings
| ├── enum.ts
| └── indexed.d.ts
├── log
| ├── common.20190512.log
| ├── common.20190513.log
| ├── request.20190512.log
| └── request.20190513.log
├── app
├── packagen
├── tsconfign
└── tslintn
複製程式碼
入口檔案
import App from 'fong'
new App().start()
複製程式碼
配置示例
預設配置config.default.ts與環境配置config.<NODE_ENV>.ts是必須的, 執行時會合並.
配置可從ctx.config和app.config獲取.
import { AppInfo, Config } from 'fong'
export default (appInfo: AppInfo): Config => {
return {
// basic
PORT: 50051,
// log
COMMON_LOG_PATH: `${appInfo.rootPath}/log/common`,
REQUEST_LOG_PATH: `${appInfo.rootPath}/log/request`,
}
}
複製程式碼
中介軟體示例
注: req沒有放到ctx, 是為了方便在controller中支援強型別.
import { Context } from 'fong'
import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs'
dayjs.locale('zh-cn')
export default async (ctx: Context, req: object, next: Function) => {
const start = dayjs()
await next()
const end = dayjs()
ctx.logger.request({
'@duration': end.diff(start, 'millisecond'),
controller: `${ctx.controller}.${ctx.action}`,
metedata: JSON.stringify(ctx.metadata),
request: JSON.stringify(req),
response: JSON.stringify(ctx.response),
})
}
複製程式碼
controller示例
import { Controller, Context } from 'fong'
import HelloReply from '../typings/greeter/HelloReply'
export default class GreeterController extends Controller {
async sayHello(ctx: Context, req: HelloRequest): Promise<HelloReply> {
return new HelloReply(
`Hello ${req.name}`,
)
}
async sayGoodbye(ctx: Context, req: HelloRequest): Promise<HelloReply> {
return new HelloReply(
`Goodbye ${req.name}`,
)
}
}
複製程式碼
日誌
日誌檔案:
請求日誌: ./log/request.<yyyyMMdd>.log
其他日誌: ./log/common.<yyyyMMdd>.log
請求日誌示例:
{
"@env": "dev",
"@region": "unknown",
"@timestamp": "2019-05-12T22:23:53.181Z",
"@duration": 5,
"controller": "Greeter.sayHello",
"metedata": "{\"user-agent\":\"grpc-node/1.20.3 grpc-c/7.0.0 (osx; chttp2; godric)\"}",
"request": "{\"name\":\"world\"}",
"response": "{\"message\":\"Hello world\"}"
}
複製程式碼
程式碼生成
程式碼生成器還未單獨封包, 現在放在示例應用的codegen目錄下.
使用方法:
1.定義好契約proto, 確保格式化了內容.
2.執行程式碼生成邏輯:
ts-node codegen
複製程式碼
這樣就會生成controller及相關請求/響應的interface/class, 未來會支援更多型別的檔案的生成.
3.從./codegen/dist目錄將生成的controller檔案移入./controller資料夾並開始編寫方法內部邏輯.
定義檢視跳轉
Peek Definition直接指向原始碼.
近期計劃
service載入
service檔案放到service資料夾即可自動載入. 通過ctx.<service>使用.
util載入
util檔案放到util資料夾即可自動載入. 通過ctx.util.<function>使用.
入參校驗
把在這裡用的引數校驗中介軟體搬過來, 用class-validator和class-transformer實現校驗, 支援自動生成.
應用內的request model將會類似:
import { IsOptional, Length, Min, Max, IsBoolean } from 'class-validator'
export default class IndexRequest {
@Length(4, 8)
@IsOptional()
foo: string
@Min(5)
@Max(10)
@IsOptional()
bar: number
@IsBoolean()
@IsOptional()
baz: boolean
}
複製程式碼
框架內的validate midware將會類似:
import { Context } from 'egg'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
import HomeIndexRequest from '../request/home/IndexRequest'
import HomeValidateRequest from '../request/home/ValidateRequest'
const typeMap = new Map([
['Home.index', HomeIndexRequest],
['Home.validate', HomeValidateRequest],
])
export default async (ctx: Context, next: Function) => {
const type = typeMap.get(ctx.routerName)
const target = plainToClass(type, ctx.query)
const errors = await validate(target)
if (!errors.length) return next()
ctx.body = {
success: false,
message: errors.map(error => ({
field: error.property,
prompt: error.constraints,
})),
}
}
複製程式碼