如何动态组合 arrays 和 objects 并按 billingCycle 分组
结论
可以先用 map() 遍历 cyclesArray,再用 filter() 找出当前周期下的 plans,并通过 slice(0, 4) 将每个周期的 plan 数量限制在 4 条以内。
这属于数据结构转换,与 React 或 Redux Saga 无关。可以将转换逻辑写成纯函数,在提交表单或 Saga 调用 API 之前执行。
实现方式
billing cycle 是字符串,所以数组中的值需要加引号:
const cyclesArray = ["annual", "monthly", "weekly"];
const plansArray = [
{ value: 200, currency: "EUR", billingCycle: "annual" },
{ value: 220, currency: "USD", billingCycle: "monthly" },
{ value: 20, currency: "CHF", billingCycle: "monthly" },
];
下面的函数可以生成 API 所需的数据:
const buildProductPayload = (name, cycles, plans) => ({
name,
bCycles: cycles.map((cycle) => ({
interval: cycle,
plan: plans
.filter((item) => item.billingCycle === cycle)
.slice(0, 4)
.map((item) => ({
currency: {
name: item.currency,
amount: item.value,
},
})),
})),
});
const objectToSend = buildProductPayload(
"whatever",
cyclesArray,
plansArray
);
console.log(objectToSend);
生成结果如下:
{
name: "whatever",
bCycles: [
{
interval: "annual",
plan: [
{
currency: {
name: "EUR",
amount: 200
}
}
]
},
{
interval: "monthly",
plan: [
{
currency: {
name: "USD",
amount: 220
}
},
{
currency: {
name: "CHF",
amount: 20
}
}
]
},
{
interval: "weekly",
plan: []
}
]
}
这段转换分为四步:
cycles.map(...)为每个 billing cycle 创建一个对象。plans.filter(...)筛选出billingCycle与当前周期相同的 plan。slice(0, 4)只保留前 4 个 plan。- 最后一层
map(...)将原始 plan 转换为后端要求的结构。
不发送空周期
上面的写法会保留没有 plan 的周期,例如:
{
interval: "weekly",
plan: []
}
如果后端不接受空的 plan 数组,可以在生成后将其过滤掉:
const buildProductPayload = (name, cycles, plans) => ({
name,
bCycles: cycles
.map((cycle) => ({
interval: cycle,
plan: plans
.filter((item) => item.billingCycle === cycle)
.slice(0, 4)
.map((item) => ({
currency: {
name: item.currency,
amount: item.value,
},
})),
}))
.filter((cycle) => cycle.plan.length > 0),
});
是否保留空周期应以后台接口约定为准,前端不应自行猜测。
数据较多时使用 reduce()
对于表单中的少量数据,map() 配合 filter() 比较容易阅读。不过,每处理一个 cycle,它都会重新遍历一次 plansArray。数据量较大时,可以先用 reduce() 分组:
const buildProductPayload = (name, cycles, plans) => {
const groupedPlans = plans.reduce((groups, item) => {
const cycle = item.billingCycle;
if (!groups[cycle]) {
groups[cycle] = [];
}
if (groups[cycle].length < 4) {
groups[cycle].push({
currency: {
name: item.currency,
amount: item.value,
},
});
}
return groups;
}, {});
return {
name,
bCycles: cycles.map((cycle) => ({
interval: cycle,
plan: groupedPlans[cycle] ?? [],
})),
};
};
这种写法只遍历一次 plansArray,并在分组时将每组限制为最多 4 条。
在 Redux Saga 中使用
Redux Saga 只负责组织异步请求,不负责数据分组。可以直接在 Saga 中调用同一个纯函数:
import { call, put } from "redux-saga/effects";
function* createProductSaga(action) {
try {
const { name, cyclesArray, plansArray } = action.payload;
const payload = buildProductPayload(
name,
cyclesArray,
plansArray
);
const response = yield call(api.createProduct, payload);
yield put({
type: "CREATE_PRODUCT_SUCCESS",
payload: response,
});
} catch (error) {
yield put({
type: "CREATE_PRODUCT_FAILURE",
error,
});
}
}
也可以在触发 action 之前完成转换。无论采用哪种方式,都不要直接修改 React state 中的原数组。上面的 map()、filter()、slice() 和 reduce() 都会返回新数据,不会破坏现有 state。
注意事项
前端使用 slice(0, 4),只能保证本次发送的数据不超过 4 条。后端仍然需要校验数量限制,否则调用方可以绕过前端,直接提交更多 plans。
提交前还可以检查以下内容:
billingCycle是否存在于cyclesArray。value是否为后端允许的数值。currency是否为有效币种。- 同一个周期和币种是否允许重复。
- plans 超过 4 条时,是直接截断还是提示用户修改。
如果超过 4 条属于表单错误,就不适合用 slice(0, 4) 静默截断。可以先检查数量,并在超出限制时阻止提交:
const hasTooManyPlans = cyclesArray.some(
(cycle) =>
plansArray.filter(
(item) => item.billingCycle === cycle
).length > 4
);
if (hasTooManyPlans) {
throw new Error("每个 billing cycle 最多只能包含 4 个 plans");
}
这样可以防止用户填写的数据在不知情的情况下被截掉。
备注:内容仅供参考。
