关于 serverpod-webserver
用于在 Serverpod 可选的 Relic Web 服务器上构建路由的技能,面向 Serverpod 开发者。它介绍如何利用共享会话处理 HTTP 请求,并涵盖路由、中间件和 Web 内容。
主要功能
- 01定义带 HTTP 方法限制的路由
- 02读取类型化路径和查询参数
- 03从字符串或数据流构造响应
- 04配置备用路由和中间件
- 05提供静态文件和 Web 应用
适用场景
- 01在 Serverpod API 旁添加 HTTP 路由
- 02使用 API 密钥中间件验证请求
- 03按主机或路径路由请求
- 04提供静态文件或 Web 应用内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ serverpod/ serverpod --skill serverpod-webserver安装后在 Claude Code 中输入/serverpod-webserver确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/server-rendered-html.mdspa-and-flutter-web.mdstatic-files.md详细介绍
Serverpod Web Server(Relic)
基于 Relic 构建,与主服务共享 Session(数据库、日志记录、身份验证)。此技能适用于可选的 webServer 监听器(默认端口为 8082),而非主 API 服务(默认端口为 8080)。
路由
扩展 Route,实现 handleCall(Session, Request)。在调用 pod.start() 前注册:
class HelloRoute extends Route {
@override
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 方法
限制路由接受的方法(默认仅接受 GET),并根据 request.method 分支处理:
class UserRoute extends Route {
UserRoute() : super(methods: {Method.get, Method.post, Method.delete});
@override
Future<Result> handleCall(Session session, Request request) async {
if (request.method == Method.post) {
final data = jsonDecode(await request.readAsString());
// ...
return Response(201);
}
// ...
}
}
路径参数
pod.webServer.addRoute(UserRoute(), '/api/users/:id');
pod.webServer.addRoute(route, '/:userId/posts/:postId');
访问类型化参数:
class UserRoute extends Route {
static const _idParam = IntPathParam(#id);
@override
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'] 进行原始访问。
通配符
pod.webServer.addRoute(route, '/item/*'); // One segment: /item/foo
pod.webServer.addRoute(route, '/item/**'); // Tail-match: /item/foo/bar/baz
** 只能出现在路径末尾。通过 request.remainingPath 访问匹配的路径。
标头和正文
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
正文只能读取一次。
响应类型
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(...) 构建。
后备路由
pod.webServer.fallbackRoute = NotFoundRoute();
当没有其他路由匹配时处理请求。
路由模块(injectIn)
通过重写 injectIn() 将相关端点分组:
class UserCrudModule extends Route {
@override
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
注意:injectIn 处理程序只接收 Request;使用 await request.session 访问 Session。
中间件
中间件会包装处理程序。注册时指定路径前缀:
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');
执行顺序
路径越具体,越作为内层中间件运行。同一路径内按注册顺序执行:
pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // Inner (last before handler)
pod.webServer.addMiddleware(apiKeyMiddleware, '/api'); // Outer (first)
对于 /api/users/list:apiKeyMiddleware → rateLimitMiddleware → handler → rateLimitMiddleware → apiKeyMiddleware。
请求范围数据(ContextProperty)
在不修改请求的情况下,将数据从中间件传递到路由:
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;
请求完成后会自动清理数据。主机专属中间件:pod.webServer.addMiddleware(mw, '/api', host: 'api.example.com')。
虚拟主机路由
将路由/中间件限制为特定的 Host 标头:
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)
所有路由类型都支持 host:Route、StaticRoute、SpaRoute、FlutterRoute。
提供文件和 Web 应用
此技能目录中的参考文件介绍了这些内容。请阅读与任务相符的文件:
references/static-files.md—StaticRoute、缓存控制、带缓存失效标记的资源 URL。references/spa-and-flutter-web.md—SpaRoute、FlutterRoute、SPA 后备路由、WASM 标头。references/server-rendered-html.md—WidgetRoute、Mustache 模板、各类 widget。