mcpField
S
serverpod-webserver

serverpod-webserver

github.com/serverpod开发3.3k44 次安装1 天前更新
代码仓库

关于 serverpod-webserver

用于在 Serverpod 可选的 Relic Web 服务器上构建路由的技能,面向 Serverpod 开发者。它介绍如何利用共享会话处理 HTTP 请求,并涵盖路由、中间件和 Web 内容。

主要功能

  1. 01定义带 HTTP 方法限制的路由
  2. 02读取类型化路径和查询参数
  3. 03从字符串或数据流构造响应
  4. 04配置备用路由和中间件
  5. 05提供静态文件和 Web 应用

适用场景

  1. 01在 Serverpod API 旁添加 HTTP 路由
  2. 02使用 API 密钥中间件验证请求
  3. 03按主机或路径路由请求
  4. 04提供静态文件或 Web 应用内容

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/serverpod/serverpod --skill serverpod-webserver

安装后在 Claude Code 中输入/serverpod-webserver确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
server-rendered-html.md
spa-and-flutter-web.md
static-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 应用

此技能目录中的参考文件介绍了这些内容。请阅读与任务相符的文件:

评论

支持 Markdown