可能大家对 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中提交,我们会在最快的时间解决。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.