
可能大家对 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 层中经常需要转发客户端请求到后端微服务,你可以使用配合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 }); }); 在网关中经常需要进行鉴权、请求速率限制以及请求分发等,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中提交,我们会在最快的时间解决。