result - 结果处理
控制器返回结果统一处理,默认返回格式为 JSON
依赖模块
- @zenweb/messagecode
配置项
| 配置项 | 类型 | 默认值 | 功能 |
|---|---|---|---|
| failCode | number | 无 | 默认失败代码 |
| failStatus | number | 422 | 默认失败HTTP状态码 |
| successWrap | (ctx: Context, data?: unknown): unknown | return { data } | 成功结果包装 |
| failWrap | (ctx: Context, err: ResultFail): unknown | return { err.code, err.data, err.message } | 错误结果包装 |
| exposeUnexpected | boolean | false | 暴露意外错误信息。可以设置环境变量 EXPOSE_UNEXPECTED==1 开启 |
| unexpectedStatus | number | 500 | 意外错误HTTP状态码 |
Core 挂载项
无
Context 挂载项
| 挂载项 | 类型 | 功能 |
|---|---|---|
| success | (data?: unknown): Promise | 成功,输出结果。(注意:代码会继续执行),如果需要等待结果包装需要 await |
全局方法
| 方法 | 类型 | 功能 |
|---|---|---|
| fail | (message: string): never | 失败,输出错误信息并终止代码执行 |
| fail | (code: number, message?: string, data?: unknown): never | 失败,带错误代码输出 |
| fail | (detail: ResultFailDetail): never | 失败,输出错误信息并终止代码执行(更多选项) |
可注入对象
- singleton
- RenderManager
演示
import { Get, fail } from 'zenweb';
export class ResultController {
@Get()
hello() {
return 'Hello';
}
@Get()
error() {
fail('error info'); // 在调用 fail 方法后会直接跳出方法并输出
console.log('这行不会执行');
}
}
fail 配置
配置 message-codes.json
{
"400": "这是一个错误代码描述",
"user.username.short: "您的用户名太短:{username}"
}
失败输出
fail(400); // 使用错误代码数值,绝对匹配
fail('user.username.short', { username: 'AAA' }); // 使用错误代码字符串,递归匹配
// 完全自定义
fail({
code: 123,
message: "自定义",
status: 200,
});
failCodeHeader 配置说明
当请求结果为失败时,failCodeHeader 选项可以将错误代码写入 HTTP 响应头中,方便前端或网关层直接从头信息读取错误代码而不需要解析响应体。
| 值 | 行为 |
|---|---|
true(默认) | 启用,使用默认头字段名 X-Fail-Code |
false | 关闭,不输出错误代码到头信息 |
string | 启用,使用自定义头字段名,例如 'X-Error-Code' |
import { create } from 'zenweb';
create({
result: {
// 默认行为:错误代码输出到 X-Fail-Code 头字段
failCodeHeader: true,
// 自定义头字段名
failCodeHeader: 'X-Error-Code',
// 关闭头字段输出
failCodeHeader: false,
},
});
当发生失败时,响应头示例:
HTTP/1.1 422 Unprocessable Entity
X-Fail-Code: 400
Content-Type: application/json
exposeUnexpected 选项
exposeUnexpected 控制是否将非 ResultFail 类型的意外错误(如 TypeError、ReferenceError 等)的错误详情暴露到响应中。
- 开发环境(默认行为):当
NODE_ENV !== 'production'时默认为true,方便调试 - 生产环境:默认为
false,避免泄露内部错误信息 - 也可以通过环境变量
EXPOSE_UNEXPECTED=1强制开启
import { create } from 'zenweb';
create({
result: {
// 开发环境自动暴露意外错误
exposeUnexpected: true,
// 意外错误的 HTTP 状态码,默认 500
unexpectedStatus: 500,
// 意外错误的代码,默认 500
unexpectedCode: 500,
},
});
开启后意外错误的响应示例(包含错误栈信息):
{
"code": 500,
"message": "Cannot read properties of undefined (reading 'name')",
"data": {
"name": "TypeError",
"stack": ["at Object.<anonymous> (app.ts:10:15)", "..."]
}
}
自定义 ResultRender
通过实现 ResultRender 接口,可以自定义结果渲染器,例如输出 XML、CSV 等格式。
ResultRender 接口定义
interface ResultRender {
/** 结果是否需要包装(成功/失败包装) */
enwrap: boolean;
/** 输出类型,例如 'json'、'text/xml' */
type?: string;
/** 当前请求是否匹配此渲染器 */
match(ctx: Context, data?: unknown): boolean | Promise<boolean>;
/** 渲染输出,支持 async */
render(ctx: Context, data?: unknown): unknown;
}
完整示例:XML 渲染器
import { Context } from '@zenweb/core';
import { ResultRender } from '@zenweb/result';
export class XMLRender implements ResultRender {
enwrap = true;
type = 'text/xml';
match(ctx: Context): boolean {
// 当 Accept 头包含 text/xml 时使用此渲染器
return ctx.accepts('text/xml') === 'text/xml';
}
async render(ctx: Context, data?: unknown): Promise<string> {
// 将 JSON 对象转换为 XML 字符串
return objectToXml(data);
}
}
注册自定义渲染器
import { create } from 'zenweb';
import { XMLRender } from './xml-render';
create({
result: {
// 在 renders 数组中注册自定义渲染器
// 后注册的渲染器优先匹配
renders: [XMLRender],
},
});
提示
后添加的渲染器优先匹配。如果所有自定义渲染器都不匹配,最终会回退到 JSONRender。
RenderManager API
RenderManager 管理所有结果渲染器,负责匹配渲染器并输出结果。
add(render)
添加渲染器实例。添加是前置的,越后添加的渲染器越优先匹配。
const renderManager = await core.injector.getInstance(RenderManager);
// 添加自定义渲染器,后添加的优先匹配
renderManager.add(new XMLRender());
renderManager.add(new CSVRender()); // CSVRender 比 XMLRender 优先匹配
渲染链匹配逻辑
- 从
renders数组头部开始遍历(后添加的在前) - 调用每个渲染器的
match(ctx, data)方法 - 第一个返回
true的渲染器被选中 - 如果没有匹配的渲染器,抛出异常
no match ResultRender - 默认情况下
JSONRender的match()始终返回true,作为兜底渲染器
// 渲染器匹配流程
const render = await renderManager.match(ctx, data);
// 匹配后执行渲染
if (render.enwrap) {
data = await renderManager.dataWrap(ctx, data); // 成功/失败包装
}
ctx.body = await render.render(ctx, data);
ctx.isFail 使用说明
ctx.isFail 是一个布尔值标记,当请求处理过程中触发了 ResultFail 错误时会被设置为 true。可以在后续中间件中判断请求是否失败。
import { Middleware } from '@zenweb/core';
// 在记录日志的中间件中使用
const logMiddleware: Middleware = async (ctx, next) => {
await next();
if (ctx.isFail) {
// 请求结果为失败,记录错误日志
console.error(`请求失败: ${ctx.method} ${ctx.path} - status: ${ctx.status}`);
} else {
console.log(`请求成功: ${ctx.method} ${ctx.path}`);
}
};
注意
ctx.isFail 仅在 @zenweb/result 的错误捕获中间件内部设置为 true。如果错误没有被 ResultFail 捕获,则不会设置此标记。
ResultFail 对象结构
ResultFail 继承自 Error,是 @zenweb/result 中用于表示业务错误的对象。通过 fail() 方法抛出,由框架中间件捕获并处理。
属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
message | string | 'request fail' | 错误消息内容 |
code | number | 由 failCode 配置决定 | 错误代码 |
data | unknown | undefined | 附加数据,可传递任意额外信息 |
status | number | 由 failStatus 配置决定,默认 422 | HTTP 响应状态码 |
extra | Record<string, unknown> | undefined | 额外字段,会被展开到响应结果的顶层 |
expose | boolean | true | 标记此错误信息是否可暴露给客户端 |
构造方式
import { ResultFail } from '@zenweb/result';
// 字符串消息
const err1 = new ResultFail('操作失败');
// 详细对象
const err2 = new ResultFail({
code: 40001,
message: '余额不足',
data: { balance: 10, required: 100 },
status: 400,
extra: { currency: 'CNY' },
});
响应输出结构
当 ResultFail 被捕获并经过默认包装后,输出结构如下:
{
"code": 40001,
"data": { "balance": 10, "required": 100 },
"message": "余额不足",
"currency": "CNY"
}
其中 extra 中的字段会被展开到响应的顶层,data、message、code 为标准字段。