跳到主要内容

result - 结果处理

控制器返回结果统一处理,默认返回格式为 JSON

依赖模块

  • @zenweb/messagecode

配置项

配置项类型默认值功能
failCodenumber默认失败代码
failStatusnumber422默认失败HTTP状态码
successWrap(ctx: Context, data?: unknown): unknownreturn { data }成功结果包装
failWrap(ctx: Context, err: ResultFail): unknownreturn { err.code, err.data, err.message }错误结果包装
exposeUnexpectedbooleanfalse暴露意外错误信息。可以设置环境变量 EXPOSE_UNEXPECTED==1 开启
unexpectedStatusnumber500意外错误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 类型的意外错误(如 TypeErrorReferenceError 等)的错误详情暴露到响应中。

  • 开发环境(默认行为):当 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 优先匹配

渲染链匹配逻辑

  1. renders 数组头部开始遍历(后添加的在前)
  2. 调用每个渲染器的 match(ctx, data) 方法
  3. 第一个返回 true 的渲染器被选中
  4. 如果没有匹配的渲染器,抛出异常 no match ResultRender
  5. 默认情况下 JSONRendermatch() 始终返回 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() 方法抛出,由框架中间件捕获并处理。

属性说明

属性类型默认值说明
messagestring'request fail'错误消息内容
codenumberfailCode 配置决定错误代码
dataunknownundefined附加数据,可传递任意额外信息
statusnumberfailStatus 配置决定,默认 422HTTP 响应状态码
extraRecord<string, unknown>undefined额外字段,会被展开到响应结果的顶层
exposebooleantrue标记此错误信息是否可暴露给客户端

构造方式

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 中的字段会被展开到响应的顶层,datamessagecode 为标准字段。