V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ScottHU
V2EX  ›  Node.js

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

  •  
  •   ScottHU · 22 小时 57 分钟前 · 421 次点击

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

    const { loading, data, error } = useRequest(() => alovaInstance.Get('/xxx'))
    

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

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

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

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

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

    在 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';
    
    // 创建异步本地存储实例
    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文件存储适配器。

    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实现,我们改造一下实现。

    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 的原子性操作。

    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中提交,我们会在最快的时间解决。

    目前尚无回复
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2680 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 13:33 · PVG 21:33 · LAX 05:33 · JFK 08:33
    ♥ Do have faith in what you're doing.