ABP VNext + BFF(Backend for Frontend)模式:Angular/React 专用聚合层

发布于:2025-06-25 ⋅ 阅读:(14) ⋅ 点赞:(0)

ABP VNext + BFF(Backend for Frontend)模式:Angular/React 专用聚合层 🚀



1. 引言 ✨

TL;DR

  • 🚀 快速上手:基于 ABP VNext 搭建 BFF 模块
  • 🔗 接口聚合:Order/User/Product 三合一,减少前端请求次数
  • 🛠️ 生产级中间件:CORS、Compression、Response Caching、Swagger、Exception Handling
  • 🏭 企业级功能:Polly 重试+Jitter+Timeout、Redis Cache-Aside、JWT 转发、IP/用户级限流
  • 📈 可观测性:HealthChecks、Prometheus、OpenTelemetry、Serilog

📚 背景与动机
SPA(Angular/React)常需并发调用多个后端 API,易受网络抖动影响且前端逻辑复杂。BFF 模式在前后端解耦中扮演“聚合层”,既为前端提供定制化 API,又统一处理鉴权、缓存、限流、重试和监控等横切关注点,显著提升性能与可维护性。

💡Tips:保持 BFF 的职责单一——路由聚合与横切关注点,不下沉核心业务逻辑。


2. 环境与依赖 📦

  • .NET SDK:6.0 +
  • ABP VNext:6.x +

必装 NuGet 包

dotnet add package Volo.Abp.AspNetCore.Mvc.UI.Bff
dotnet add package Microsoft.Extensions.Caching.Memory
dotnet add package StackExchange.Redis
dotnet add package AspNetCoreRateLimit
dotnet add package prometheus-net.AspNetCore
dotnet add package Polly.Extensions.Http
dotnet add package Swashbuckle.AspNetCore
dotnet add package Serilog.AspNetCore
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore

appsettings.json 示例

{
  "Bff": {
    "Authority": "https://auth.example.com",
    "ApiScopes": [ "order_api", "user_api", "product_api" ]
  },
  "Services": {
    "Order":   { "BaseUrl": "https://order.example.com" },
    "User":    { "BaseUrl": "https://user.example.com" },
    "Product": { "BaseUrl": "https://product.example.com" }
  },
  "Cors": {
    "AllowedOrigins": [ "https://app.example.com" ]
  },
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "GeneralRules": [
      { "Endpoint": "*", "Period": "1m", "Limit": 60 },
      { "Endpoint": "get:/api/bff/dashboard", "Period": "1m", "Limit": 30 }
    ]
  },
  "ClientRateLimiting": {
    "EnableClientRateLimiting": true,
    "ClientIdHeader": "X-ClientId",
    "GeneralRules": [
      { "ClientId": "*", "Endpoint": "*", "Period": "1m", "Limit": 30 },
      { "ClientId": "*", "Endpoint": "get:/api/bff/dashboard", "Period": "1m", "Limit": 10 }
    ]
  },
  "Redis": {
    "Configuration": "localhost:6379"
  }
}

3. 完整管道配置(Program.cs)🛠️

using Microsoft.AspNetCore.Diagnostics;
using OpenTelemetry.Resources;
using Prometheus;
using Polly;
using Polly.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);
var config  = builder.Configuration;
var services = builder.Services;

// 1. Serilog 日志
builder.Host.UseSerilog((ctx, lc) => lc
    .ReadFrom.Configuration(ctx.Configuration)
    .WriteTo.Console());

// 2. ASP.NET Core 核心服务
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();

// 3. ABP BFF 模块
services.AddApplication<YourCompany.BffModule>();

// 4. CORS 🌐
services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
        policy.WithOrigins(config.GetSection("Cors:AllowedOrigins").Get<string[]>())
              .AllowAnyHeader()
              .AllowAnyMethod());
});

// 5. 响应压缩与缓存 🎁
services.AddResponseCompression();
services.AddResponseCaching();

// 6. 内存 & 分布式缓存 🗄️
services.AddMemoryCache();
services.AddStackExchangeRedisCache(opt =>
    opt.Configuration = config["Redis:Configuration"]);

// 7. HealthChecks ❤️‍🩹
services.AddHealthChecks()
    .AddUrlGroup($"{config["Services:Order:BaseUrl"]}/hc",   name: "Order")
    .AddUrlGroup($"{config["Services:User:BaseUrl"]}/hc",    name: "User")
    .AddUrlGroup($"{config["Services:Product:BaseUrl"]}/hc", name: "Product");

// 8. Prometheus 指标 📈
services.AddMetricServer();
services.AddHttpMetrics();

// 9. OpenTelemetry 分布式追踪 🌐
services.AddOpenTelemetryTracing(b =>
{
    b.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BffService"))
     .AddAspNetCoreInstrumentation()
     .AddHttpClientInstrumentation()
     .AddConsoleExporter();
});

// 10. 鉴权 & 授权 🔒
services.AddAbpAuthentication()
    .AddJwtBearer("Bff", options =>
    {
        options.Authority = config["Bff:Authority"];
        options.Audience  = "bff_api";
    })
    .AddAbpJwtBearer("OrderApi", options =>
    {
        options.Authority = config["Bff:Authority"];
        options.Audience  = "order_api";
    })
    .AddAbpJwtBearer("UserApi", options =>
    {
        options.Authority = config["Bff:Authority"];
        options.Audience  = "user_api";
    })
    .AddAbpJwtBearer("ProductApi", options =>
    {
        options.Authority = config["Bff:Authority"];
        options.Audience  = "product_api";
    });

// 11. 限流:IP + 用户 🛑
services.AddOptions();
services.Configure<IpRateLimitOptions>(config.GetSection("IpRateLimiting"));
services.Configure<ClientRateLimitOptions>(config.GetSection("ClientRateLimiting"));
services.AddInMemoryRateLimiting();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

// 12. HttpClientFactory + Polly + JWT 转发 🔄
static IAsyncPolicy<HttpResponseMessage> GetPolicy() =>
    HttpPolicyExtensions
        .HandleTransientHttpError()
        .WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry))
            + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100)))
        .WrapAsync(Policy.TimeoutAsync<HttpResponseMessage>(5));

services.AddUserAccessTokenHttpClient("OrderApi", client =>
    client.BaseAddress = new Uri(config["Services:Order:BaseUrl"]))
  .AddPolicyHandler(GetPolicy());
services.AddUserAccessTokenHttpClient("UserApi", client =>
    client.BaseAddress = new Uri(config["Services:User:BaseUrl"]))
  .AddPolicyHandler(GetPolicy());
services.AddUserAccessTokenHttpClient("ProductApi", client =>
    client.BaseAddress = new Uri(config["Services:Product:BaseUrl"]))
  .AddPolicyHandler(GetPolicy());

// 13. ABP BFF 服务配置 🎯
services.AddAbpBff(options =>
{
    options.Authority        = config["Bff:Authority"];
    options.DefaultApiScopes = config["Bff:ApiScopes"].Split(',');
});

var app = builder.Build();

// ABP 应用 初始化 & 关闭 🔄
await app.InitializeApplicationAsync();

// —— 中间件管道 —— 
app.UseSerilogRequestLogging();
app.UseCors("AllowFrontend");
app.UseResponseCompression();

app.UseRouting();

// 全局异常处理 🚨
app.UseExceptionHandler(a => a.Run(async context =>
{
    var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    context.Response.StatusCode = 500;
    await context.Response.WriteAsJsonAsync(new { error = ex?.Message });
}));

app.UseResponseCaching();

app.UseAuthentication();
app.UseAuthorization();

// 将用户 ID 注入限流中间件 🆔
app.Use(async (ctx, next) =>
{
    if (ctx.User?.Identity?.IsAuthenticated == true)
    {
        ctx.Request.Headers["X-ClientId"] =
          ctx.User.FindFirst("sub")?.Value;
    }
    await next();
});

// 限流中间件
app.UseIpRateLimiting();
app.UseClientRateLimiting();

// 本地化 & Serilog Enrichers 🌍
app.UseAbpRequestLocalization();
app.UseAbpSerilogEnrichers();

// Prometheus & OpenTelemetry
app.UseMetricServer();   // /metrics
app.UseHttpMetrics();

// Swagger(仅开发环境)📝
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// 映射 Controllers & HealthChecks
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();
await app.ShutdownApplicationAsync();

4. 系统架构概览 📊

微服务集群
HTTP
gRPC/HTTP
gRPC/HTTP
gRPC/HTTP
Redis
Prometheus
HealthChecks
Tracing
OrderService
UserService
ProductService
Angular/React SPA
BFF Service
Redis
Grafana
health
OpenTelemetry Collector

5. 接口聚合实现 🔗

5.1 聚合 DTO

public class DashboardDto
{
    public List<OrderDto>   RecentOrders { get; set; }
    public UserProfileDto   Profile      { get; set; }
    public List<ProductDto> TopProducts  { get; set; }
}

5.2 DashboardController

[ApiController]
[Route("api/bff/dashboard")]
[Authorize(AuthenticationSchemes = "Bff")]
public class DashboardController : AbpController
{
    private readonly IHttpClientFactory _factory;
    public DashboardController(IHttpClientFactory factory) 
        => _factory = factory;

    [HttpGet]
    public async Task<DashboardDto> GetAsync(CancellationToken ct)
    {
        var (orders, profile, products) = await Task.WhenAll(
            _factory.CreateClient("OrderApi")
                    .GetFromJsonAsync<List<OrderDto>>("orders/recent", ct),
            _factory.CreateClient("UserApi")
                    .GetFromJsonAsync<UserProfileDto>("users/me", ct),
            _factory.CreateClient("ProductApi")
                    .GetFromJsonAsync<List<ProductDto>>("products/top", ct)
        );

        return new DashboardDto
        {
            RecentOrders = orders,
            Profile      = profile,
            TopProducts  = products
        };
    }
}

💡Tips:自动携带用户 JWT,异常由全局中间件处理。


6. 缓存策略 🗄️

6.1 Response Caching

[ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any)]
[HttpGet("products/top")]
public async Task<List<ProductDto>> GetTopProductsAsync(CancellationToken ct)
{
    return await _factory.CreateClient("ProductApi")
           .GetFromJsonAsync<List<ProductDto>>("products/top", ct);
}

6.2 Redis Cache-Aside

private readonly IDistributedCache _cache;

public ProductsController(IDistributedCache cache, IHttpClientFactory factory)
{
    _cache   = cache;
    _factory = factory;
}

[HttpGet("products/top")]
public async Task<List<ProductDto>> GetTopProductsAsync(CancellationToken ct)
{
    const string key = "top_products";
    var bytes = await _cache.GetAsync(key, ct);
    if (bytes != null)
    {
        try { return JsonSerializer.Deserialize<List<ProductDto>>(bytes); }
        catch { await _cache.RemoveAsync(key, ct); }
    }

    var data = await _factory.CreateClient("ProductApi")
                  .GetFromJsonAsync<List<ProductDto>>("products/top", ct);

    var serialized = JsonSerializer.SerializeToUtf8Bytes(data);
    await _cache.SetAsync(key, serialized, new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
    }, ct);

    return data;
}

7. JWT 鉴权与转发 🔒

  • 使用 ABP 扩展 AddAbpAuthentication() + AddAbpJwtBearer()
  • AddUserAccessTokenHttpClient 自动将当前用户 JWT 转发到后端服务

8. 请求节流与防刷 🛑

  • IP 限流app.UseIpRateLimiting()
  • 用户级限流:通过中间件注入 sub Claim 至 X-ClientId,再 app.UseClientRateLimiting()

9. 可观测性与监控 📈

  • HealthChecks/health 展示下游服务状态
  • Prometheus/metrics 导出 HTTP、GC、CPU、内存等指标
  • OpenTelemetry:全链路分布式追踪,导出至 Collector/Zipkin
  • Serilog:控制台 + 文件/Elasticsearch/Seq

10. 全局异常处理 🚨

app.UseExceptionHandler(a => a.Run(async context =>
{
    var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    var problem = Results.Problem(detail: ex?.Message, statusCode: 500);
    await context.Response.WriteAsJsonAsync(problem);
}));

所有未捕获异常均返回标准 Problem JSON,前端可统一解析处理。


11. 端到端示例 🔍

Angular Service

@Injectable({ providedIn: 'root' })
export class DashboardService {
  constructor(private http: HttpClient) {}
  getDashboard(): Observable<DashboardDto> {
    return this.http.get<DashboardDto>('/api/bff/dashboard');
  }
}

React Hook

export function useDashboard() {
  const [data, setData] = useState<DashboardDto|null>(null);
  useEffect(() => {
    axios.get<DashboardDto>('/api/bff/dashboard')
         .then(res => setData(res.data));
  }, []);
  return data;
}

性能对比

模式 请求数 平均响应时延 数据流量
直连 3 后端 3 ~250ms 3×JSON
BFF 聚合一次调用 1 ~120ms 1×合并JSON

12. 附录 📂


网站公告

今日签到

点亮在社区的每一天
去签到