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