从客户端到服务器:Alova 3 的全栈请求策略实践

发布: (2025年12月4日 GMT+8 22:49)
5 min read
原文: Dev.to

Source: Dev.to

在 BFF 层转发客户端请求

在 BFF 层,我们经常需要将客户端请求转发到后端微服务。可以使用 async_hooks 获取每个请求的上下文,并在 Alova 的 beforeRequest 中将其加入到请求中,从而实现用户相关数据的转发。

import { createAlova } from 'alova';
import adapterFetch from '@alova/fetch';
import express from 'express';
import { AsyncLocalStorage } from 'node:async_hooks';

// Create async local storage instance
const asyncLocalStorage = new AsyncLocalStorage();

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(),
  beforeRequest(method) {
    // Get request headers from async context and pass to downstream
    const context = asyncLocalStorage.getStore();
    if (context && context.headers) {
      method.config.headers = {
        ...method.config.headers,
        ...context.headers,
      };
    }
  },
  responded: {
    onSuccess(response) {
      // Data transformation processing
      return {
        data: response.data,
        timestamp: Date.now(),
        transformed: true,
      };
    },
    onError(error) {
      console.error('Request failed:', error);
      throw error;
    },
  },
});

const app = express();

// Set once in middleware, automatically passed throughout
app.use((req, res, next) => {
  const context = {
    userId: req.headers['x-user-id'],
    token: req.headers['authorization'],
  };
  asyncLocalStorage.run(context, next);
});

// Business code focuses on business logic
app.get('/api/user-profile', async (req, res) => {
  // No need to manually pass context anymore!
  const [userInfo, orders] = await Promise.all([
    alovaInstance.Get('http://gateway.com/user/profile'),
    alovaInstance.Get('http://gateway.com/order/recent'),
  ]);

  res.json({ user: userInfo.data, orders: orders.data });
});

API 网关中的使用场景

在网关中,通常需要实现身份验证、请求限流和请求分发等功能。Alova 3 的 Redis 存储适配器和 rateLimiter 能够有效实现分布式身份验证服务和请求限流。

认证可以这样实现

如果认证令牌有过期时间,可以在网关中配置 Redis 存储适配器来缓存令牌以供复用。对于单机集群服务,也可以使用 @alova/storage-file 文件存储适配器。

import { createAlova } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
import express from 'express';

const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: '6379',
  username: 'default',
  password: 'my-top-secret',
  db: 0,
});

const gatewayAlova = createAlova({
  requestAdapter: adapterFetch(),
  async beforeRequest(method) {
    const newToken = await authRequest(
      method.config.headers['Authorization'],
      method.config.headers['UserId']
    );
    method.config.headers['Authorization'] = `Bearer ${newToken}`;
  },
  // Set L2 storage adapter
  l2Cache: redisAdapter,
  // ...
});

const authRequest = (token, userId) =>
  gatewayAlova.Post('http://auth.com/auth/token', null, {
    // Cache for 3 hours; subsequent requests with the same parameters will hit Redis
    cacheFor: {
      mode: 'restore',
      expire: 3 * 3600 * 1000,
    },
    headers: {
      'x-user-id': userId,
      Authorization: `Bearer ${token}`,
    },
  });

const app = express;

// Forward all incoming requests to alova
const methods = [
  'get',
  'post',
  'put',
  'delete',
  'patch',
  'options',
  'head',
];
methods.forEach((m) => {
  app[m]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    const response = await gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers,
    });

    // Forward response headers
    for (const [key, value] of response.headers.entries()) {
      res.setHeader(key, value);
    }

    // Send response data
    res.status(response.status).send(await response.json());
  });
});

app.listen(3000, () => {
  console.log('Gateway server started on port 3000');
});

注意: 如果需要对每个请求都重新认证,只需在 authRequest 中移除 cacheFor 选项即可关闭缓存。

限流策略

Alova 的 rateLimiter 可以实现分布式限流策略,内部使用 node-rate-limiter-flexible。下面是一个重构后的实现示例,将限流器应用到所有网关路由。

import { createRateLimiter } from 'alova/server';

const rateLimit = createRateLimiter({
  /**
   * Time for points reset, in ms
   * @default 4000
   */
  duration: 60 * 1000,
  /**
   * Maximum consumable quantity within duration
   * @default 4
   */
  points: 4,
  /**
   * Namespace, prevents conflicts when multiple rateLimits use the same storage
   */
  keyPrefix: 'user-rate-limit',
  /**
   * Lock duration in ms; when the limit is reached, the block lasts this long.
   */
  blockDuration: 24 * 60 * 60 * 1000,
});

const methods = [
  'get',
  'post',
  'put',
  'delete',
  'patch',
  'options',
  'head',
];
methods.forEach((m) => {
  app[m]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    const alovaRequest = gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers,
    });

    const response = await rateLimit(alovaRequest, {
      key: req.ip, // Use IP as tracking key to prevent frequent requests from the same IP
    });

    // Forward response (similar to the previous example)
    for (const [key, value] of response.headers.entries()) {
      res.setHeader(key, value);
    }
    res.status(response.status).send(await response.json());
  });
});

第三方服务集成:自动令牌维护

与外部 API 集成时,通常需要管理 access_token 的生命周期。结合 Alova 3、Redis 存储适配器以及 atom 钩子,可以实现分布式的访问令牌自动维护。

import { createAlova, queryCache, atom } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';

// Configure Redis for token caching
const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: 6379,
});

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(),
  l2Cache: redisAdapter,
});

// Atomic operation to refresh token only once across distributed instances
const refreshTokenAtom = atom(async (currentToken) => {
  if (currentToken && Date.now()  {
  const res = await requestWithToken({
    method: 'GET',
    url: 'https://thirdparty.com/api/resource',
  });
  console.log(res.data);
})();
Back to Blog

相关文章

阅读更多 »