serverpod-webserver

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Serverpod Web Server (Relic)

Serverpod Web服务器(Relic)

Built on Relic, shares
Session
(DB, logging, auth) with the main server. This skill is for the optional
webServer
listener (default port 8082), not the main API server (default port 8080).
基于Relic构建,与主服务器共享
Session
(数据库、日志、认证)。该技能适用于可选的
webServer
监听器(默认端口8082),而非主API服务器(默认端口8080)。

Routes

路由

Extend
Route
, implement
handleCall(Session, Request)
. Register before
pod.start()
:
dart
class HelloRoute extends Route {
  
  Future<Result> handleCall(Session session, Request request) async {
    return Response.ok(
      body: Body.fromString(
        jsonEncode({'message': 'Hello'}),
        mimeType: MimeType.json,
      ),
    );
  }
}

pod.webServer.addRoute(HelloRoute(), '/api/hello');
Routes matched in registration order.
Session
provides DB, logging, and auth access just like in endpoints.
继承
Route
,实现
handleCall(Session, Request)
方法。在
pod.start()
之前注册:
dart
class HelloRoute extends Route {
  
  Future<Result> handleCall(Session session, Request request) async {
    return Response.ok(
      body: Body.fromString(
        jsonEncode({'message': 'Hello'}),
        mimeType: MimeType.json,
      ),
    );
  }
}

pod.webServer.addRoute(HelloRoute(), '/api/hello');
路由按注册顺序匹配。
Session
提供的数据库、日志和认证访问权限与端点中的一致。

HTTP methods

HTTP方法

Restrict which methods a route accepts (defaults to GET only) and branch on
request.method
:
dart
class UserRoute extends Route {
  UserRoute() : super(methods: {Method.get, Method.post, Method.delete});

  
  Future<Result> handleCall(Session session, Request request) async {
    if (request.method == Method.post) {
      final data = jsonDecode(await request.readAsString());
      // ...
      return Response(201);
    }
    // ...
  }
}
限制路由接受的方法(默认仅接受GET),并根据
request.method
分支处理:
dart
class UserRoute extends Route {
  UserRoute() : super(methods: {Method.get, Method.post, Method.delete});

  
  Future<Result> handleCall(Session session, Request request) async {
    if (request.method == Method.post) {
      final data = jsonDecode(await request.readAsString());
      // ...
      return Response(201);
    }
    // ...
  }
}

Path parameters

路径参数

dart
pod.webServer.addRoute(UserRoute(), '/api/users/:id');
pod.webServer.addRoute(route, '/:userId/posts/:postId');
Access typed params:
dart
class UserRoute extends Route {
  static const _idParam = IntPathParam(#id);

  
  Future<Result> handleCall(Session session, Request request) async {
    int userId = request.pathParameters.get(_idParam);
    // ...
  }
}
Raw access:
request.pathParameters.raw[#id]
. Typed query params work the same way with
IntQueryParam('page')
and
request.queryParameters.get(...)
, with raw access through
request.queryParameters.raw['query']
.
dart
pod.webServer.addRoute(UserRoute(), '/api/users/:id');
pod.webServer.addRoute(route, '/:userId/posts/:postId');
访问类型化参数:
dart
class UserRoute extends Route {
  static const _idParam = IntPathParam(#id);

  
  Future<Result> handleCall(Session session, Request request) async {
    int userId = request.pathParameters.get(_idParam);
    // ...
  }
}
原始访问方式:
request.pathParameters.raw[#id]
。类型化查询参数的使用方式类似,通过
IntQueryParam('page')
request.queryParameters.get(...)
访问,原始访问方式为
request.queryParameters.raw['query']

Wildcards

通配符

dart
pod.webServer.addRoute(route, '/item/*');   // One segment: /item/foo
pod.webServer.addRoute(route, '/item/**');  // Tail-match: /item/foo/bar/baz
**
only at end of path. Access matched path via
request.remainingPath
.
dart
pod.webServer.addRoute(route, '/item/*');   // 匹配单个分段:/item/foo
pod.webServer.addRoute(route, '/item/**');  // 匹配尾部路径:/item/foo/bar/baz
**
只能放在路径末尾。通过
request.remainingPath
访问匹配的路径。

Headers and body

请求头与请求体

dart
final userAgent = request.headers.userAgent;
final contentLength = request.headers.contentLength;
final auth = request.headers.authorization;
final apiKey = request.headers['X-API-Key']?.first;

final body = await request.readAsString();  // JSON, form data
final stream = request.read();              // Stream for large uploads
Body can only be read once.
dart
final userAgent = request.headers.userAgent;
final contentLength = request.headers.contentLength;
final auth = request.headers.authorization;
final apiKey = request.headers['X-API-Key']?.first;

final body = await request.readAsString();  // JSON、表单数据
final stream = request.read();              // 用于大文件上传的流
请求体只能读取一次。

Response types

响应类型

Response
has named constructors for the common statuses:
ok
,
noContent
,
notModified
,
movedPermanently
,
found
,
seeOther
,
badRequest
,
unauthorized
,
forbidden
,
notFound
,
notImplemented
,
internalServerError
. Any other status uses the unnamed constructor, e.g.
Response(201)
. Bodies are built with
Body.fromString(content, mimeType: MimeType.json)
,
Body.fromData(...)
or
Body.fromDataStream(...)
.
Response
为常见状态码提供了命名构造函数:
ok
noContent
notModified
movedPermanently
found
seeOther
badRequest
unauthorized
forbidden
notFound
notImplemented
internalServerError
。其他状态码使用未命名构造函数,例如
Response(201)
。响应体通过
Body.fromString(content, mimeType: MimeType.json)
Body.fromData(...)
Body.fromDataStream(...)
构建。

Fallback route

回退路由

dart
pod.webServer.fallbackRoute = NotFoundRoute();
Handles requests when no other route matches.
dart
pod.webServer.fallbackRoute = NotFoundRoute();
当没有其他路由匹配时处理请求。

Route modules (injectIn)

路由模块(injectIn)

Group related endpoints by overriding
injectIn()
:
dart
class UserCrudModule extends Route {
  
  void injectIn(RelicRouter router) {
    router
      ..get('/', _list)
      ..get('/:id', _get);
  }

  static const _idParam = IntPathParam(#id);

  Future<Result> _list(Request request) async {
    final session = await request.session;
    final users = await User.db.find(session);
    return Response.ok(
      body: Body.fromString(jsonEncode(users.map((u) => u.toJson()).toList()),
        mimeType: MimeType.json),
    );
  }

  Future<Result> _get(Request request) async {
    final session = await request.session;
    final user = await User.db.findById(session, request.pathParameters.get(_idParam));
    if (user == null) return Response.notFound();
    return Response.ok(
      body: Body.fromString(jsonEncode(user.toJson()), mimeType: MimeType.json),
    );
  }
}

pod.webServer.addRoute(UserCrudModule(), '/api/users');
// Creates GET /api/users and GET /api/users/:id
Note:
injectIn
handlers receive only
Request
; access
Session
with
await request.session
.
通过重写
injectIn()
将相关端点分组:
dart
class UserCrudModule extends Route {
  
  void injectIn(RelicRouter router) {
    router
      ..get('/', _list)
      ..get('/:id', _get);
  }

  static const _idParam = IntPathParam(#id);

  Future<Result> _list(Request request) async {
    final session = await request.session;
    final users = await User.db.find(session);
    return Response.ok(
      body: Body.fromString(jsonEncode(users.map((u) => u.toJson()).toList()),
        mimeType: MimeType.json),
    );
  }

  Future<Result> _get(Request request) async {
    final session = await request.session;
    final user = await User.db.findById(session, request.pathParameters.get(_idParam));
    if (user == null) return Response.notFound();
    return Response.ok(
      body: Body.fromString(jsonEncode(user.toJson()), mimeType: MimeType.json),
    );
  }
}

pod.webServer.addRoute(UserCrudModule(), '/api/users');
// 创建GET /api/users和GET /api/users/:id
注意:
injectIn
处理器仅接收
Request
;需通过
await request.session
访问
Session

Middleware

中间件

Middleware wraps handlers. Register with path prefix:
dart
Handler apiKeyMiddleware(Handler next) {
  return (Request request) async {
    final apiKey = request.headers['X-API-Key']?.firstOrNull;
    if (apiKey == null || !await isValidApiKey(apiKey)) {
      return Response.unauthorized(body: Body.fromString('API key required'));
    }
    return await next(request);
  };
}

pod.webServer.addMiddleware(apiKeyMiddleware, '/api');
中间件包装处理器。按路径前缀注册:
dart
Handler apiKeyMiddleware(Handler next) {
  return (Request request) async {
    final apiKey = request.headers['X-API-Key']?.firstOrNull;
    if (apiKey == null || !await isValidApiKey(apiKey)) {
      return Response.unauthorized(body: Body.fromString('API key required'));
    }
    return await next(request);
  };
}

pod.webServer.addMiddleware(apiKeyMiddleware, '/api');

Execution order

执行顺序

More specific paths run as inner middleware. Within the same path, order of registration:
dart
pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // Inner (last before handler)
pod.webServer.addMiddleware(apiKeyMiddleware, '/api');           // Outer (first)
For
/api/users/list
: apiKeyMiddleware → rateLimitMiddleware → handler → rateLimitMiddleware → apiKeyMiddleware.
更具体的路径对应的中间件作为内层中间件执行。同一路径下,按注册顺序执行:
dart
pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // 内层(最后执行,紧邻处理器)
pod.webServer.addMiddleware(apiKeyMiddleware, '/api');           // 外层(最先执行)
对于路径
/api/users/list
:执行顺序为apiKeyMiddleware → rateLimitMiddleware → 处理器 → rateLimitMiddleware → apiKeyMiddleware。

Request-scoped data (ContextProperty)

请求作用域数据(ContextProperty)

Pass data from middleware to routes without modifying the request:
dart
final _tenantProperty = ContextProperty<String>('tenant');

extension TenantEx on Request {
  String get tenant => _tenantProperty.get(this);
}

Handler tenantMiddleware(Handler next) {
  return (Request request) async {
    final session = await request.session;
    final tenant = await extractTenant(session, request.headers.host);
    if (tenant == null) return Response.notFound();
    _tenantProperty[request] = tenant;
    return await next(request);
  };
}

// In route:
final tenant = request.tenant;
Data cleaned up automatically when request completes. Host-specific middleware:
pod.webServer.addMiddleware(mw, '/api', host: 'api.example.com')
.
无需修改请求即可将数据从中间件传递到路由:
dart
final _tenantProperty = ContextProperty<String>('tenant');

extension TenantEx on Request {
  String get tenant => _tenantProperty.get(this);
}

Handler tenantMiddleware(Handler next) {
  return (Request request) async {
    final session = await request.session;
    final tenant = await extractTenant(session, request.headers.host);
    if (tenant == null) return Response.notFound();
    _tenantProperty[request] = tenant;
    return await next(request);
  };
}

// 在路由中:
final tenant = request.tenant;
请求完成后数据会自动清理。主机专属中间件:
pod.webServer.addMiddleware(mw, '/api', host: 'api.example.com')

Virtual host routing

虚拟主机路由

Restrict routes/middleware to a specific
Host
header:
dart
pod.webServer.addRoute(ApiRoute(), '/v1');  // ApiRoute has host: 'api.example.com'
pod.webServer.addRoute(SpaRoute(webDir, fallback: index, host: 'www.example.com'), '/');
pod.webServer.addRoute(HealthRoute(), '/health');  // All hosts (default)
All route types support
host
:
Route
,
StaticRoute
,
SpaRoute
,
FlutterRoute
.
将路由/中间件限制为特定的
Host
请求头:
dart
pod.webServer.addRoute(ApiRoute(), '/v1');  // ApiRoute指定了host: 'api.example.com'
pod.webServer.addRoute(SpaRoute(webDir, fallback: index, host: 'www.example.com'), '/');
pod.webServer.addRoute(HealthRoute(), '/health');  // 所有主机(默认)
所有路由类型均支持
host
参数:
Route
StaticRoute
SpaRoute
FlutterRoute

Serving files and web apps

提供文件与Web应用服务

These are covered in reference files in this skill directory. Read the one that matches the task:
  • references/static-files.md
    StaticRoute
    , cache control, cache-busted asset URLs.
  • references/spa-and-flutter-web.md
    SpaRoute
    ,
    FlutterRoute
    , SPA fallbacks, WASM headers.
  • references/server-rendered-html.md
    WidgetRoute
    , Mustache templates, the widget types.
相关内容请查看本技能目录下的参考文档,选择与任务匹配的文档阅读:
  • references/static-files.md
    StaticRoute
    、缓存控制、缓存失效的资源URL。
  • references/spa-and-flutter-web.md
    SpaRoute
    FlutterRoute
    、SPA回退、WASM请求头。
  • references/server-rendered-html.md
    WidgetRoute
    、Mustache模板、组件类型。