fong: A service framework of node gRPC.
github: github.com/xiaozhongli…
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: github.com/xiaozhongli…java
使用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}`,
)
}
}
複製代碼
日誌文件:
請求日誌: ./log/request.<yyyyMMdd>.log
其餘日誌: ./log/common.<yyyyMMdd>.lognpm
請求日誌示例: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,
})),
}
}
複製代碼