用React和Redux实现高效可扩展的Backlog待办列表管理

萌新.玉杰 工具 阅读 1,079
赞 11 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我上周在重构一个内部项目的需求看板时,发现 Backlog 待办列表的交互体验太拉胯:拖拽排序不灵敏、状态切换没反馈、跨列移动后数据错位……折腾了两天,最后不是靠什么高大上的框架,而是把 Backlog 的原生 API + 一点轻量 JS 搞定了。今天就直接甩出我最终落地的方案,附带真实踩坑记录。

用React和Redux实现高效可扩展的Backlog待办列表管理

核心代码就这几行

Backlog 提供了官方 JS SDK(@backloghq/js-sdk),但说实话——文档写得像考古文献,而且默认只支持 OAuth 登录流,我们内网系统根本用不上。所以我改用最朴实的方式:直接调用其 REST API(v2),配合 fetch 封装几个关键方法。

先上最常用的功能:获取当前项目的待办列表(含状态、优先级、负责人):

const BACKLOG_API_BASE = 'https://your-company.backlog.com/api/v2';
const PROJECT_KEY = 'PROJ';

async function fetchBacklogIssues() {
  const token = localStorage.getItem('backlog_token'); // 你自己的 token 管理方式
  const res = await fetch(
    ${BACKLOG_API_BASE}/issues?projectIdOrKey=${PROJECT_KEY}&statusId[]=1&statusId[]=2&count=100,
    {
      headers: {
        'X-Api-Key': token,
      },
    }
  );
  return res.json();
}

注意:Backlog 的 statusId 是数字,不是名称。1=未开始,2=进行中,3=已完成——这个必须查自己空间的 /statuses 接口确认,我一开始硬编码了 1/2/3,结果客户环境里状态 ID 完全不一样,白忙活半天。

这个场景最好用:拖拽更新状态 + 排序

我们团队用 Kanban 看板,列是「待处理」「进行中」「已完成」。用户拖动 issue 到不同列时,要自动更新 statusId,并保持同状态下的顺序(Backlog 自身不保存 order 字段,得靠我们自己维护)。

关键点来了:Backlog 的 PUT /issues/{idOrKey} 支持更新 statusIdcustomField,但不支持直接改“排序值”。所以我的做法是——用一个自定义字段叫 sort_order(类型为数值),每次拖拽后计算新位置并更新它:

async function updateIssueSortAndStatus(issueId, newStatusId, newSortOrder) {
  const token = localStorage.getItem('backlog_token');
  const res = await fetch(
    ${BACKLOG_API_BASE}/issues/${issueId},
    {
      method: 'PUT',
      headers: {
        'X-Api-Key': token,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        statusId: String(newStatusId),
        customFields: JSON.stringify([
          { id: 12345, value: newSortOrder }, // 这里填你后台创建的 sort_order 字段 ID
        ]),
      }),
    }
  );
  return res.json();
}

⚠️ 踩坑提醒:这个 customFields 必须是数组,且每个对象必须含 idvalue;ID 是你在 Backlog 后台「项目设置 → 自定义字段」里看到的真实数字 ID,不是字段名。我第一次传了字符串 "sort_order",接口直接 400 不报错原因,debug 半小时才发现。

批量更新?别信文档里的 bulk 接口

Backlog 文档说有 POST /issues/bulk,但实测它只支持创建,不支持批量更新。真要批量改状态?老老实实 for 循环 + Promise.all —— 别嫌慢,它本身就有请求限频(每分钟 100 次),强行并发反而被 429 打回来。

我最后写的批量处理逻辑是这样的:

async function batchUpdateIssues(issuesToUpdate) {
  const chunks = [];
  for (let i = 0; i < issuesToUpdate.length; i += 5) {
    chunks.push(issuesToUpdate.slice(i, i + 5));
  }

  const results = [];
  for (const chunk of chunks) {
    const promises = chunk.map(({ id, statusId, sortOrder }) =>
      updateIssueSortAndStatus(id, statusId, sortOrder)
    );
    const chunkRes = await Promise.allSettled(promises);
    results.push(...chunkRes);
    await new Promise(r => setTimeout(r, 500)); // 主动控频
  }

  return results;
}

为什么是 5 个一组?因为实测 5 个并发 + 500ms 间隔,刚好卡在限频阈值下,成功率 100%。试过 10 个一组,失败率飙升到 30%。

权限和 Token 怎么搞?别碰 OAuth

如果你只是内部工具,别卷 OAuth。Backlog 支持「个人访问令牌(PAT)」,路径是:右上角头像 → My settings → API → Generate new token。勾选 Issues: Read & Write 就够了。

Token 存 localStorage 是方便,但别忘了加前缀防冲突:

// ✅ 好习惯
localStorage.setItem('mytool_backlog_token', 'xxx');

// ❌ 别这么干,容易被其他脚本覆盖
localStorage.setItem('backlog_token', 'xxx');

踩坑提醒:这三点一定注意

  • 时间字段全是字符串,不是 Date 对象:比如 createdOn 返回的是 "2024-05-12T08:23:45+09:00",JS 里直接 new Date() 就行,但别想当然用 moment 或 dayjs 的 parse,Backlog 有些时间字段格式不规范(比如缺时区),会解析成 Invalid Date。
  • 分页靠 nextCursor,不是 page 参数:Backlog v2 分页用的是游标式(cursor-based),返回体里有 nextCursor 字段,下一页请求要带上 ?cursor=xxx。文档里藏得特别深,我翻了三遍才找到。
  • 附件上传必须用 multipart/form-data,不能 JSON:哪怕你只传一个文件,也得构造 FormData,且 file 字段名固定为 file,不是 attachment。试过 JSON 发送二进制,接口直接静默失败。

高级技巧:用 Webhook 做实时同步

我们有个需求:当 Backlog 里某 issue 被修改,前端看板要立刻刷新。官方 Webhook 支持订阅 issue_updated 事件,但 payload 只给 issueId,不给新字段值。所以我在后端建了个轻量代理服务:

  1. Backlog Webhook 推送到 https://jztheme.com/api/backlog-webhook
  2. 服务收到后,立即调用 GET /issues/{id} 拉最新数据
  3. 通过 SSE 或 WebSocket 推给前端

这样避免前端轮询,也绕开了 Backlog 没提供 delta payload 的缺陷。代码不多,但省了大量 polling 请求和 UI 卡顿。

结语

Backlog 的 API 真的不算友好,文档滞后、字段命名随意、错误提示模糊——但只要你接受它是个“能用就行”的企业级工具,而不是追求优雅的开源项目,其实很快就能摸清套路。我现在这套封装已经跑了三个月,没出过一次线上事故。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如:结合 GitHub PR 自动关联 issue、用 Backlog 数据生成燃尽图、对接飞书机器人自动通知……后续会继续分享这类博客。

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

暂无评论