深入解析 Actions 动作机制与实战应用技巧

Zz文瑞 工具 阅读 694
赞 8 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月接了个后台管理系统重构的活,核心需求是把一堆静态按钮操作换成可配置的动作流。说白了就是用户点个按钮,系统能按预设逻辑执行一连串操作:比如先调接口、弹确认框、再跳转页面、最后刷新列表。一开始我本能想用 Vuex 或 Redux 搞个全局状态机,但琢磨半天发现太重了——这些动作基本都是独立流程,耦合度低,硬塞进状态管理反而绕远路。

深入解析 Actions 动作机制与实战应用技巧

后来翻文档时看到 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,通过回调暴露进度(但会增加使用复杂度)

总之,对于中小型项目,这种轻量级动作封装比上状态管理更实用。代码量少,学习成本低,还能快速响应需求变化。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的实现方式,比如怎么优雅处理动作回滚,欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论