npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ppfly

v2.7.7

Published

Egg.js framework

Downloads

41

Readme

✪ 关于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)
     */
    code: number;

    /**
     * 操作是否成功
     */
    success: boolean;

    /**
     * 具体的消息描述
     */
    msg: string;

    /**
     * 返回数据
     */
    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
     */
    pageId: number;

    /**
     * 每页数据数量
     */
    pageSize: number;

    /**
     * 数据总数
     */
    dataCount: number;

    /**
     *  游标信息
     */
    cursor?: string | number;
}

/**
 * 分页数据约定
 */
export interface IPagingData<TData> {
    /**
     * 分页信息
     */
    pageInfo: IPageInfo;

    /**
     * 分页数据
     */
    data: Array<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);
    }
}