跳到主要内容

全局模式与 $ 快捷方式

从 ZenWeb v4.0 开始,框架引入了全局模式。全局模式基于 Node.js 的 asyncLocalStorage API,让你无需在函数之间层层传递 ctx 参数,就可以在任何位置直接访问当前请求上下文和相关服务。

传统方式下,你需要通过控制器方法注入 ctx,再传递给 Service 层。全局模式让你可以在任意层级的代码中直接使用 $ 前缀的全局函数获取所需对象。

启用全局模式

使用 $initCore() 代替传统的 new Core() 来创建应用实例,全局 Core 会自动启用 asyncLocalStorage

src/index.ts
import { $initCore } from 'zenweb';

const app = $initCore();
app.setup(/* ... */);
app.start();
警告

$initCore() 只能调用一次,重复调用会抛出异常。全局实例保存在 globalThis.__zenweb_core 中。

上下文与 Core

$ctx / $getContext()

获取当前请求的上下文对象(Context),等同于控制器方法中注入的 ctx 参数。

import { $ctx, $getContext } from 'zenweb';

// $ctx 是代理对象,直接使用即可
console.log($ctx.method); // 'GET'
console.log($ctx.path); // '/api/users'

// $getContext() 是函数调用形式
const ctx = $getContext();
ctx.throw(403, '无权访问');

$getContext() 接受一个可选的 force 参数(默认为 true):

// 在请求上下文中调用,返回 Context 对象
const ctx = $getContext(); // force = true,如果不在请求上下文中则抛出异常

// 在请求上下文外调用,返回 undefined 而不是抛出异常
const ctx = $getContext(false);
if (ctx) {
console.log('当前在请求上下文中');
}

$core / $getCore()

获取全局 Core 单例实例。

import { $core, $getCore } from 'zenweb';

// $core 是代理对象,直接访问属性
$core.start();
$core.setup();

// $getCore() 是函数调用形式
const core = $getCore();
console.log(core.env); // 当前环境

$debug

带有请求上下文信息和调用行号的调试输出。在请求上下文中使用时,输出会自动附带 METHOD /path 前缀。

import { $debug } from 'zenweb';

// 基本输出
$debug('用户注册流程开始');

// 输出对象
$debug({ userId: 123, action: 'login' });

// 创建命名空间
const serviceDebug = $debug.extend('service');
serviceDebug('服务层调试信息');

// 条件输出
if ($debug.enabled) {
serviceDebug('当前用户: %o', user);
}

在请求中执行时,输出格式类似:

app POST /api/auth/login +0ms
src/service/auth.ts:15:3
用户登录验证 +2ms

请求参数

$query

获取 URL 查询参数(QueryHelper 快捷方式),支持类型转换和数据校验。

src/controller/user.ts
import { Get, $query } from 'zenweb';

export class UserController {
@Get()
async list() {
// 获取并校验查询参数
const { page, size, keyword } = await $query.get({
page: 'int',
size: 'int',
keyword: '!trim',
});
// ...
}
}
// 获取原始 query 数据
const data = await $query.data();

// 分页参数快捷获取
const { offset, limit } = await $query.page();

$param

获取路径参数(ParamHelper 快捷方式),用于 RESTful 风格的路由参数提取。

src/controller/user.ts
import { Get, $param } from 'zenweb';

export class UserController {
@Get(':id')
async detail() {
const { id } = await $param.get({ id: '!int' });
// ...
}
}

请求体

$body / $getTextBody / $getRawBody / $getObjectBody

解析请求体的快捷方法集合。

src/controller/auth.ts
import { Post, $body, $getObjectBody } from 'zenweb';

export class AuthController {
@Post()
async register() {
// 使用 $body.get 获取并校验请求体数据
const { name, password } = await $body.get({
name: '!trim1',
password: '!trim1',
});
// ...
}

@Post()
async uploadAvatar() {
// 获取原始二进制数据
const raw = await $getRawBody();

// 获取文本内容(自动处理编码)
const text = await $getTextBody();

// 获取对象形式的请求体
const obj = await $getObjectBody();
}
}
// $body 同样支持 data() 和 get() 方法
const bodyData = await $body.data(); // 原始数据
const parsed = await $body.get({ // 校验后的数据
name: '!trim1',
email: 'email',
});

日志

$log

获取当前上下文关联的 Logger 对象。在请求上下文中使用时自动关联请求日志,在请求外使用时关联 Core 全局日志。

src/service/auth.ts
import { $log } from 'zenweb';

export class AuthService {
async register(name: string, password: string) {
$log.info('用户注册: %s', name);

// ... 注册逻辑 ...

$log.info('用户注册成功: userId=%d', user.id);
return { user, token };
}
}

数据库

$mysql

获取 MySQL 连接池的快捷方式。在请求上下文中使用时会自动使用请求级连接(支持事务),在请求外使用时使用全局连接池。

src/service/chat-room.ts
import { $mysql } from 'zenweb';

export class ChatRoomService {
async getMessages(roomId: number, limit: number = 50) {
const messages = await $mysql.sql`
SELECT m.*, u.name as user_name
FROM chat_room_message m
LEFT JOIN user u ON u.id = m.user_id
WHERE m.room_id = ${roomId}
ORDER BY m.created_at DESC
LIMIT ${limit}
`;
return messages;
}
}

缓存

$cache / $cacheHelper

缓存操作的快捷方式。

src/service/config.ts
import { $cache, $cacheHelper } from '@zenweb/cache';

export class ConfigService {
// 直接使用缓存实例
async getSystemConfig() {
let config = await $cache.get('system:config');
if (!config) {
config = await loadConfigFromDB();
await $cache.set('system:config', config, 3600);
}
return config;
}

// 使用缓存助手(自动获取并缓存)
async getUserInfo(userId: number) {
return $cacheHelper(
['user:info', userId],
async (uid) => {
return User.findByPk(uid);
},
{ ttl: 1800 }
);
}
}

文件上传

$getUpload / $getUploadHelper

文件上传相关操作的快捷方式。

src/controller/upload.ts
import { Post } from 'zenweb';
import { $getUpload, $getUploadHelper } from '@zenweb/upload';

export class UploadController {
@Post()
async upload() {
const upload = await $getUpload();
// 处理上传文件
// ...
}
}

依赖注入

$getInstance

在任意位置获取注入容器中的实例。如果在请求上下文中,从请求级容器获取;否则从全局容器获取。

src/service/chat-room.ts
import { $getInstance } from 'zenweb';
import { AuthService } from './auth';

export class ChatRoomService {
async createUserMessage(roomId: number, content: string) {
// 在 Service 中获取其他 Service 实例,无需注入
const authService = await $getInstance(AuthService);
const userId = authService.getCurrentUserId();

// ...
}
}

这个特性在 Service 层特别有用——你不再需要通过构造函数或 @Inject 注入依赖的其他 Service:

// 传统方式:需要在控制器中手动注入并传递
export class ChatRoomController {
@Post()
async create(ctx: Context, service: ChatRoomService, authService: AuthService) {
// ...
}
}

// 全局模式:Service 内部直接获取依赖
export class ChatRoomService {
async create(name: string) {
const authService = await $getInstance(AuthService);
const userId = authService.getCurrentUserId();
// ...
}
}

自定义 Helper

$helperBase()

$helperBase 是一个工厂函数,用于为任何继承自 HelperBase 的类创建全局快捷方式。框架内置的 $query$param 就是通过这个函数创建的。

你可以为自己定义的 Helper 类创建全局快捷方式:

src/helper/header.ts
import { HelperBase } from '@zenweb/helper';

export class HeaderHelper extends HelperBase {
// 自定义逻辑
}

// 在全局注册文件中
import { $helperBase } from '@zenweb/helper';
import { HeaderHelper } from './helper/header';

export const $header = $helperBase(HeaderHelper);

使用方式与内置 Helper 一致:

const { authorization } = await $header.get({
authorization: '!trim',
});

传统模式 vs 全局模式对比

以下对比展示了两种模式在控制器和 Service 层的写法差异:

控制器层

传统模式
import { Context, Get, Post, QueryHelper } from 'zenweb';
import { AuthService } from '../service/auth';

export class AuthController {
@Post()
async login(ctx: Context, query: QueryHelper, service: AuthService) {
// ctx、QueryHelper、service 都通过参数注入
const { name, password } = query.get({
name: '!trim1',
password: '!trim1',
});
try {
return await service.login(name, password);
} catch (e: any) {
ctx.throw(400, e.message);
}
}
}
全局模式
import { Post, $query, $ctx } from 'zenweb';
import { $getInstance } from 'zenweb';
import { AuthService } from '../service/auth';

export class AuthController {
@Post()
async login() {
// 无需注入 ctx 和 service,直接使用全局函数
const { name, password } = await $query.get({
name: '!trim1',
password: '!trim1',
});
const service = await $getInstance(AuthService);
try {
return await service.login(name, password);
} catch (e: any) {
$ctx.throw(400, e.message);
}
}
}

Service 层

传统模式
import { Context, Inject } from 'zenweb';

export class ChatRoomService {
// 需要通过 @Inject 注入 ctx
@Inject ctx!: Context;

async create(name: string) {
const userId = this.ctx.user!.id!;
// ...
}

async sendMessage(roomId: number, content: string) {
const userId = this.ctx.user!.id!;
// ...
}
}
全局模式
import { $ctx, $log, $mysql } from 'zenweb';

export class ChatRoomService {
// 无需注入 ctx,直接使用 $ctx
async create(name: string) {
const userId = $ctx.user!.id!;
$log.info('创建聊天室: %s, 操作者: %d', name, userId);
// ...
}

async sendMessage(roomId: number, content: string) {
const userId = $ctx.user!.id!;
$log.info('发送消息到房间 %d', roomId);
// ...
}
}

注意事项

asyncLocalStorage 依赖

全局模式依赖 Node.js 的 async_hooks 模块提供的 AsyncLocalStorage。确保你的运行环境满足以下要求:

  • Node.js >= 16.14
  • 使用 $initCore() 初始化应用(自动启用 asyncLocalStorage
// 正确:使用 $initCore
const app = $initCore();

// 错误:手动 new Core 且未启用 asyncLocalStorage
const app = new Core(); // 全局函数将无法获取请求上下文

请求上下文外使用

部分全局函数可以在请求上下文之外使用(比如启动脚本、定时任务等),它们会回退到全局 Core 实例:

全局函数请求上下文内请求上下文外
$getCore() / $core可用可用
$debug可用(附带请求信息)可用(无请求信息)
$log请求级日志全局日志
$mysql请求级连接全局连接池
$getInstance()请求级实例全局实例
$cache可用可用
$getContext() / $ctx可用抛出异常
$query / $param / $body可用抛出异常
危险

$ctx$query$param$body 等函数必须在请求上下文中使用。在定时任务、消息队列消费者等非 HTTP 请求场景中使用会抛出异常。如果你不确定是否在请求上下文中,可以使用 $getContext(false) 进行安全判断。