深入alova3服务端能力:分布式BFF层到API网关的最佳实践

可能大家对alova还停留在轻量化的请求策略库的层面,这当然是alova2的核心特点,比如以下这段

js 复制代码
const { loading, data, error } = useRequest(() => alovaInstance.Get('/xxx'))

这是一段alova在客户端使用的典型代码,不过现在alova已经更新到3了,当然这些client strategies依然是原汁原味的,不过它不仅局限于客户端,而是在服务端也可以游刃有余了。

在alova3中提供了服务端请求策略(server hooks)和redis、file等服务端的存储适配器,可以让我们很方便地在服务端实现全链路的请求和转发。

我们先来看一个请求的全流程:

plaintext 复制代码
客户端(浏览器/App)
    → Node.js BFF 层(转换数据等)
    → API 网关(鉴权、速率限制、路由分发等)
    → 后端微服务

alova提供的server hook和分布式的多级缓存,可以让我们很方便地实现以上的全部层级的请求处理。

在BFF层转发客户端请求

在BFF层中经常需要转发客户端请求到后端微服务,你可以使用配合async_hooks访问每个请求的上下文,并在alova的beforeRequest中添加到请求中,实现用户相关数据的转发。

js 复制代码
import { createAlova } from 'alova';
import adapterFetch from '@alova/fetch';
import express from 'express';
import { AsyncLocalStorage } from 'node:async_hooks';

// 创建异步本地存储实例
const asyncLocalStorage = new AsyncLocalStorage();

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(),
  beforeRequest(method) {
    // 从异步上下文中获取请求头并传递到下游
    const context = asyncLocalStorage.getStore();
    if (context && context.headers) {
      method.config.headers = {
        ...method.config.headers,
        ...context.headers
      };
    }
  },
  responded: {
    onSuccess(response) {
      // 数据转换处理
      return {
        data: response.data,
        timestamp: Date.now(),
        transformed: true
      };
    },
    onError(error) {
      console.error('Request failed:', error);
      throw error;
    }
  }
});

const app = express();

// 中间件里设置一次,全程自动传递
app.use((req, res, next) => {
  const context = {
    userId: req.headers['x-user-id'],
    token: req.headers['authorization']
  };
  asyncLocalStorage.run(context, next);
});

// 业务代码专注业务逻辑
app.get('/api/user-profile', async (req, res) => {
  // 不用手动传递上下文了!
  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网关中的使用场景

在网关中经常需要进行鉴权、请求速率限制以及请求分发等,alova3的redis存储适配器和rateLimiter可以很好地实现分布式的鉴权服务和请求速率限制。

鉴权可以这么搞

如果鉴权token有一定的过期时间,可在网关中配置redis存储适配器,将token存储在redis中便于重复使用,对于单机的集群服务也可以使用@alova/storage-file文件存储适配器。

js 复制代码
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}`;
  }
  // 设置2级存储适配器
  l2Cache: redisAdapter,
  // ...
});

const authRequest = (token, userId) => gatewayAlova.Post('http://auth.com/auth/token', null, {
  // 设置3个小时的缓存,将保存在redis中,再次以相同参数请求会命中缓存
  cacheFor: {
    mode: 'restore',
    expire: 3 * 3600 * 1000
  },
  headers: {
    'x-user-id': userId,
    'Authorization': `Bearer ${token}`
  }
});

const app = express();

// 实现app接收所有请求,并转发到alova
// 注册所有 HTTP 方法的路由
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
methods.forEach(method => {
  app[method]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    // 使用 alova 发送请求
    const response = await gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers
    });
    
    // 转发响应头部
    for (const [key, value] of response.headers.entries()) {
      res.setHeader(key, value);
    }
    
    // 发送响应数据
    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实现,我们改造一下实现。

js 复制代码
import { createRateLimiter } from 'alova/server';

const rateLimit = createRateLimiter({
  /**
   * 点数重置的时间,单位ms
   * @default 4000
   */
  duration: 60 * 1000,
  /**
   * duration内可消耗的最大数量
   * @default 4
   */
  points: 4,
  /**
   * 命名空间,多个rateLimit使用相同存储器时可防止冲突
   */
  keyPrefix: 'user-rate-limit',
  /**
   * 锁定时长,单位ms,表示当到达速率限制后,将延长[blockDuration]ms,例如1小时内密码错误5次,则锁定24小时,这个24小时就是此参数
   */
  blockDuration: 24 * 60 * 60 * 1000
});

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

    // 在此使用rateLimit包裹调用即可,它将默认使用l2Cache存储适配器作为控制参数的存储,这边的例子会用redis存储适配器。
    const method = gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers
    });
    const response = await rateLimit(method, {
      key: req.ip // 使用ip作为追踪key,防止同一ip频繁请求
    });
    
    // ...
  });
});

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

和外部API打交道需要access_token管理,并且很多第三方access_token具有调用限制,在这里我们可以使用alova3+redis存储适配器来实现分布式的access_token生命周期自动维护,其中redis用于access_token缓存,atom hook用于分布式更新token的原子性操作。

js 复制代码
import { createAlova, queryCache } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
import { atomize } from 'alova/server';

const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: '6379',
  username: 'default',
  password: 'my-top-secret',
  db: 0
});
const thirdPartyAlova = createAlova({
  requestAdapter: adapterFetch(),
  async beforeRequest(method) {
    // 判断是否为第三方API,如果是的话则获取令牌
    if (method.meta?.isThirdPartyApi) {
      // 以原子性的方式获取令牌,防止多进程同时获取token
      const accessTokenGetMethod = getAccessToken();
      let accessToken = await queryCache(accessTokenGetMethod);
      if (!accessToken) {
        // 获取成功后将会缓存
        accessToken = await atomize(accessTokenGetMethod);
      }
      method.config.params.access_token = accessToken;
    }
  },
  l2Cache: redisAdapter,
});

const getAccessToken = () => thirdPartyAlova.Get('http://third-party.com/token', {
  params: {
    grant_type: 'client_credentials',
    client_id: process.env.THIRD_PARTY_CLIENT_ID,
    client_secret: process.env.THIRD_PARTY_CLIENT_SECRET
  },
  cacheFor: {
    mode: 'restore',
    expire: 1 * 3600 * 1000 // 两小时缓存时间
  }
});

const getThirdPartyUserInfo = userId => thirdPartyAlova.Get('http://third-party.com/user/info', {
  params: {
    userId
  },
  meta: {
    isThirdPartyApi: true
  }
});

写在最后

除此以外,alova还提供了分布式的验证码发送和验证、请求重试等server hooks,想了解更多的同学可以参考服务端请求策略

如果觉得alova还不错,真诚希望你可以尝试体验一下,也可以给我们来一个免费的github stars

访问alovajs的官网查看更多详细信息:alovajs官网

有兴趣可以加入我们的交流社区,在第一时间获取到最新进展,也能直接和开发团队交流,提出你的想法和建议。

有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。

相关推荐
m0_740043731 小时前
JavaScript
开发语言·javascript·ecmascript
艾小码1 小时前
Vue开发三年,我才发现依赖注入的TypeScript正确打开方式
前端·javascript·vue.js
无心水3 小时前
【分布式利器:分布式ID】6、中间件方案:Redis/ZooKeeper分布式ID实现
redis·分布式·zookeeper·中间件·分库分表·分布式id·分布式利器
j***51896 小时前
Redis 安装及配置教程(Windows)【安装】
数据库·windows·redis
A***F1579 小时前
Redis开启远程访问
数据库·redis·缓存
z***75159 小时前
Node.js卸载超详细步骤(附图文讲解)
node.js
W***r269 小时前
nvm下载安装教程(node.js 下载安装教程)
node.js
云中飞鸿10 小时前
函数:委托
javascript
j***294810 小时前
Redis 设置密码(配置文件、docker容器、命令行3种场景)
数据库·redis·docker