Egg.js framework
npm install ppfly js
app
├── config (可选)
├── controller (egg目录规范,保存控制器)
├── model (egg目录规范,保存数据库数据模型)
├── permission (可选,保存权限配置)
├── rule (必须,保存参数验证规则)
└── service (egg目录规范)
`
✪ 请求的处理流程
` bash
┌───────────┐ ┌──────────────┐ ┌────────┐ ┌────────┐
│ 客户端请求 │ =====> │ Interceptors │ =====> │ action │ =====> │ Result │
└───────────┘ └──────────────┘ └────────┘ └────────┘
`
- Interceptor(拦截器): 主要实现参数验证(Validator)、权限验证(RoleAuthorize)
- Result(结果渲染器): 输出结果,框架实现的主要是 :ActionResult、 ViewResult、 JsonResult、XmlResult、 FileResult、 RedirectResult、 StatusResult
- action 为真正的业务逻辑处理方法
✪ 返回消息格式
` js
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. 客户端需要的数据内容
#### 前端判断是否异常,可以借鉴以下代码:
`js
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'} |
✪ 分页数据格式
` js
/**
* 分页信息约定
*/
export interface IPageInfo {
/**
* 页面ID,第一页值为1
*/
pageId: number;
/**
* 每页数据数量
*/
pageSize: number;
/**
* 数据总数
*/
dataCount: number;
/**
* 游标信息
*/
cursor?: string | number;
}
/**
* 分页数据约定
*/
export interface IPagingData {
/**
* 分页信息
*/
pageInfo: IPageInfo;
/**
* 分页数据
*/
data: Array;
}
`
#### 只需要一句代码即可实现分页数据返回(目前只支持MongoDB)
`js
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修饰符可以实现路由注册、指定参数验证规则(自动注入)、指定使用的拦截器和渲染器。
` js
// @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 中注册
`js
export default (app: Application) => {
// 注册路由
Action.registerRouter(app);
};
`
#### @action 修饰符的参数 ActionConfig定义如下:
` js
/**
* 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 中注册
` js
export default (app: Application) => {
app.beforeStart(async () => {
....
});
// 配置异常处理
Action.handleError(app, new DefaultErrorHandler());
};
`
` js
// 代码: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;
}
};
`
✪ 参数验证
` js
export default (app: Application) => {
app.beforeStart(async () => {
...
});
// 配置参数验证器
Validator.service = new ParameterValidator();
};
`
- 在app.ts注册验证器后就可以在@action修饰器中指定验证规则
- 目前框架只有一个ValidatorService实现:ParameterValidator,部分规则基于validator.js
- 验证器支持数组的结构验证,也支持嵌套验证
- 验证有错误就会抛 ValidateError (ppfly/error/validate)
` js
@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支持的类型有:
` js
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'
}
`
✪ 角色验证
` js
export default (app: Application) => {
app.beforeStart(async () => {
...
});
// 配置全局角色验证
RoleAuthorize.service = new RoleValidator();
};
`
` js
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;
}
}
`
` js
@role(['XX权限'])
public async test(){
console.log(this.ctx.user);
}
`
✪ API文档生成
` js
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;
}
`
✪ 分布式缓存
`js
import CacheService, { RedisProvider } from 'ppfly/service/cache';
export default (app: Application) => {
app.beforeStart(async () => {
...
await CacheService.init(new RedisProvider(app.config.redis.client));
});
};
`
#### 演示代码:
`js
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``