You need to enable JavaScript to run this app. 如何为Web Worker onmessage事件创建Promise适配不定量递归REST调用
优惠活动
大模型
产品
解决方案
定价
更多

如何为Web Worker onmessage事件创建Promise适配不定量递归REST调用

问题根因拆解
  • 你遇到的resolver[e.data.url]()报错“不是函数”,并非语法问题:对象按键名取方法调用的写法完全合法,报错核心是键名完全不匹配——你存储根站点resolve方法时用的键是带https前缀的https://www.somewebsite.com/,但给Worker发请求时传的子站点url是www.somewebsite.com开头无协议的字符串,Worker回传的e.data.url和发请求时传的值一致,两边键名对不上,取到的就是undefined,自然调用报错。
  • 原逻辑核心硬伤1:初始化阶段直接写了resolver[baseUrl]().then(...),等于在Worker还没返回根站点响应的时候,就手动把根站点对应的Promise给解决了,完全没等真实接口返回。
  • 原逻辑核心硬伤2:Promise.all(Object.values(resolver))是快照机制——执行这行代码的瞬间,会把当时resolver对象里所有的方法收集成固定的任务列表,后续递归新增的子站点请求、新加的resolve方法,根本不会被纳入等待范围,所以永远等不到所有请求跑完。
  • 关于你疑惑的“keyName[index]()能不能调用指定resolve方法”:语法本身完全支持,只要键名严格匹配存值时的键,就可以正常调用,报错是键名匹配的问题,不是语法不支持。
实现调整方案

应对不定数量的递归请求,不能依赖快照式的Promise.all,要采用「在途请求计数器 + 统一键名匹配」的逻辑,完全不需要提前知道站点总数量:

  • 统一所有请求的路径生成规则,确保请求发送时存储resolve方法的键,和Worker回传时的url值严格一致,从根源避免键名不匹配的报错。
  • 不依赖固定长度的Promise列表,维护一个全局在途请求计数器:每次发新请求就给计数器+1,不管是请求成功、失败还是Worker报错,只要请求完成结算就给计数器-1。
  • 每次计数器结算时做一次检查:如果计数器降到0,说明所有递归派出去的请求都跑完了,此时再触发最终的数据格式化回调即可。
调整后的主页面业务代码
function getTreeData(cb) {
  let allSites = [];
  const baseUrl = "https://www.somewebsite.com/";
  // 统一存储请求对应的resolve/reject方法,用站点完整路径做唯一键
  const resolverMap = {};
  const rejecterMap = {};
  // 在途请求计数器,实现递归完成检测
  let pendingRequestCount = 0;
  const workerUrl = "https://www.somewebsite.com/siteassets/worker.js";
  const myWorker = new Worker(workerUrl);
  const requestDigest = document.getElementById("__REQUESTDIGEST").value;

  // 封装通用请求方法,统一计数、统一逻辑
  function fetchSiteInfo(sitePath) {
    return new Promise((resolve, reject) => {
      // 发请求前在途计数+1
      pendingRequestCount++;
      // 按统一键存储resolve/reject
      resolverMap[sitePath] = resolve;
      rejecterMap[sitePath] = reject;
      // 向Worker发送请求
      myWorker.postMessage({
        requestDigest: requestDigest,
        qs1: "/_api/web/webinfos?$select=ServerRelativeUrl,Title",
        qs2: "/_api/Web/RoleAssignments?$expand=Member/Users,RoleDefinitionBindings",
        url: sitePath,
      });
    }).then((result) => {
      // 请求成功,在途计数-1
      pendingRequestCount--;
      return result;
    }).catch((err) => {
      // 请求失败,在途计数-1
      console.error(`站点${sitePath}请求失败`, err);
    }).finally(() => {
      // 每次请求结算完成,检查是否所有任务都跑完
      checkAllTaskDone();
    });
  }

  // 全任务完成检测逻辑
  function checkAllTaskDone() {
    if (pendingRequestCount === 0) {
      // 终止Worker释放资源
      myWorker.terminate();
      // 触发最终回调,处理所有收集到的站点数据
      reduceData(cb, allSites);
    }
  }

  // Worker消息响应处理
  myWorker.onmessage = function (e) {
    const { url, info, sites } = e.data;
    // 将当前站点数据加入总集合
    allSites = allSites.concat([
      { pathname: url, groups: info },
    ]);
    // 触发当前请求对应的resolve(键名严格匹配,不会报错)
    if (resolverMap[url]) {
      resolverMap[url](e.data);
      // 调用完成后删除对应存储,避免内存泄漏
      delete resolverMap[url];
      delete rejecterMap[url];
    }
    // 递归处理子站点
    sites.forEach((subSite) => {
      // 统一拼接为和请求规则一致的完整路径,避免键名不匹配
      const subSitePath = baseUrl.replace(/\/$/, "") + subSite.url;
      // 发送子站点请求,自动纳入在途计数
      fetchSiteInfo(subSitePath);
    });
  };

  // Worker错误处理
  myWorker.onerror = function (err) {
    console.error("Worker运行错误", err);
    // 错误场景也要扣减计数,避免计数器卡死
    pendingRequestCount--;
    if (pendingRequestCount < 0) pendingRequestCount = 0;
    checkAllTaskDone();
  };

  // 启动根站点的初始请求
  fetchSiteInfo(baseUrl);
}
配套Worker脚本调整

原Worker中的formatSites函数写死了返回的url值,需改为传递实际的子站点路径,否则回传的子站点列表会异常:

function formatSites(props) {
  return {
    url: props.ServerRelativeUrl,
  };
}

内容的提问来源于stack exchange,提问作者CleverlyDone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.27 03:36:03