深入解析 Actions 动作机制与实战应用技巧
项目初期的技术选型
上个月接了个后台管理系统重构的活,核心需求是把一堆静态按钮操作换成可配置的动作流。说白了就是用户点个按钮,系统能按预设逻辑执行一连串操作:比如先调接口、弹确认框、再跳转页面、最后刷新列表。一开始我本能想用 Vuex 或 Redux 搞个全局状态机,但琢磨半天发现太重了——这些动作基本都是独立流程,耦合度低,硬塞进状态管理反而绕远路。
后来翻文档时看到 Vue 3 的组合式 API 能封装可复用逻辑,突然想到:为什么不直接搞个 useActions 钩子?每个动作封装成独立函数,传入配置就能跑,还能自动处理 loading、错误提示这些脏活。关键是轻量,不用改现有架构,后端给个动作配置表前端直接渲染就行。就这么定了。
核心代码就这几行
先搭个骨架:定义动作配置类型,包含 API 地址、成功回调、失败处理这些。重点是要支持异步链式操作,比如「确认 → 请求 → 跳转」这种顺序执行。
// types.js
export const ACTION_TYPES = {
CONFIRM: 'confirm',
API_CALL: 'apiCall',
NAVIGATE: 'navigate'
};
// useActions.js
import { ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
export function useActions() {
const loading = ref(false);
const executeAction = async (actionConfig) => {
loading.value = true;
try {
for (const step of actionConfig.steps) {
await handleStep(step);
}
} catch (error) {
ElMessage.error(error.message || '操作失败');
} finally {
loading.value = false;
}
};
const handleStep = async (step) => {
switch (step.type) {
case ACTION_TYPES.CONFIRM:
return ElMessageBox.confirm(step.message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
});
case ACTION_TYPES.API_CALL:
const res = await fetch(https://jztheme.com/api${step.endpoint}, {
method: step.method || 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(step.payload)
});
if (!res.ok) throw new Error('请求失败');
return res.json();
case ACTION_TYPES.NAVIGATE:
window.location.href = step.url;
return Promise.resolve();
default:
throw new Error(未知动作类型: ${step.type});
}
};
return { executeAction, loading };
}
用的时候贼简单,比如删除用户:
// 组件内
const { executeAction, loading } = useActions();
const deleteUser = () => {
executeAction({
steps: [
{ type: 'confirm', message: '确定删除该用户?' },
{
type: 'apiCall',
endpoint: '/user/delete',
payload: { id: currentUserId }
},
{ type: 'navigate', url: '/user/list' }
]
});
};
最大的坑:异步中断和 loading 状态错乱
上线前测着挺顺,结果 QA 一通狂点按钮直接炸了。问题出在用户快速点击时,前一个动作还没结束,新的动作又触发,导致:
- loading 状态反复横跳(因为多个动作同时修改同一个 loading 变量)
- 确认框弹出来又被新动作覆盖,用户点了确定实际执行的是另一个操作
折腾了半天才发现,根本原因是所有动作共享同一个 loading 状态。但改成分离状态又麻烦——每个动作都要单独管理 loading,组件里得维护一堆变量。
后来灵机一动:既然动作是顺序执行的,为啥不直接禁用按钮?在 executeAction 开头加个锁:
// useActions.js 修改部分
const isExecuting = ref(false); // 新增执行锁
const executeAction = async (actionConfig) => {
if (isExecuting.value) return; // 快速点击直接忽略
isExecuting.value = true;
loading.value = true;
try {
// ...原有逻辑
} finally {
loading.value = false;
isExecuting.value = false; // 执行完释放锁
}
};
这招亲测有效,而且简单粗暴。虽然牺牲了「并行动作」的可能性,但业务场景里本来就不需要同时跑多个动作流,反而避免了状态污染。
另一个隐蔽问题:导航动作的 Promise 处理
有次测试发现,执行到跳转步骤时控制台报错 Promise rejection。查了半天才明白:当执行 window.location.href = xxx 时,页面会立即跳转,后续的 Promise 链就断了。但我们的 handleStep 要求所有步骤都返回 Promise,跳转操作虽然写了 return Promise.resolve(),实际上跳转后 JS 环境已经销毁,这个 resolve 根本没机会执行。
解决方案是在跳转前手动 resolve:
case ACTION_TYPES.NAVIGATE:
// 先 resolve 当前 Promise,再跳转
setTimeout(() => {
window.location.href = step.url;
}, 0);
return Promise.resolve();
用 setTimeout 把跳转扔到下一个事件循环,确保当前 Promise 链能正常结束。虽然有点 hack,但实测稳定,而且不影响用户体验——用户根本感觉不到那几毫秒延迟。
最终效果和遗留问题
上线两周后,动作配置从最初的 5 种扩展到 12 种(加了下载文件、复制文本等),新增需求基本不用动核心逻辑,只改配置就行。最爽的是错误处理统一了,以前每个按钮都要写 try-catch,现在全在 executeAction 里兜底。
不过还有两个小问题没彻底解决:
- 动作回滚困难:比如 API 调用成功但跳转失败,没法自动撤销前面的操作。目前靠后端做幂等性保证,前端只提示错误
- loading 粒度太粗:整个动作流共用一个 loading,其实中间步骤可能需要不同提示(比如「正在删除…」「正在跳转…」)。但加细粒度又会让 API 变复杂,暂时搁置了
但说实话,这些问题对业务影响不大。产品经理反而夸这次改动让运营配动作快多了——他们现在直接在后台填 JSON 配置,不用等开发排期。
回顾与反思
回头看,这个方案最大的优势是隔离了业务逻辑和动作执行。以前改个按钮行为要翻三四层组件,现在只改一行配置。虽然初期踩了异步和导航的坑,但解决成本远低于推倒重来。
如果重做的话,可能会考虑两点优化:
- 用 AbortController 支持动作取消(不过业务场景真用不上)
- 把 loading 状态下沉到每个 step,通过回调暴露进度(但会增加使用复杂度)
总之,对于中小型项目,这种轻量级动作封装比上状态管理更实用。代码量少,学习成本低,还能快速响应需求变化。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的实现方式,比如怎么优雅处理动作回滚,欢迎评论区交流!

暂无评论