如何为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
相关产品推荐
相关产品推荐

