serverpod-webserver
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServerpod Web Server (Relic)
Serverpod Web服务器(Relic)
Built on Relic, shares (DB, logging, auth) with the main server. This skill is for the optional listener (default port 8082), not the main API server (default port 8080).
SessionwebServer基于Relic构建,与主服务器共享(数据库、日志、认证)。该技能适用于可选的监听器(默认端口8082),而非主API服务器(默认端口8080)。
SessionwebServerRoutes
路由
Extend , implement . Register before :
RoutehandleCall(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');Routes matched in registration order. provides DB, logging, and auth access just like in endpoints.
Session继承,实现方法。在之前注册:
RoutehandleCall(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');路由按注册顺序匹配。提供的数据库、日志和认证访问权限与端点中的一致。
SessionHTTP methods
HTTP方法
Restrict which methods a route accepts (defaults to GET only) and branch on :
request.methoddart
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.methoddart
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: . Typed query params work the same way with and , with raw access through .
request.pathParameters.raw[#id]IntQueryParam('page')request.queryParameters.get(...)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**request.remainingPathdart
pod.webServer.addRoute(route, '/item/*'); // 匹配单个分段:/item/foo
pod.webServer.addRoute(route, '/item/**'); // 匹配尾部路径:/item/foo/bar/baz**request.remainingPathHeaders 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 uploadsBody 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
响应类型
ResponseoknoContentnotModifiedmovedPermanentlyfoundseeOtherbadRequestunauthorizedforbiddennotFoundnotImplementedinternalServerErrorResponse(201)Body.fromString(content, mimeType: MimeType.json)Body.fromData(...)Body.fromDataStream(...)ResponseoknoContentnotModifiedmovedPermanentlyfoundseeOtherbadRequestunauthorizedforbiddennotFoundnotImplementedinternalServerErrorResponse(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/:idNote: handlers receive only ; access with .
injectInRequestSessionawait 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注意:处理器仅接收;需通过访问。
injectInRequestawait request.sessionSessionMiddleware
中间件
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 : apiKeyMiddleware → rateLimitMiddleware → handler → rateLimitMiddleware → apiKeyMiddleware.
/api/users/list更具体的路径对应的中间件作为内层中间件执行。同一路径下,按注册顺序执行:
dart
pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // 内层(最后执行,紧邻处理器)
pod.webServer.addMiddleware(apiKeyMiddleware, '/api'); // 外层(最先执行)对于路径:执行顺序为apiKeyMiddleware → rateLimitMiddleware → 处理器 → rateLimitMiddleware → apiKeyMiddleware。
/api/users/listRequest-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 header:
Hostdart
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 : , , , .
hostRouteStaticRouteSpaRouteFlutterRoute将路由/中间件限制为特定的请求头:
Hostdart
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'); // 所有主机(默认)所有路由类型均支持参数:、、、。
hostRouteStaticRouteSpaRouteFlutterRouteServing 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, cache control, cache-busted asset URLs.StaticRoute - —
references/spa-and-flutter-web.md,SpaRoute, SPA fallbacks, WASM headers.FlutterRoute - —
references/server-rendered-html.md, Mustache templates, the widget types.WidgetRoute
相关内容请查看本技能目录下的参考文档,选择与任务匹配的文档阅读:
- —
references/static-files.md、缓存控制、缓存失效的资源URL。StaticRoute - —
references/spa-and-flutter-web.md、SpaRoute、SPA回退、WASM请求头。FlutterRoute - —
references/server-rendered-html.md、Mustache模板、组件类型。WidgetRoute