浏览器扩展URL预览请求竞态问题求助(禁用第三方包)
解决浏览器扩展URL预览的请求竞态问题
问题
我开发的浏览器扩展里,有个功能是用户在文本框输入内容时,实时提取首个URL,展示带OG标签的预览弹窗。但遇到个坑:先输无效URL(比如https://www.google.co),再输有效URL(https://www.google.com),无效请求的响应反而晚回来,把有效结果覆盖成null,导致预览框出不来。
不想加第三方包增大体积,之前试了AbortController但总是误杀有效请求,求正确解法。
现有代码
React钩子部分
useEffect(() => { const url = value.match(/(https?://[^\s]+)/g)?.[0]; if (!url) return setUserLinkExtract(null); (async () => { const dom = await extractDomFromBackground(url); if (!dom) return setUserLinkExtract(null); const parser = new DOMParser(); const htmlDoc = parser.parseFromString(dom, "text/html"); const ogTitle = /* 提取标题逻辑 */ const ogDescription = /* 提取描述逻辑 */ const ogImage = /* 提取图片逻辑 */ setUserLinkExtract({ dom, ogTags: { description: ogDescription, image: ogImage, title: ogTitle }, title: ogTitle, url, }); })(); }, [deferredValue]);
后台脚本部分
export async function extractDomFromBackground(url: string): Promise<string> { try { const options = { headers: { "Content-Type": "text/html", "User-Agent": `Chrome/90.0.4430.212`, }, method: "GET", }; const res = await fetch(url, options); if (!res.ok && res.status !== 200) return ""; const dom = await res.text(); return dom; } catch (e) { return ""; } }
解决方案
方案1:正确用AbortController中止旧请求
之前用AbortController出错,大概率是没在useEffect的清理函数里中止上一次请求,或者没把信号和请求绑定好。
修改React钩子,给每个请求配独立的控制器,依赖更新时自动中止旧请求:
useEffect(() => { const abortCtrl = new AbortController(); const signal = abortCtrl.signal; const url = value.match(/(https?://[^\s]+)/g)?.[0]; if (!url) { setUserLinkExtract(null); return; } (async () => { try { const dom = await extractDomFromBackground(url, signal); // 请求已被中止的话直接返回,不更新状态 if (signal.aborted) return; if (!dom) { setUserLinkExtract(null); return; } const parser = new DOMParser(); const htmlDoc = parser.parseFromString(dom, "text/html"); // 补全OG标签提取逻辑 const ogTitle = htmlDoc.querySelector('meta[property="og:title"]')?.content || htmlDoc.title; const ogDescription = htmlDoc.querySelector('meta[property="og:description"]')?.content || ""; const ogImage = htmlDoc.querySelector('meta[property="og:image"]')?.content || ""; setUserLinkExtract({ dom, ogTags: { description: ogDescription, image: ogImage, title: ogTitle }, title: ogTitle, url, }); } catch (e) { // 忽略中止错误,其他错误再设为null if (e.name !== 'AbortError') setUserLinkExtract(null); } })(); // 清理函数:组件卸载/依赖更新时中止上一次请求 return () => abortCtrl.abort(); }, [deferredValue]);
然后给后台脚本加signal参数,传给fetch:
export async function extractDomFromBackground(url: string, signal?: AbortSignal): Promise<string> { try { const options = { headers: { "Content-Type": "text/html", "User-Agent": `Chrome/90.0.4430.212`, }, method: "GET", signal, // 绑定中止信号 }; const res = await fetch(url, options); if (!res.ok && res.status !== 200) return ""; const dom = await res.text(); return dom; } catch (e) { // 请求被中止时返回空,不报错 if (e.name === 'AbortError') return ""; return ""; } }
方案2:追踪当前URL,忽略旧请求响应
如果不想改后台脚本,也可以在前端记录发起请求时的URL,响应回来后对比最新的URL,不一致就直接忽略:
useEffect(() => { const requestUrl = value.match(/(https?://[^\s]+)/g)?.[0]; if (!requestUrl) { setUserLinkExtract(null); return; } (async () => { const dom = await extractDomFromBackground(requestUrl); // 对比最新URL和发起请求时的URL,不一样就不更新状态 const latestUrl = value.match(/(https?://[^\s]+)/g)?.[0]; if (latestUrl !== requestUrl) return; if (!dom) { setUserLinkExtract(null); return; } const parser = new DOMParser(); const htmlDoc = parser.parseFromString(dom, "text/html"); const ogTitle = htmlDoc.querySelector('meta[property="og:title"]')?.content || htmlDoc.title; const ogDescription = htmlDoc.querySelector('meta[property="og:description"]')?.content || ""; const ogImage = htmlDoc.querySelector('meta[property="og:image"]')?.content || ""; setUserLinkExtract({ dom, ogTags: { description: ogDescription, image: ogImage, title: ogTitle }, title: ogTitle, url: requestUrl, }); })(); }, [deferredValue]);
两种方案对比
- AbortController方案:能直接中止无用请求,省网络资源,性能更好,推荐用这个。
- URL追踪方案:代码更简单,不用改后台,但旧请求还是会跑完,浪费点资源,适合快速临时解决。
内容的提问来源于stack exchange,提问作者Atharane
相关产品推荐
相关产品推荐

