一个现实的技术挑战摆在面前:团队维护着一套混合架构系统,包含一个陈旧的 PHP Monolith API 和数个新兴的 Node.js 微服务。前端应用需要同时与这些异构的后端交互,导致认证逻辑分散、API 路径不统一、跨域问题频发。我们需要一个统一的入口,一个 API 网关,来解决这些问题。但预算和运维资源有限,我们不想引入 Kong 或 Traefik 这样重量级的解决方案。团队主力技术栈是 Vercel 和 Flutter,这意味着我们对 Serverless 和 Dart 有着深厚的积累。
一个大胆但合乎逻辑的构想应运而生:能否在 Vercel Functions 上,使用 Dart 构建一个轻量级、可插拔的 API 网关?这不仅能复用团队现有技术栈,还能将网关的运维成本降至最低。挑战在于,Vercel Functions 的官方运行时并不直接支持 Dart。这意味着我们必须将 Dart 编译为 JavaScript。
这正是我们要攻克的路径。我们将设计一个完全在 Serverless 环境中运行的代理网关,其核心逻辑由 Dart 编写,通过中间件管道实现功能插拔,并利用 Vitest 为其提供坚实的质量保障。
架构设计与技术选型决策
核心目标是创建一个代理,它能接收所有前端请求,然后根据预设规则,智能地将请求转发到不同的上游服务。同时,它必须能在请求转发前后执行一系列操作,例如身份验证、日志记录、请求头改写等。
运行时: Vercel Functions 的 Node.js 运行时是我们的目标平台。因此,Dart 代码必须通过
dart2js
工具链编译成高效的 JavaScript 代码。这要求我们使用 Dart 的 JS 互操作库 (package:js
) 来与 Vercel 的请求和响应对象进行交互。核心逻辑 (Dart): 网关的核心将是一个中间件管道(Middleware Pipeline)。每个进入的请求都会流经这个管道。管道中的每个中间件都是一个独立的 Dart 函数,负责单一职责。这种设计模式使得添加、删除或重排网关功能变得极其简单。
路由与配置: Vercel 的
vercel.json
文件天然提供了强大的路径重写和路由能力。我们将用它来捕获所有指向/api/gateway/*
的请求,并将它们导向我们用 Dart 编写的单一 Serverless Function。具体的上游服务映射关系,我们将硬编码在 Dart 代码中作为路由表,对于我们当前的规模,这比引入外部配置中心更简单直接。测试策略 (Vitest): 由于最终产物是 JavaScript,使用一个现代的 JS 测试框架是最佳选择。Vitest 速度快、API 与 Jest 兼容,且对 TypeScript/ESM 有良好支持,非常适合测试编译后的 Dart 代码。我们将编写两类测试:
- 单元测试: 针对单个中间件的逻辑进行独立测试。
- 集成测试: 模拟完整的 Vercel Function 调用,测试整个中间件管道和路由转发逻辑。
下面是整个请求流程的架构图:
sequenceDiagram participant Client as 客户端 participant VercelEdge as Vercel 边缘网络 participant GatewayFunc as 网关 Function (Dart -> JS) participant UpstreamA as 上游服务A (PHP Monolith) participant UpstreamB as 上游服务B (Node.js Microservice) Client->>VercelEdge: POST /api/gateway/users VercelEdge->>GatewayFunc: 触发函数,传递 Request 对象 GatewayFunc->>GatewayFunc: 1. 进入中间件管道 GatewayFunc->>GatewayFunc: 2. 执行 LoggingMiddleware GatewayFunc->>GatewayFunc: 3. 执行 AuthMiddleware (JWT 校验) GatewayFunc->>GatewayFunc: 4. 路由解析: '/users' -> UpstreamA GatewayFunc->>GatewayFunc: 5. 执行 HeaderRewriteMiddleware GatewayFunc->>UpstreamA: 转发修改后的请求 UpstreamA-->>GatewayFunc: 返回响应 GatewayFunc-->>VercelEdge: 返回最终响应 VercelEdge-->>Client: 返回响应
项目搭建与核心实现
项目结构如下:
.
├── api/
│ └── gateway.dart # Serverless Function 入口
├── lib/
│ ├── core/
│ │ ├── gateway_request.dart
│ │ ├── gateway_response.dart
│ │ └── pipeline.dart
│ ├── middlewares/
│ │ ├── auth_middleware.dart
│ │ ├── logging_middleware.dart
│ │ └── proxy_middleware.dart
│ └── services/
│ └── router.dart
├── test/
│ ├── middlewares/
│ │ └── auth_middleware.test.ts
│ └── gateway.integration.test.ts
├── package.json
├── pubspec.yaml
├── tsconfig.json
├── vercel.json
└── vitest.config.ts
1. 环境配置
package.json
定义了我们的构建和测试脚本。关键在于 build
命令,它调用 dart2js
。
{
"name": "dart-gateway-on-vercel",
"version": "1.0.0",
"scripts": {
"build": "dart compile js api/gateway.dart -o api/index.js -O4",
"test": "vitest",
"test:ci": "vitest run"
},
"devDependencies": {
"@types/node": "^20.8.9",
"@vercel/node": "^3.0.7",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
}
}
-
-O4
: 开启最高级别的优化,这对于 Serverless 函数的性能至关重要。 -
api/index.js
: 这是 Vercel 约定的 Node.js 函数入口文件名。
vercel.json
负责将所有流量导向我们的网关。
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": { "buildCommand": "npm run build" }
}
],
"routes": [
{
"src": "/api/gateway/(.*)",
"dest": "/api/index.js"
}
]
}
2. Dart 与 JS 的桥梁
我们需要定义 Dart 类来与 Vercel 的 VercelRequest
和 VercelResponse
对象交互。这里使用 package:js
。
lib/core/gateway_request.dart
:
()
library gateway_request;
import 'package:js/js.dart';
// 使用 @JS() 注解来映射 Vercel/Node.js 的 Request 对象
// 这里的结构需要与实际的 JS 对象匹配
()
class GatewayRequest {
external String get method;
external String get url;
external dynamic get body;
external Map<String, dynamic> get headers;
external Map<String, dynamic> get query;
}
// 为 Response 对象也创建类似的定义
()
class GatewayResponse {
external void status(int code);
external void send(dynamic body);
external void json(dynamic jsonBody);
external void setHeader(String name, String value);
}
这是一个关键步骤,它允许我们的强类型 Dart 代码安全地操作动态的 JavaScript 对象。
3. 中间件管道
这是网关架构的核心。管道负责按顺序执行一系列异步的中间件函数。
lib/core/pipeline.dart
:
import 'dart:async';
import 'gateway_request.dart';
import 'gateway_response.dart';
// 定义上下文,在中间件之间传递数据
class GatewayContext {
final GatewayRequest request;
final GatewayResponse response;
final Map<String, dynamic> _data = {};
// 用于存储上游服务的 URL
Uri? targetUri;
GatewayContext(this.request, this.response);
void set(String key, dynamic value) {
_data[key] = value;
}
dynamic get(String key) {
return _data[key];
}
}
// 定义中间件函数的类型签名
typedef Middleware = Future<void> Function(GatewayContext context, Future<void> Function() next);
class Pipeline {
final List<Middleware> _middlewares = [];
// 添加中间件到管道
Pipeline add(Middleware middleware) {
_middlewares.add(middleware);
return this;
}
// 执行管道
Future<void> run(GatewayContext context) {
// 从最后一个中间件开始,递归地构建调用链
Future<void> dispatch(int index) {
if (index >= _middlewares.length) {
// 如果所有中间件都执行完毕,返回一个已完成的 Future
return Future.value();
}
final middleware = _middlewares[index];
// 调用当前中间件,并将下一个中间件的调用作为 next 函数传入
return middleware(context, () => dispatch(index + 1));
}
return dispatch(0);
}
}
Pipeline.run
的实现是一个经典的洋葱模型。每个中间件都可以决定是在调用 next()
之前还是之后执行操作,这为实现请求/响应日志、错误处理等功能提供了极大的灵活性。
4. 编写具体中间件
让我们实现几个有代表性的中间件。
lib/middlewares/auth_middleware.dart
:
import 'dart:async';
import '../core/pipeline.dart';
// 伪代码,实际项目中会使用一个真正的 JWT 库
import '../services/jwt_validator.dart';
Future<void> authMiddleware(GatewayContext context, Future<void> Function() next) async {
final authHeader = context.request.headers['authorization'] as String?;
if (authHeader == null || !authHeader.startsWith('Bearer ')) {
context.response.status(401);
context.response.json({'error': 'Unauthorized', 'reason': 'Missing Bearer token'});
return; // 中断管道执行
}
final token = authHeader.substring(7);
try {
final claims = await JwtValidator.validate(token);
// 可以在上下文中存储解析后的用户信息,供后续中间件使用
context.set('user_claims', claims);
// 验证通过,继续执行下一个中间件
await next();
} catch (e) {
context.response.status(403);
context.response.json({'error': 'Forbidden', 'reason': 'Invalid token'});
// 同样中断管道
}
}
这个中间件演示了如何终止请求链并立即返回响应。
lib/middlewares/proxy_middleware.dart
:
这个中间件是管道的终点,它负责将请求真正地发送到上游服务。
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../core/pipeline.dart';
import '../services/router.dart';
Future<void> proxyMiddleware(GatewayContext context, Future<void> Function() next) async {
final originalPath = Uri.parse(context.request.url!).path;
// 从路径中剥离网关前缀 /api/gateway/
final servicePath = originalPath.replaceFirst('/api/gateway/', '/');
// 使用路由服务查找上游目标
final targetBaseUrl = routeToUpstream(servicePath);
if (targetBaseUrl == null) {
context.response.status(404);
context.response.json({'error': 'Service not found for path: $servicePath'});
return;
}
final targetUri = Uri.parse('$targetBaseUrl$servicePath');
context.targetUri = targetUri; // 存入上下文,方便日志记录
final client = http.Client();
try {
final request = http.Request(context.request.method, targetUri);
// 复制请求头,并可以添加/修改头
// 生产环境中需要过滤掉 host, connection 等头
final headers = Map<String, String>.from(
(context.request.headers as Map).cast<String, String>());
headers.remove('host');
headers['X-Forwarded-For'] = context.request.headers['x-forwarded-for'] as String? ?? 'unknown';
request.headers.addAll(headers);
if (context.request.body != null) {
request.body = jsonEncode(context.request.body);
}
// 发送请求到上游
final response = await client.send(request);
// 将上游响应写回客户端
context.response.status(response.statusCode);
response.headers.forEach((key, value) {
// 过滤一些不应透传的头
if (key.toLowerCase() != 'transfer-encoding' && key.toLowerCase() != 'content-encoding') {
context.response.setHeader(key, value);
}
});
final responseBody = await response.stream.bytesToString();
context.response.send(responseBody);
} catch (e) {
// 真实的错误处理需要更完善
context.response.status(502);
context.response.json({'error': 'Bad Gateway', 'details': e.toString()});
} finally {
client.close();
}
// 代理中间件通常是管道的最后一个实际操作者,
// 但我们仍然调用 next() 以保持模式一致性,尽管它后面可能没有其他中间件了。
await next();
}
5. 组装入口函数
最后,在 api/gateway.dart
中,我们将所有部分组装起来。
()
library gateway;
import 'package:js/js.dart';
import '../lib/core/gateway_request.dart';
import '../lib/core/gateway_response.dart';
import '../lib/core/pipeline.dart';
import '../lib/middlewares/auth_middleware.dart';
import '../lib/middlewares/logging_middleware.dart';
import '../lib/middlewares/proxy_middleware.dart';
// Vercel 要求导出一个默认函数
('default')
external set _default(void Function(GatewayRequest, GatewayResponse) f);
void main() {
_default = allowInterop((req, res) async {
final context = GatewayContext(req, res);
// 构建我们的中间件管道
final pipeline = Pipeline()
.add(loggingMiddleware) // 第一个执行
.add(authMiddleware)
.add(proxyMiddleware); // 最后一个执行
try {
await pipeline.run(context);
} catch (e) {
// 兜底的全局错误处理
if (!context.response.headersSent) { // 避免重复发送响应头
context.response.status(500);
context.response.json({'error': 'Internal Server Error'});
}
}
});
}
allowInterop
是一个关键函数,它将一个 Dart 函数包装成一个 JavaScript 可以调用的函数。
使用 Vitest 进行测试
测试是保证网关稳定性的基石。
1. 单元测试中间件
我们可以独立测试 authMiddleware
。
test/middlewares/auth_middleware.test.ts
:
import { describe, it, expect, vi } from 'vitest';
// 我们需要模拟 Dart 编译后的 JS 模块
// 实际操作中,你需要先运行 build,然后从 api/index.js 导入
// 这里为了演示,我们假设有模拟的 authMiddleware 函数
import { authMiddleware } from '../../api/__mocks__/auth_middleware';
// 模拟 GatewayContext 和 next 函数
const createMockContext = (headers: Record<string, string>) => {
const mockResponse = {
status: vi.fn(),
json: vi.fn(),
};
const mockRequest = { headers };
return {
// @ts-ignore
request: mockRequest,
// @ts-ignore
response: mockResponse,
set: vi.fn(),
get: vi.fn(),
};
};
describe('authMiddleware', () => {
it('should call next() if a valid token is provided', async () => {
// 模拟 JWT 验证器
vi.doMock('../../api/services/jwt_validator', () => ({
JwtValidator: {
validate: vi.fn().mockResolvedValue({ userId: '123' }),
},
}));
const context = createMockContext({ authorization: 'Bearer valid.token' });
const next = vi.fn();
await authMiddleware(context, next);
expect(next).toHaveBeenCalledOnce();
expect(context.response.status).not.toHaveBeenCalled();
expect(context.set).toHaveBeenCalledWith('user_claims', { userId: '123' });
});
it('should return 401 if no authorization header is present', async () => {
const context = createMockContext({});
const next = vi.fn();
await authMiddleware(context, next);
expect(next).not.toHaveBeenCalled();
expect(context.response.status).toHaveBeenCalledWith(401);
expect(context.response.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Unauthorized' }));
});
it('should return 403 if token is invalid', async () => {
vi.doMock('../../api/services/jwt_validator', () => ({
JwtValidator: {
validate: vi.fn().mockRejectedValue(new Error('Invalid signature')),
},
}));
const context = createMockContext({ authorization: 'Bearer invalid.token' });
const next = vi.fn();
await authMiddleware(context, next);
expect(next).not.toHaveBeenCalled();
expect(context.response.status).toHaveBeenCalledWith(403);
expect(context.response.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Forbidden' }));
});
});
2. 集成测试整个网关
集成测试会调用编译后的 index.js
文件,模拟一个完整的 Vercel Function 调用。
test/gateway.integration.test.ts
:
import { describe, it, expect, vi, beforeAll } from 'vitest';
import handler from '../../api/index.js'; // 直接导入编译后的产物
import http from 'http';
// 我们需要模拟 'http' 模块来拦截出站请求
vi.mock('http', async (importOriginal) => {
const actualHttp = await importOriginal<typeof http>();
const mockRequest = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
return {
...actualHttp,
request: vi.fn(() => mockRequest),
};
});
// 模拟 Vercel 的请求和响应对象
const createVercelMocks = (method: string, url: string, headers: Record<string, string>, body?: any) => {
const req = { method, url, headers, body, query: {} };
let statusCode: number;
let responseBody: any;
const res = {
status: vi.fn((code) => { statusCode = code; }),
setHeader: vi.fn(),
send: vi.fn((b) => { responseBody = b; }),
json: vi.fn((b) => { responseBody = JSON.stringify(b); }),
};
return { req, res, getResult: () => ({ statusCode, body: responseBody }) };
};
describe('Gateway Integration Test', () => {
// 在所有测试前,确保模拟是干净的
beforeAll(() => {
vi.mock('../../api/services/jwt_validator', () => ({
JwtValidator: {
validate: vi.fn().mockResolvedValue({ userId: 'test-user' }),
},
}));
});
it('should proxy a valid request to the correct upstream', async () => {
const { req, res, getResult } = createVercelMocks(
'GET',
'/api/gateway/legacy/posts/1',
{ authorization: 'Bearer valid-token' }
);
await handler(req, res);
const mockedHttpRequest = vi.mocked(http.request);
// 验证请求是否被正确转发
expect(mockedHttpRequest).toHaveBeenCalledOnce();
const requestOptions = mockedHttpRequest.mock.calls[0][0];
// 假设路由规则将 /legacy/* 路由到 'http://php-monolith.internal'
expect(requestOptions.hostname).toBe('php-monolith.internal');
expect(requestOptions.path).toBe('/legacy/posts/1');
expect(requestOptions.method).toBe('GET');
expect(requestOptions.headers).toHaveProperty('x-forwarded-for');
});
it('should block a request without a token', async () => {
const { req, res, getResult } = createVercelMocks('GET', '/api/gateway/some/path', {});
await handler(req, res);
const { statusCode, body } = getResult();
expect(statusCode).toBe(401);
expect(JSON.parse(body)).toEqual({ error: 'Unauthorized', reason: 'Missing Bearer token' });
// 确认没有向上游发送请求
expect(vi.mocked(http.request)).not.toHaveBeenCalled();
});
});
这种测试方法让我们对整个系统的行为充满信心,从请求入口到中间件逻辑,再到最终的代理转发。
局限性与未来展望
这个基于 Dart 和 Vercel Functions 的轻量级网关方案在我们的场景中表现出色,但它并非银弹。
首先,性能是一个考量点。dart2js
编译出的 JavaScript 代码性能优异,但与手写的、高度优化的原生 Node.js 代码或 Go/Rust 编写的网关相比,在极端高并发下可能存在差距。此外,Vercel Functions 的冷启动延迟是所有 Serverless 应用都需要面对的问题,对于延迟敏感的网关场景,需要开启 Pro/Enterprise 计划的预热功能。
其次,当前的路由和中间件配置是硬编码在 Dart 代码中的。随着业务复杂度的增加,我们可能需要一个更动态的配置系统,例如从 Vercel Edge Config 或外部数据库读取路由规则和中间件链。这将增加系统的复杂性。
最后,这个方案高度依赖 Vercel 平台。虽然核心的 Dart 逻辑是可移植的,但与 Vercel 请求/响应对象的交互部分是平台相关的。迁移到其他 Serverless 平台(如 AWS Lambda)需要进行适配工作。
未来的迭代方向可能包括:实现一个更复杂的中间件用于请求限流(需要借助 Upstash Redis 等外部服务),引入基于配置的动态路由,以及对编译产物进行更精细的性能剖析与优化。