ppfly
TypeScript icon, indicating that this package has built-in type declarations

2.7.7 • Public • Published

✪ 关于ppfly

ppfly是一个基于Typescript和 egg.js 的轻量级框架,目的是实现高度统一的开发规范、降低开发难度、大幅度提高开发效率,让后端开发人员可以更加专注业务逻辑的实现,而不是把时间浪费在写重复代码、写API文档之类的事情。

  • 使用装饰器 @action 完成路由设置、参数验证
  • 完善数据验证和角色验证流程
  • 约定了统一消息交换格式,包括成功消息、失败消息、分页数据格式
  • 统一异常处理流程,实现不依赖任何插件
  • 自动生成API文档(运行时动态生成,自带一个比swagger更强大的API文档浏览客户端)
  • 分布式缓存、MQ、文件图片上传处理等
  • 根据模型定义自动生成模型的接口、参数验证规则(需要配合ppfly的vscode插件)

✪ 目录结构

app
├── config (可选)
├── controller (egg目录规范,保存控制器)
├── model (egg目录规范,保存数据库数据模型)
├── permission (可选,保存权限配置)
├── rule  (必须,保存参数验证规则)
└── service (egg目录规范)

✪ 请求的处理流程

┌───────────┐          ┌──────────────┐          ┌────────┐          ┌────────┐
│ 客户端请求 │  =====>  │ Interceptors │  =====>  │ action │  =====>  │ Result │ 
└───────────┘          └──────────────┘          └────────┘          └────────┘
  • Interceptor(拦截器): 主要实现参数验证(Validator)、权限验证(RoleAuthorize)
  • Result(结果渲染器): 输出结果,框架实现的主要是 :ActionResult、 ViewResult、 JsonResult、XmlResult、 FileResult、 RedirectResult、 StatusResult
  • action 为真正的业务逻辑处理方法

✪ 返回消息格式

public static readonly SUCCESS_CODE = 200;
public static readonly ERROR_CODE = -1000;
 
export interface IResult {
    /**
     * 错误代码:成功为正数(默认200),失败为负数(默认-1000)
     */
    codenumber;
 
    /**
     * 操作是否成功
     */
    successboolean;
 
    /**
     * 具体的消息描述
     */
    msgstring;
 
    /**
     * 返回数据
     */
    data?: any;
}
  • 成功执行:Result.success(msg: string, data?: any)
  • 失败执行:Result.fail(msg: string, code: number = Result.ERROR_CODE, data?: any)
  • 异常执行:Result.error(err: BaseError, data?: any)

在ppfly的世界里,服务端在正常响应情况下均返回HTTP CODE 200,所以前端判断是否执行成功、有无出现异常要解析HTTP返回的数据内容。通常只会出现3种情况:

  1. {code: 200, success: true, msg: ...}
  2. {code: -XXX, success: false, msg: ...}
  3. 客户端需要的数据内容

前端判断是否异常,可以借鉴以下代码:

import axios from 'axios';
const service = axios.create({
    baseURL: API_URL,
    timeout: 15000
})
service.interceptors.response.use(
    response => {
        store.commit(LOADING_SET_STATUS, false);
        Toast.clear();
        const { data } = response;
        if (typeof data === 'object'
            && typeof data.success === 'boolean'
            && typeof data.msg === 'string') {
            if (!data.success) {
                // 服务器返回错误信息
                Toast.fail(data.msg);
                if (data.type === 'error.timeout') {
                    router.replace({ path: '/login', query: { timeout: true } });
                    return;
                }
                // 返回空的数据
                return;
            }
        }
        return response.data;
    },
    error => {
        store.commit(LOADING_SET_STATUS, false);
        // 服务器无法响应,返回空的数据
        Toast.fail("服务器暂时无法响应您的请求,请稍后重试。");
        return;
    }
)

框架声明的异常类型主要有:

异常名称 code 附加数据
ActionError -1000 {type: 'error.action'}
APIError ----- {type: 'error.api'}
ValidateError -10086 {type: 'error.validate', errors: [{ field:'', message:'' }] }
TimeoutError -10010 {type: 'error.timeout'}
AccessError -10020 {type: 'error.access'}

✪ 分页数据格式

/**
 * 分页信息约定
 */
export interface IPageInfo {
    /**
     * 页面ID,第一页值为1
     */
    pageIdnumber;
 
    /**
     * 每页数据数量
     */
    pageSizenumber;
 
    /**
     * 数据总数
     */
    dataCountnumber;
 
    /**
     *  游标信息
     */
    cursor?: string | number;
}
 
/**
 * 分页数据约定
 */
export interface IPagingData<TData> {
    /**
     * 分页信息
     */
    pageInfoIPageInfo;
 
    /**
     * 分页数据
     */
    dataArray<TData>;
}

只需要一句代码即可实现分页数据返回(目前只支持MongoDB)

import PagingService from 'ppfly/service/paging';
 
export default class UserService extends Service {
    public async searchUser(filter: any, pageInfo: any, orderBy: any): IPagingData {
        return PagingService.getPagingData(this.app.model.User, filter, pageInfo, orderBy);
    }
}

注意: 框架为了优化性能,只会在第一页(pageInfo.pageId为1,且pageInfo.dataCount为0)的情况下才会查询数据总数并设置pageInfo.dataCount,其余情况下pageInfo.dataCount为前端传过来的值或者0。

✪ @action 和 @role 修饰符

通过使用@action修饰符可以实现路由注册、指定参数验证规则(自动注入)、指定使用的拦截器和渲染器。

// @controller主要是为了API文档生成,没有其他用途
@controller('home', '首页')
export default class HomeController extends Controller {
 
    @role(['权限1', '权限2'])
    @action({
        methods: [HttpMethod.GET, HttpMethod.POST],
        name: '首页',
        path: '/home/index',
        params:{
            id: ...
        }
    })
    public async home({id}) {
        // 这里可以不返回数据,默认返回 ActionResult.create(Result.success(`${action}执行成功`))
        // 也可以直接返回数据或者 IActionResult
        // return StatusResult.create(404)
        // return RedirectResult.create('/404.html')
        // return XmlResult.create(...);
        // 业务逻辑出现错误可以直接抛出 throw new ActionError('....')
 
        return 'hello';  // 等同:return ActionResult.create('hello'); 
    }
}

需要在 app/router.ts 中注册

export default (app: Application) => {
  // 注册路由
  Action.registerRouter(app);
};

@action 修饰符的参数 ActionConfig定义如下:

/**
 * Action配置信息
 */
export default interface ActionConfig {
    /**
     * Action的名称
     */
    name: string;
 
    /**
     * 路由地址
     */
    path: string;
 
    /**
     * HTTP 方法,可以是数组也可以是单个值, 默认为 HttpMethod.POST
     */
    methods?: string | string[];
 
    /**
     * Action 方法参数注入;
     * 如果值类型为字符串数组,则通过全局的规则验证;
     * 如果值类型为对象,则根据对象中每个字段的配置验证;
     */
    params?: string[] | object;
 
    /**
     * Action 结果渲染器, 默认为 ActionType.action
     */
    result?: string | IActionResult;
 
    /**
     * Action 请求拦截器,默认为 ['validator','role']
     */
    interceptors?: string[] | Interceptor[];
 
    /**
     *  指定Action返回的数据模型
     */
    model?: string;
 
    /**
     * 使用中间件
     */
    middleware?: any;
 
    /**
     * 自定义附加数据
     */
    data?: object;
}

✪ 统一的异常处理流程

首先在 /app.ts 中注册

export default (app: Application) => {
    app.beforeStart(async () => {
        ....
    });
 
    // 配置异常处理
    Action.handleError(app, new DefaultErrorHandler());
};
// 代码:ppfly/handle/error
// 也可以自己实现 IErrorHandler
export default class DefaultErrorHandler implements IErrorHandler {
    public async handle(ctx, err) {
        let data = {};
        if (err instanceof ValidateError) {
            data = Result.error(err, { errors: err.errors });
        }
        else if (err instanceof APIError) {
            data = Result.error(err);
        }
        else if (err instanceof ActionError) {
            data = Result.error(err);
        }
        else if (err instanceof TimeoutError) {
            data = Result.error(err);
        }
        else if (err instanceof AccessError) {
            data = Result.error(err);
        }
        else {
            ctx.logger.error('未知异常', err);
            data = Result.fail('服务端暂时无法处理您的请求。', -1024);
        }
        return data;
    }
};

✪ 参数验证

export default (app: Application) => {
    app.beforeStart(async () => {
       ...
    });
 
    // 配置参数验证器
    Validator.service = new ParameterValidator();
};
  • 在app.ts注册验证器后就可以在@action修饰器中指定验证规则
  • 目前框架只有一个ValidatorService实现:ParameterValidator,部分规则基于validator.js
  • 验证器支持数组的结构验证,也支持嵌套验证
  • 验证有错误就会抛 ValidateError (ppfly/error/validate)
 @action({
        ...
        params: {
            id: {
                type: 'string',
                min: 24,
                max: 24,
                memo: 'ID',
                message: '参数ID验证失败'
            },
            images: {
                type: 'array',
                min: 1,
                required: false,
                schema: {
                    url: {
                        type: 'string'
                        ...
                    }
                }
            },
            xxx: {
                type: {
                    type: 'number',
                    ...
                }
            }
        }
    })
public async test({ id, images, xxx }){
    console.log( id, images, xxx)
}

目前验证规则支持以下参数:

参数名称 说明
type 值类型,必填
memo 描述(备忘),选填
required 是否必须,选填, 默认值为 true
message 错误消息,选填
min 最小值,当类型为字符串时代表最小长度(同len属性),当类型为数组时代表数组最小长度
max 最大值

当类型为枚举(enum)时,支持以下扩展参数:

参数名称 说明
value 枚举值类型
values 枚举可使用的值

当类型为数组(array)时,支持以下扩展参数:

参数名称 说明
schema 数组内的元素结构规则描述

其中参数type支持的类型有:

export enum ParamType {
    NUMBER = 'number',
    INTEGER = 'int',
    FLOAT = 'float',
    STRING = 'string',
    DATE = 'date',
    BOOLEAN = 'bool',
    ARRAY = 'array',
    ENUM = 'enum',
    EMAIL = 'email',
    URL = 'url',
    HASH = 'hash',
    JSON = 'json',
    JWT = 'jwt',
    PHONE = 'phone',
    OBJECT_ID = 'objectId'
}

✪ 角色验证

export default (app: Application) => {
    app.beforeStart(async () => {
       ...
    });
 
   // 配置全局角色验证
    RoleAuthorize.service = new RoleValidator();
};
export default class RoleValidator implements RoleService {
    async validate(target: any, metadata: ActionMetaData, permissions: string[]) {
        // permissions => 'XX权限'
         if (!permissions || permissions.length === 0) {
            return;
        }
        const { ctx, app } = target;
        const token = ctx.request.header.authorization;
        const session: IUserSession = await ctx.service.session.getSession(token);
         if (!session) {
            throw new TimeoutError('令牌过期,请重新登录。');
        }
        const group = await ctx.service.group.getGroupByName(session.group);
        if(!group.hasPermission(permissions)){
            throw new AccessError('执行[' + metadata.config.name + ']无权限.');
        }
        // 保存全局
        ctx.user = session;
    }
}
@role(['XX权限'])
public async test(){
    console.log(this.ctx.user);
}

✪ API文档生成

import ApiDoc from 'ppfly/api';
 
@action({
    name: 'API文档生成',
    path: '/dev/api',
})
public async api(params) {
    const configs = await this.service.config.getAllConfig();
 
    const data: any = ApiDoc.build(this.ctx, {
        info: {
            title: 'XXX商城',
            description: 'XXX商城API文档',
            version: '1.0.0',
            author: 'XXX',
            copyright: '2018 © 浙江XXX电子商务有限公司',
            host: this.ctx.request.headers.host,
        },
        markdowns: [
            { name: 'readme', path: '/public/README.md' },
            { name: '中文说明', path: '/public/中文示例.md' },
        ],
        configs
    });
 
    return data;
}

✪ 分布式缓存

import CacheService, { RedisProvider } from 'ppfly/service/cache';
 
export default (app: Application) => {
    app.beforeStart(async () => {
        ...
        await CacheService.init(new RedisProvider(app.config.redis.client));
    });
};

演示代码:

import CacheService, { IDataProvider } from 'ppfly/service/cache';
 
export default class ConfigService extends Service implements IDataProvider {
    private readonly cache_key = 'app_config';
 
    public async fetchData(): Promise<object> {
        this.app.logger.info('config 数据刷新。');
        return await this.ctx.model.Config.find({});
    }
 
    /**
     * 获得所有配置
     */
    public async getAllConfig() {
        return CacheService.getData(this.cache_key, this);
    }
 
    /**
     * 更新缓存
     */
    public async updateConfig() {
        CacheService.update(this.cache_key);
    }
}

Readme

Keywords

none

Package Sidebar

Install

npm i ppfly

Weekly Downloads

320

Version

2.7.7

License

ISC

Unpacked Size

138 kB

Total Files

122

Last publish

Collaborators

  • mypc59