fong: A service framework of node gRPC.
github: https://github.com/xiaozhongliu/fong
fong是一個徹底用typescript編寫的node gRPC框架, 能夠基於它很方便地編寫gRPC微服務應用. 通常是用來編寫service層應用, 以供bff層或前端層等調用.javascript
1.純typescript編寫, typescript的好處不用多說了. 而且用戶使用這個框架框架時, 查看定義都是ts源碼, 用戶使用框架感覺不到type definition文件.
2.效仿egg.js的『約定優於配置』原則, 按照統一的約定進行應用開發, 項目風格一致, 開發模式簡單, 上手速度極快. 若是用過egg, 就會發現一切都是那麼熟悉.html
目前能找到的開源node gRPC框架不多, 跟其中star稍微多點的mali簡單對比一下:前端
對比方面 | mali | fong |
---|---|---|
項目風格約定 | √ | |
定義查看跳轉 | definition | 源代碼 |
編寫語言 | javascript | typescript |
proto文件加載 | 僅能加載一個 | 按目錄加載多個 |
代碼生成 | √ | |
中間件 | √ | √ |
配置 | √ | |
日誌 | √ | |
controller加載 | √ | |
service加載 | 即將支持, 目前能夠本身import便可 | |
util加載 | 即將支持, 目前能夠本身import便可 | |
入參校驗 | 即將支持 | |
插件機制 | 打算支持 | |
更多功能 | TBD |
github: https://github.com/xiaozhongliu/ts-rpc-seedjava
使用vscode的話直接進F5調試typescript.
或者:node
npm start
ts-node tester # 或者: npm run tsc node dist/tester.js
不一樣類型文件只要按如下目錄放到相應的文件夾便可自動加載.git
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獲取.github
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中支持強類型.typescript
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), }) }
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}`, ) } }
日誌文件:npm
請求日誌: ./log/request.\<yyyyMMdd>.log 其餘日誌: ./log/common.\<yyyyMMdd>.log
請求日誌示例:json
{ "@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文件夾便可自動加載. 經過ctx.<service>使用.
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, })), } }