fong - 純typescript的node gRPC微服務框架

xiaozhong發表於2019-05-13

簡介

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直接指向原始碼.

fong - 純typescript的node gRPC微服務框架

近期計劃

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,
        })),
    }
}
複製程式碼

相關文章