HTTP 接口
Cell 的 HTTP 由两个包组成:@celljs/web(服务器、中间件链、请求体解析、cookie、session、CORS、错误处理)和建在它之上的 @celljs/mvc(控制器)。两者都运行在 Node 的 node:http 上,不依赖第三方框架。装饰器从 @celljs/mvc/lib/node 导入,请求相关的类型从 @celljs/web/lib/node 导入。
控制器与路由
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@celljs/mvc/lib/node';
@Controller('users') // 前缀:/users
export class UserController {
@Get() // GET /users?role=admin
list(@Query('role') role?? string) { /* ... */ }
@Get(':id') // GET /users/42
get(@Param('id') id: number) { /* ... */ }
@Post()
create(@Body() user: CreateUser) { /* ... */ }
@Patch(':id')
rename(@Param('id') id: number, @Body('name') name: string) { /* ... */ }
@Delete(':id')
remove(@Param('id') id: number): void { /* ... */ }
}- 方法装饰器:
@Get、@Post、@Put、@Patch、@Delete、@Head、@Options、@All(所有方法)。不带路径时响应控制器路径本身。 - 路径:
:name匹配一段,*匹配任意内容,(...)表示可选。server.path(默认/)是所有路由的前缀。 - 没有路由匹配的请求得到
404。 - 控制器也是组件:用
@Autowired注入服务,用@Value读配置。记得在src/node/module.ts里导入控制器的文件。
参数
| 装饰器 | 取到的值 |
|---|---|
@Param('id') / @Param() | 一个路径参数 / 全部 |
@Query('q') / @Query() | 一个查询参数 / 解析后的整个查询(?a[b]=1 → { a: { b: '1' } }) |
@Body('name') / @Body() | 请求体的一个字段 / 整个请求体 |
@Header('x-api-key') / @Header() | 一个请求头 / 全部请求头 |
@Cookie('sid') | 一个请求 cookie |
@Session('cart') | session 里的一个值 |
@Req() / @Res() | Node 的请求和响应对象(IncomingMessage / ServerResponse,带扩展) |
类型为 string、number、boolean 或 Date 的参数会被转换('42' → 42)。请求体按 JSON、text/plain、application/octet-stream(Buffer)或 URL 编码表单解析,gzip、deflate、br 压缩的请求体会被解压。非法 JSON 返回 400,超过 web.body.limit(默认 100kb)返回 413。
校验请求体
加上 @celljs/validation 和 class-validator,再用类来声明参数类型:
import { IsString, Length } from 'class-validator';
export class CreateUser {
@IsString()
@Length(1, 20)
name: string;
}
@Post()
create(@Body() user: CreateUser) { /* user 是校验通过的 CreateUser 实例 */ }请求体不合法时返回 400 和 JSON 格式的错误,方法不会被调用。选项在 validation 下(@celljs/validation)。
响应
| 返回值 / 装饰器 | 响应 |
|---|---|
| 对象、数组、数字…… | JSON(application/json),默认视图 |
@Text() 并返回字符串 | text/plain |
@Html('page.mustache') 并返回对象 | 用它渲染 mvc.mustache.baseViewDir(默认 assets/views)里的 Mustache 模板 |
@File() 并返回 Buffer 或流 | 原样以流输出 |
undefined(void) | 200,空响应体 |
@Header('cache-control', 'no-store') | 添加响应头 |
@Cookie('flavor', 'oat') / @Session('step', 'two') | 设置 cookie / session 值 |
async 方法同样适用。要指定状态码,取得响应对象:@Res() res: Response(Response 是接口,用 import type { Response } from '@celljs/web/lib/node' 导入)或 Context.getResponse(),设置 res.statusCode = 201,再照常返回响应体。
错误
import { HttpError, NotFoundError } from '@celljs/web/lib/node';
throw new NotFoundError('No user 42'); // 404
throw new HttpError(409, 'Name already taken'); // 任意状态码
throw new Error('boom'); // 500状态码取自 HttpError.statusCode;其他错误一律返回 500。调用栈从不发给客户端:500 连同调用栈(以及 cause)记在 error 级别的日志里,4xx 的 HttpError(以及被处理器以重定向或 4xx 回答的错误,如需要登录、无权访问)只记在 debug 级别。客户端能看到 4xx HttpError 的信息;生产环境下 5xx 和其他错误只返回状态文本,除非 HttpError 创建时带了 { expose: true },或设置了 web.errors.exposeMessages。 单个控制器内,带 @Catch(ErrorType) 的方法处理本控制器抛出的该类型错误,它的返回值就是响应:
@Catch(QuotaError)
@Text()
onQuota(err: QuotaError) { return `Quota exceeded: ${err.message}`; }整个应用,添加一个 ErrorHandler。按 priority 从高到低询问各个处理器能否处理(canHandle),由第一个能处理的处理;它的 handle 抛错时再试下一个。内置处理器的优先级最高到 2000,你的要更高才会先执行:
import { Component } from '@celljs/core';
import { Context, ErrorHandler } from '@celljs/web/lib/node';
@Component(ErrorHandler)
export class QuotaErrorHandler implements ErrorHandler {
readonly priority = 2100;
async canHandle(ctx: Context, err: Error) { return err instanceof QuotaError; }
async handle(ctx: Context, err: Error) {
ctx.response.statusCode = 429;
ctx.response.setHeader('content-type', 'application/json');
ctx.response.end(JSON.stringify({ error: err.message }));
}
}中间件
中间件是绑定到 Middleware 的组件,在控制器之前看到每个请求;priority 越大越早执行。
import { Component } from '@celljs/core';
import { Context, HTTP_MIDDLEWARE_PRIORITY, HttpError, Middleware } from '@celljs/web/lib/node';
@Component(Middleware)
export class ApiKeyMiddleware implements Middleware {
readonly priority = HTTP_MIDDLEWARE_PRIORITY - 10; // 在 trace、CORS 和请求体解析之后
async handle(ctx: Context, next: () => Promise<void>): Promise<void> {
if (ctx.request.path.startsWith('/admin') && ctx.request.get('x-api-key') !== process.env.API_KEY) {
throw new HttpError(401, 'Missing or wrong API key');
}
await next();
}
}只需要 Node 的 req / res 的 express 中间件可以用 convertExpressMiddleware(fn) 包装后使用。
请求上下文
请求处理期间的任何位置(包括离控制器很远的服务),都能用 @celljs/web/lib/node 的 Context 拿到当前请求:Context.getRequest()、Context.getResponse()、Context.getSession()、Context.getCookies()、Context.getTraceId(),以及请求作用域的属性 Context.setAttr(key, value) / Context.getAttr(key)。它跨 await、定时器和 Promise 链保持有效。
内置功能
| 功能 | 配置 |
|---|---|
Trace ID:每个响应都带 X-Cell-Trace-ID 头(Context.getTraceId()) | web.trace |
健康检查:GET /health | web.health |
| CORS 响应头,默认允许所有来源 | web.cors(origin、methods、credentials……) |
| 签名 cookie,以及存放在 cookie 里的 session | web.cookies.keys、web.session。生产环境务必设置自己的 web.cookies.keys |
| 请求体限制 | web.body |
全部选项见 @celljs/web 文档。
更多
| 需求 | 包 |
|---|---|
从控制器生成 OpenAPI 文档(/openapi.json)和 Swagger UI(/api-docs) | @celljs/openapi |
登录、session、@Authenticated()、@PreAuthorize() | @celljs/security |
| JSON Web Token | @celljs/jwt |
| 返回前端构建产物 | @celljs/serve-static |
| 从前端或其他服务做类型安全的调用 | 前端与 RPC |
| gRPC 服务 | @celljs/grpc |