Skip to content

HTTP 接口 ​

Cell 的 HTTP 由两个包组成:@celljs/web(服务器、中间件链、请求体解析、cookie、session、CORS、错误处理)和建在它之上的 @celljs/mvc(控制器)。两者都运行在 Node 的 node:http 上,不依赖第三方框架。装饰器从 @celljs/mvc/lib/node 导入,请求相关的类型从 @celljs/web/lib/node 导入。

控制器与路由 ​

ts
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,再用类来声明参数类型:

ts
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,再照常返回响应体。

错误 ​

ts
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) 的方法处理本控制器抛出的该类型错误,它的返回值就是响应:

ts
@Catch(QuotaError)
@Text()
onQuota(err: QuotaError) { return `Quota exceeded: ${err.message}`; }

整个应用,添加一个 ErrorHandler。按 priority 从高到低询问各个处理器能否处理(canHandle),由第一个能处理的处理;它的 handle 抛错时再试下一个。内置处理器的优先级最高到 2000,你的要更高才会先执行:

ts
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 越大越早执行。

ts
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 /healthweb.health
CORS 响应头,默认允许所有来源web.cors(origin、methods、credentials……)
签名 cookie,以及存放在 cookie 里的 sessionweb.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