ppfly v2.7.7
✪ 关于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种情况:
- {code: 200, success: true, msg: ...}
- {code: -XXX, success: false, msg: ...}
- 客户端需要的数据内容
前端判断是否异常,可以借鉴以下代码:
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);
}
}
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago