18 KiB
dianshan 插件爬取任务开发文档(逻辑/通信/状态)
目的:把“点击爬取后发生了什么、会触发哪些方法、给谁通信、改变哪些状态”说清楚,方便后续二开与排查。
范围:
dianshan/目录内的扩展(popup + background + content/overlay + 外部网页桥接)。注意:当前项目代码里没有
startDianshanCrawl()/getDianshanCrawlState()这类方法名; 正确入口分别是startCrawl()(dianshan/src/background/task/crawlTask.ts:14)与getCrawlTaskState()(dianshan/src/background/task/taskState.ts:10),以及对应的消息 action(START_CRAWL/GET_CRAWL_STATE)。
1. 关键文件索引(按职责)
1.1 Popup(扩展弹窗 UI)
dianshan/src/popup/App.vue- UI 展示:平台选择、开始/取消/继续按钮、步骤列表、底部语言切换/版本号
dianshan/src/popup/hook/use-scan.ts- 与 background 通信(开始/取消/继续/读取状态)
- 监听
chrome.storage.onChanged同步任务状态 - 计时器:根据
startedAt计算已用时
dianshan/src/popup/hook/use-i18n.ts- Popup 文案 i18n(中文/英文),持久化到
chrome.storage.local
- Popup 文案 i18n(中文/英文),持久化到
1.2 Background(后台调度 + 任务状态机)
dianshan/src/background/index.ts- 统一接收 popup/content/external 的 message(
chrome.runtime.onMessage) - 监听窗口/Tab 关闭(自动暂停任务)
- 监听 storage 变化并对外广播(external bridge)
- 统一接收 popup/content/external 的 message(
dianshan/src/background/task/crawlTask.ts- 任务生命周期:start/cancel/pause/resume/dismiss
- 步骤执行器:按 steps 顺序打开页面、等待、抓取、处理中断/重试
- 完成后收尾:发送结果 -> 清空记录 -> 关闭窗口
dianshan/src/background/task/taskState.ts- 读写
chrome.storage.local的crawlTaskState - 写入时同时
sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)推送给爬取窗口(overlay 用)
- 读写
dianshan/src/background/task/helper.tsopenSingleTabWindow()创建 popup 类型新窗口并返回{windowId, tabId}waitForTabLoaded()等待 tab load completescrapeStepInContent()与 content script 通信执行抓取(tabs.sendMessage)
dianshan/src/background/service/externalBridge.ts- 与外部网页(官网/业务系统)进行 external message / long-connect 通信
- 监听 storage 变化,广播
DIANSHAN_CRAWL_STATE / DONE / ...等事件
1.3 Content Script(爬取窗口页内逻辑)
dianshan/src/content/pageRunner.ts- 接收 background 指令:
CHECK_INTERRUPT:判断登录/验证码/404/未就绪等“需要人工处理”的中断SCRAPE_STEP:等待关键 selector 稳定后执行 DOM 抓取
- 接收 background 指令:
dianshan/src/content/crawlOverlay.ts- 爬取窗口左下角悬浮 UI(圆形计时菜单)
- 点击圆形菜单展开后展示“与 popup 类似”的步骤进度/暂停提示/继续/取消
- 接收 background 推送的
CRAWL_STATE_UPDATE状态 - 初始化时会拉取一次
GET_CRAWL_STATE_FOR_TAB快照(只在爬取 tab 返回状态)
1.4 Shared(跨模块协议)
dianshan/src/shared/message.ts- popup/content -> background 的消息 action 类型(例如
START_CRAWL)
- popup/content -> background 的消息 action 类型(例如
dianshan/src/shared/tab.ts- background -> content(爬取窗口 tab)消息 action 类型(例如
CRAWL_STATE_UPDATE)
- background -> content(爬取窗口 tab)消息 action 类型(例如
dianshan/src/types/crawl.tsCrawlTaskState/CrawlProgressStep/CrawlPauseInfo等任务数据结构
1.5 关键方法定位(按真实代码)
下面列的都是当前项目里“确实存在”的方法/入口,后面章节会反复引用;每条都附带文件地址+行号,方便你 Ctrl+P 直达。
-
popup 启动爬取:
handleScan():dianshan/src/popup/hook/use-scan.ts:46 -
popup 取消爬取:
handleCancelCrawl():dianshan/src/popup/hook/use-scan.ts:70 -
popup 继续爬取:
handleResumeCrawl():dianshan/src/popup/hook/use-scan.ts:83 -
popup 同步状态:
handleStorageChanged():dianshan/src/popup/hook/use-scan.ts:146 -
background 消息总入口:
chrome.runtime.onMessage.addListener(...):dianshan/src/background/index.ts:22 -
background 路由:
case "START_CRAWL"等:dianshan/src/background/index.ts:34 -
background 窗口/Tab 关闭监听:
windows.onRemoved/tabs.onRemoved:dianshan/src/background/index.ts:92 -
任务启动:
startCrawl():dianshan/src/background/task/crawlTask.ts:14 -
任务执行器:
runCrawlSteps():dianshan/src/background/task/crawlTask.ts:63 -
完成收尾:
finalizeCompletedTask():dianshan/src/background/task/crawlTask.ts:312 -
取消任务:
cancelCrawl():dianshan/src/background/task/crawlTask.ts:131 -
窗口关闭自动暂停:
pauseCrawlOnWindowRemoved():dianshan/src/background/task/crawlTask.ts:158 -
Tab 关闭自动暂停:
pauseCrawlOnTabRemoved():dianshan/src/background/task/crawlTask.ts:188 -
继续/恢复:
resumeCrawl():dianshan/src/background/task/crawlTask.ts:219 -
状态读写:
getCrawlTaskState()/setCrawlTaskState():dianshan/src/background/task/taskState.ts:10/dianshan/src/background/task/taskState.ts:20 -
新开爬取窗口:
openSingleTabWindow():dianshan/src/background/task/helper.ts:8 -
等待页面加载:
waitForTabLoaded():dianshan/src/background/task/helper.ts:45 -
让 content 抓取:
scrapeStepInContent():dianshan/src/background/task/helper.ts:90 -
content 执行器入口:
setupPageRunner():dianshan/src/content/pageRunner.ts:28 -
content 抓取处理:
handlePageRunnerMessage():dianshan/src/content/pageRunner.ts:38 -
爬取窗口悬浮 UI:
mountCrawlOverlay():dianshan/src/content/crawlOverlay.ts:36 -
悬浮 UI 初始化拉取快照:
GET_CRAWL_STATE_FOR_TAB:dianshan/src/content/crawlOverlay.ts:67 -
悬浮 UI 实时状态推送:
CRAWL_STATE_UPDATE:dianshan/src/content/crawlOverlay.ts:54
2. 核心数据结构:CrawlTaskState(状态机)
存储位置:chrome.storage.local['crawlTaskState']
关键字段(简化说明):
id:任务唯一 ID(platformId-startedAt)platformId/platformName:平台信息windowId/tabId:当前爬取窗口与承载爬取的 tab(overlay 只在这个 tab 渲染)startedAt:任务开始时间戳(用于 popup/overlay 计时)status:running | paused | completed | failed | canceledpause:当status='paused'时存在,包含reason:reauth | shield | not_found | page_not_ready | window_closedmessage:给用户看的暂停提示
currentStepIndex:当前执行到的步骤索引steps[]:每个步骤的进度条记录uniqueKey/namestatus:pending | running | success | failedresult/message:抓取结果或失败原因
状态同步策略:
- background 每次
setCrawlTaskState()/updateCrawlTaskState()都会写入 storage - popup:监听
chrome.storage.onChanged,永远以 storage 为准渲染 UI - overlay:通过
sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)实时更新(并可在初始化时请求快照) - 外部网页:
chrome.storage.onChanged->externalBridge.broadcastCrawlStorageChange()广播结果/状态
3. “点击爬取”后的完整时序(从 UI 到抓取)
以下用“触发方法 / 通信对象 / 状态变化”描述每一步。
3.1 Popup 点击 “Scan now”
- 触发方法
- popup:
use-scan.ts -> handleScan()(dianshan/src/popup/hook/use-scan.ts:46)
- 通信
- popup -> background:
sendBackgroundMessage({ action:'START_CRAWL', payload:{ platformId } })
- background 入口
background/index.ts的chrome.runtime.onMessage收到START_CRAWL(dianshan/src/background/index.ts:22/dianshan/src/background/index.ts:34)- 调用:
crawlTask.startCrawl(platformId)(dianshan/src/background/task/crawlTask.ts:14)
- 状态变化(startCrawl)
openSingleTabWindow(steps[0].url):创建爬取窗口(popup 类型)得到{windowId, tabId}(dianshan/src/background/task/helper.ts:8)- 构建初始
CrawlTaskState:status='running'currentStepIndex=0steps[0].status='running',其余pending
setCrawlTaskState(nextState)写入 storage(dianshan/src/background/task/taskState.ts:20)- 这一步会触发:
- popup 的 storage 监听更新 UI
- background 同时
sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)推送给 overlay(dianshan/src/background/task/taskState.ts:20) - externalBridge 通过 storage.onChanged 对外广播(如果网页有连 port)
- 这一步会触发:
- 启动执行器
- background:创建
AbortController,并异步执行:runCrawlSteps(taskId, tabId, platform.steps, signal, startIndex=0)(dianshan/src/background/task/crawlTask.ts:63)
4. “执行一个步骤”会发生什么(runCrawlSteps 的循环体)
每个 step 的处理流程如下(对每个 i 从 startIndex 到 steps.length-1):
4.1 进入新 step:先更新状态机
- 触发方法
- background:
updateCrawlTaskState(taskId, updater)
- 状态变化
currentStepIndex = isteps[i].status = 'running'(其它步骤保持原样)
- 通信影响
- 因为
updateCrawlTaskState()内部会setCrawlTaskState():- storage 写入 -> popup UI 更新
- 同时
sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)-> overlay 更新 - storage.onChanged -> external bridge 广播(如果外部网页连着)
4.2 跳转页面并等待加载完成
- 触发方法
- background:
chrome.tabs.update(tabId, { url: step.url, active: true }) - background:
waitForTabLoaded(tabId, signal)
- 通信对象
- background -> Chrome tabs API(无 content 参与)
- 状态变化
- 不改变任务状态,只是准备让 content script 进入目标页面上下文
4.3 让 content script 执行抓取(核心通信)
- 触发方法
- background:
scrapeStepInContent(tabId, step, signal)
- 通信
- background -> content(爬取窗口 tab):
chrome.tabs.sendMessage(tabId, { action:'SCRAPE_STEP', payload:{ fields, checkSelector } })
- content 入口
content/pageRunner.ts的chrome.runtime.onMessage收到SCRAPE_STEPdetectPageInterrupt():先判断是否需要人工处理(登录/验证码/404/未就绪)waitForStableSelector(checkSelector, timeout):等待关键 DOM 稳定出现processFields(fields, document.body):按配置抓取 DOM 数据
- 返回值约定(PageRunnerResponse)
ok: true, data: DomScrapeResult:本步骤抓取成功interrupt: CrawlPauseInfo:需要人工处理(会进入 paused)ok: false, error:未就绪/异常,background 会重试
4.4 抓取成功:写入结果并标记 step success
- 触发方法
- background:
updateCrawlTaskState(taskId, updater)
- 状态变化
steps[i].status = 'success'steps[i].result = res.data
- 通信影响
- 同 4.1:storage + overlay 推送 + external bridge 广播
4.5 抓取中断:进入 paused(需要用户处理)
中断来源:
- content 检测到
shield/reauth/not_found/page_not_ready - 或者:爬取窗口被用户关掉(见第 6 章)
- 触发方法(以 content 中断为例)
- background:
updateCrawlTaskState(taskId, s => ({ ...s, status:'paused', pause: interrupt }))(dianshan/src/background/task/crawlTask.ts:93)
- 状态变化
status='paused'pause={reason,message}
- 执行器行为
- background 在
runCrawlSteps内部“死等恢复”:- 循环检查
getCrawlTaskState()?.status === 'paused' - 直到用户点击“继续”把状态改回
running才继续
- 循环检查
- UI 行为
- popup / overlay 都会显示暂停提示 + “Continue”
5. “点击继续”会发生什么(RESUME_CRAWL)
触发来源:
- popup 的 Continue
- overlay 的 Continue
5.1 popup/overlay 发消息
- popup:
use-scan.ts -> handleResumeCrawl() -> sendBackgroundMessage({action:'RESUME_CRAWL'})(dianshan/src/popup/hook/use-scan.ts:83) - overlay:
crawlOverlay.ts -> chrome.runtime.sendMessage({action:'RESUME_CRAWL'})(dianshan/src/content/crawlOverlay.ts:392)
5.2 background 处理 resume
入口:
background/index.tsswitch caseRESUME_CRAWL(dianshan/src/background/index.ts:66)- 调用
crawlTask.resumeCrawl()(dianshan/src/background/task/crawlTask.ts:219)
- 调用
resumeCrawl 分两类:
A) 登录/验证码等中断(窗口仍存在)
条件:
state.status === 'paused'state.pause.reason !== 'window_closed'state.windowId && state.tabId还在
行为:
- 仅把状态改回
running(清掉 pause) - 原来的
runCrawlSteps()仍在“死等恢复”,会自动继续跑下一轮
B) 窗口被关闭导致中断(需要重开窗口)
条件:
state.pause.reason === 'window_closed'或windowId/tabId不存在
行为:
- 根据
currentStepIndex找到“第一个未 success 的步骤”作为startIndex openSingleTabWindow(url)重新打开爬取窗口(dianshan/src/background/task/helper.ts:8)setCrawlTaskState()写入新的windowId/tabId/status='running'/currentStepIndex=startIndex(dianshan/src/background/task/taskState.ts:20)- 新建
AbortController,重新启动执行器:runCrawlSteps(taskId, newTabId, platform.steps, signal, startIndex)(dianshan/src/background/task/crawlTask.ts:63)
6. “爬取过程中窗口被关掉”会发生什么(自动暂停)
触发点:
background/index.ts监听:chrome.windows.onRemoved(dianshan/src/background/index.ts:92)chrome.tabs.onRemoved(兜底)(dianshan/src/background/index.ts:97)
处理逻辑:
- 调用
crawlTask.pauseCrawlOnWindowRemoved(windowId)(dianshan/src/background/task/crawlTask.ts:158)或pauseCrawlOnTabRemoved(tabId)(dianshan/src/background/task/crawlTask.ts:188)
行为:
- 中止执行器
- 找到当前任务对应的
AbortController并abort() - 目的:避免后台继续对不存在的 tab 进行
tabs.update/sendMessage导致刷屏错误
- 状态改为 paused
status='paused'pause.reason='window_closed'pause.message提示用户点击继续可恢复- 清空
windowId/tabId(避免 UI 侧再尝试 focus 老窗口)
用户点击继续后:
- 走第 5.2 的 B 分支:重开窗口 + 从未完成步骤继续
7. “全部步骤完成”会发生什么(发送结果 + 清理 + 关窗)
触发点:
runCrawlSteps()完成 for 循环后:updateCrawlTaskState(...status='completed')- 调用
finalizeCompletedTask(taskId)
finalizeCompletedTask 做三件事(顺序很重要):
- 发送结果给页面
- background -> content(爬取 tab):
sendTabMessage(tabId,'CRAWL_COMPLETED',{ result: ... }) - 同时 storage 完成态会触发
externalBridge对外广播DIANSHAN_CRAWL_DONE
- background -> content(爬取 tab):
- 清空本次任务记录
clearCrawlTaskState():移除chrome.storage.local['crawlTaskState']- popup 会收到 storage 改变并自动把 UI 恢复到“未开始”状态
- 关闭爬取窗口
chrome.windows.remove(windowId)
8. Popup/Overlay 如何拿到状态(同步机制总结)
8.1 Popup:只看 storage(强一致)
- 初次加载:发
GET_CRAWL_STATE - 后续更新:监听
chrome.storage.onChanged,只要crawlTaskState变化就刷新 UI
8.2 Overlay:推送为主,拉取为辅
- 推送:background 写入 state 时会
sendTabMessage(tabId,'CRAWL_STATE_UPDATE',state)- overlay 监听
chrome.runtime.onMessage,收到action==='CRAWL_STATE_UPDATE'立刻渲染
- overlay 监听
- 拉取:overlay 初始化时发
GET_CRAWL_STATE_FOR_TAB- background 只在
sender.tab.id === state.tabId时返回 state,否则返回 null
- background 只在
9. 外部网页(官网/业务系统)如何接收结果(externalBridge)
两种方式:
9.1 onMessageExternal(短消息)
- 网页发
DIANSHAN_START_CRAWL / DIANSHAN_GET_CRAWL_STATE / ... - background
externalBridge.handleExternalMessage()返回当前状态/平台列表等
9.2 onConnectExternal(长连接 Port)
- 网页建立 Port(name=
DIANSHAN_CRAWL) - background 把当前 state 发一次
DIANSHAN_CRAWL_STATE - 后续只要 storage 变化,就广播:
DIANSHAN_CRAWL_STATE(进行中)DIANSHAN_CRAWL_DONE(完成且带 result)DIANSHAN_CRAWL_CLEARED / FAILED / CANCELED(清理/失败/取消)
结果结构:
externalBridge.buildCrawlWebPayload(state)会给出:state:完整状态机result:仅当state.status==='completed'时非空,为按 step.uniqueKey 聚合的结果对象
10. 常见问题排查(建议)
- overlay 不显示 / 不更新
- 先确认该 tab 是否为
crawlTaskState.tabId - overlay 初始化会请求
GET_CRAWL_STATE_FOR_TAB,只有爬取 tab 才返回 state - 确认 background 写入 state 时是否调用了
setCrawlTaskState()(它会推送CRAWL_STATE_UPDATE)
- 点击取消 popup “闪一下”
- 通常是计时器回调解引用了空状态;popup 侧必须在 state 清空时 stop timer
- 窗口被关后任务还在后台跑
- 应当由
windows.onRemoved/tabs.onRemoved触发pauseCrawlOnWindowRemoved/TabRemoved - 检查 background/index.ts 是否注册了监听
11. 约定:消息 action 一览(当前)
popup/content -> background(dianshan/src/shared/message.ts):
START_CRAWLGET_CRAWL_STATEGET_CRAWL_STATE_FOR_TAB(仅爬取 tab 返回 state)CANCEL_CRAWLRESUME_CRAWLDISMISS_CRAWLCANCEL_AUTOCLOSE(兼容旧 overlay 按钮,不建议新逻辑依赖)
background -> content(dianshan/src/shared/tab.ts):
CRAWL_STATE_UPDATE(overlay/pagerunner 可用)CRAWL_COMPLETED(完成时额外发送结果)