Skip to content
/ tsrpc-template Public template

TsRpc 应用框架封装,支持 日志,队列,限流,自定义请求等特性

Notifications You must be signed in to change notification settings

StringKe/tsrpc-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
陈泉
Nov 22, 2022
243ccd1 · Nov 22, 2022

History

23 Commits
Nov 21, 2022
Nov 22, 2022
Nov 22, 2022
Nov 18, 2022
Nov 22, 2022
Nov 20, 2022
Nov 18, 2022
Nov 21, 2022
Nov 21, 2022
Nov 18, 2022
Nov 21, 2022
Nov 22, 2022
Nov 22, 2022
Nov 22, 2022
Nov 22, 2022
Nov 22, 2022

Repository files navigation

TSRPC Template

客户端需要自行根据 src/shared/protocols/base.ts 将数据进行进行本地存储或使用 由于环境不同这里不提供具体实现代码

功能附加

  • 自定义请求封装,通过 src/trd/index.ts 可以自定义请求,同时会解析 url 参数和 body 序列化
  • Session 支持多种作用域方式 (public,private,user)
  • Throttler 频率限制,可以基于 key 或者 ip 或 用户 进行限制
  • 覆盖 tsrpc 日志,来自 @midwayjs/logger 支持日志审计,日志文件切割,日志级别控制

其他特性

  • Api 第三方调度,大小写忽略,api 文档中可忽略大小写
  • Prisma 类型支持,执行 prisma generate 自动生成 tsrpc 所需的类型定义
  • env 环境变量加载以及解析,同时支持类型定义和校验,代码中使用请导入 import { env } from '/src/env' 使用
  • 社会登陆 OAuth 支持 目前支持 QQ,如需要其他社会登陆请自行添加

第三方封装

  • tnwx 微信封装支持,已配置 redis 缓存,回调接口已封装,仅需在 kernal/wechat 中实现自己的 adapter 即可
  • Casbin 权限处理支持 ACL RBAC ABAC RESTful
  • Bullmq 队列, 通过修改 /src/index.ts 里的代码可以控制是否启用队列
  • COS,OSS,LOCAL 存储封装, 通过修改 /src/env/index.ts 里的代码可以检测环境变量,需要自行在 /src/index.ts 中初始化内容
  • nanoid 生成随机字符串,导入 /src/utils/naonid 使用

项目结构

  • /src/index.ts 项目入口,初始化 appserver,并启动服务
  • /src/kernel 大多数代码封装在此处
  • /src/env 环境变量加载,同时支持类型定义和校验
  • /src/queue 队列任务
  • /src/api/custom 自定义 api 请求

Session

  • Cookie 类似 Cookie 的使用方式,会发送给客户端,客户端可以读取,但无法修改, 通过 call.session.setPublic 或者 call.session.setPublic 设置
  • Session 不会发送给客户端,仅和当前客户端绑定, 通过 call.session.setPrivate 或者 call.session.setPrivate 设置
  • User 会发送给客户端,和登陆后用户绑定,同一个客户不同端可以互通, 通过 call.session.setUser 或者 call.session.setUser 设置
  • Device 用户设备管理,可以控制设备的登陆状态

自定义请求

import {ApiHandleMap} from './types'

export const TrdApis: ApiHandleMap = {
    '/test': (req, res) => {
        // 以扩展的参数解析,可以直接获取
        console.log(req.query, req.rawQuery, req.body, req.rawBody)
        // 一定需要通过 res.end() 结束请求,否则不会返回任何内容
        res.end('Hello World')
    },
}

如果需要实现约定式路由,可以修改 src/kernel/withHttpServer/index.ts 里的代码

React 客户端实现代码

如果你有一些自定义需求,请酌情修改

/* eslint-disable react-hooks/rules-of-hooks */
import dayjs from 'dayjs'
import {serviceProto} from '../shared/protocols/serviceProto'
import {BaseResponse} from '../shared/protocols/base'
import React from 'react'
import {
    ApiReturn,
    ApiReturnSucc,
    HttpClient,
    HttpClientOptions,
    HttpClientTransportOptions,
} from 'tsrpc-browser'
import {ApiReturnError, BaseServiceType, ServiceProto} from 'tsrpc-proto'
import {
    QueryClient,
    useMutation,
    UseMutationOptions,
    useQuery,
} from '@tanstack/react-query'
import {UseQueryOptions} from '@tanstack/react-query/src/types'

let publicStorage: BaseResponse['_publicData'] = undefined
export const queryClient = new QueryClient()

function checkExpired(expires: number | null | undefined) {
    if (expires) {
        return dayjs().isBefore(dayjs(expires))
    }
    return false
}

class NewHttpClient<ServiceType extends BaseServiceType,
    > extends HttpClient<ServiceType> {
    constructor(
        proto: ServiceProto<ServiceType>,
        options: Partial<HttpClientOptions>,
    ) {
        super(proto, options)
    }

    isSuccess<T extends string & keyof ServiceType['api']>(
        data: ApiReturn<ServiceType['api'][T]['res']>,
    ): data is ApiReturnSucc<ServiceType['api'][T]['res']> {
        return data.isSucc
    }

    callApiCatch<T extends string & keyof ServiceType['api']>(
        apiName: T,
        req: ServiceType['api'][T]['req'],
        options?: HttpClientTransportOptions,
    ): Promise<ApiReturnSucc<ServiceType['api'][T]['res']>> {
        return new Promise((resolve, reject) => {
            this.callApi(apiName, req, options)
                .then((data) => {
                    if (this.isSuccess(data)) {
                        return resolve(data.res)
                    } else {
                        return reject(data.err)
                    }
                })
                .catch(reject)
        })
    }

    getPublicData(key: string) {
        if (!publicStorage) {
            return undefined
        }
        if (publicStorage[key] && checkExpired(publicStorage[key]?.[0])) {
            return publicStorage[key]?.[1]
        }
        return undefined
    }

    usePublicData(key: string) {
        const [value, setValue] = React.useState(this.getPublicData(key))

        React.useEffect(() => {
            setValue(this.getPublicData(key))
        }, [key])

        return value
    }

    useQuery<T extends string & keyof ServiceType['api']>(
        apiName: T,
        req: ServiceType['api'][T]['req'],
        options?: Omit<UseQueryOptions<ServiceType['api'][T]['req'],
            ApiReturnError,
            ServiceType['api'][T]['res'],
            [T, ServiceType['api'][T]['req']]>,
            'queryKey' | 'queryFn' | 'initialData'> & { initialData?: () => undefined },
    ) {
        return useQuery<ServiceType['api'][T]['req'],
            ApiReturnError,
            ServiceType['api'][T]['res'],
            [T, ServiceType['api'][T]['req']]>([apiName, req], () => this.callApiCatch(apiName, req), options)
    }

    useMutation<T extends string & keyof ServiceType['api']>(
        name: T,
        options?: Omit<UseMutationOptions<ServiceType['api'][T]['res'],
            ApiReturnError,
            ServiceType['api'][T]['req'],
            unknown>,
            'mutationFn'>,
    ) {
        return useMutation((params: ServiceType['api'][T]['req'] = {}) => {
            return this.callApiCatch(name, params)
        }, options)
    }
}

// 创建全局唯一的 apiClient,需要时从该文件引入
export const apiClient = new NewHttpClient(serviceProto, {
    server: 'https://ap1.nat.xxika.com',
    json: true,
    logApi: true,
    logMsg: true,
})

apiClient.flows.preCallApiFlow.push((node) => {
    if (!publicStorage) {
        const localData = localStorage.getItem('publicData')
        if (localData) {
            publicStorage = JSON.parse(localData)
        }
    }
    // 附加公共数据
    if (publicStorage) {
        node.req._publicData = publicStorage
    }
    // 附加请求时间
    node.req._timestamp = Date.now().valueOf()
    return node
})

apiClient.flows.preApiReturnFlow.push(async (node) => {
    if (node.return && node.return.res && node.return.res._publicData) {
        publicStorage = node.return.res._publicData
        localStorage.setItem('publicData', JSON.stringify(publicStorage))
    }
    return node
})

export default apiClient

使用方式

'use client'

import React from 'react'
import apiClient from '上方代码的文件'

export default function Home() {
    // 主动请求方式
    const {mutateAsync} = apiClient.useMutation('auth/Social', {})

    // 轮训
    const {data, isLoading, isError} = apiClient.useQuery(
        'auth/Social',
        {
            id: '123',
        },
        {
            // 每 300ms 轮训一次
            refetchInterval: 300,
        },
    )

    React.useEffect(() => {
        console.log('query res', data, isLoading, isError)
    }, [data, isError, isLoading])

    const onClick = React.useCallback(() => {
        // 主动调度方式
        mutateAsync({
            id: '123',
        })
            .then((res) => {
                console.log('mutate res', res)
            })
            .catch((err) => {
                console.log('mutate err', err)
            })
    }, [mutateAsync])

    return <div onClick={onClick}> test < /div>
}

About

TsRpc 应用框架封装,支持 日志,队列,限流,自定义请求等特性

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages