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

簡介

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),
    })
}

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}`,
        )
    }
}

日誌

日誌文件: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文件放到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,
        })),
    }
}
相關文章
相關標籤/搜索