18 KiB
店闪扩展执行链路说明
这份文档按当前项目代码整理,目的是方便顺着代码阅读整个爬取流程。
1. 扩展入口关系
扩展入口由 manifest.config.ts 配置:
action.default_popup指向src/popup/index.html,点击浏览器插件图标后打开 popup。background.service_worker指向src/background/index.ts,负责接收消息、创建爬取窗口、执行爬取任务。content_scripts指向src/content/main.ts,会注入到所有http/https页面,用于右下角进度浮窗和页面 DOM 抓取。permissions中的storage、tabs、windows支撑状态保存、页面跳转和窗口管理。
2. 点击 popup 后发生什么
2.1 popup 初始化
文件:src/popup/App.vue
触发方法:
onMountedgetTokenrefreshCrawlStateupdateElapsedSeconds
执行过程:
- 用户点击扩展图标,Chrome 打开
src/popup/index.html。 - Vue 加载
src/popup/App.vue。 onMounted执行:- 调用
getToken()读取登录状态。 - 调用
refreshCrawlState()获取当前爬取任务。 - 启动
setInterval,每秒刷新爬取状态和计时。
- 调用
getToken()来自src/shared/auth.ts:- 优先从
chrome.storage.local读取token。 - 非扩展环境下从
localStorage读取。
- 优先从
2.2 点击“立即爬取”
文件:src/popup/App.vue
触发方法:
handleScansendBackgroundMessage
执行过程:
- 用户点击“立即爬取”按钮。
handleScan()检查当前是否选择了平台。- 通过
sendBackgroundMessage()发送消息给 background:
{
action: 'START_CRAWL',
payload: { platformId: selectedPlatform.value.id },
}
sendBackgroundMessage()实际调用:
chrome.runtime.sendMessage(message)
- popup 收到 background 返回的
CrawlTaskState后,更新本地的crawlState,页面开始显示进度。
3. background 如何接住 popup 消息
3.1 background 消息入口
文件:src/background/index.ts
触发方法:
chrome.runtime.onMessage.addListenerhandleBackgroundMessage
执行过程:
- background service worker 启动后,注册
chrome.runtime.onMessage。 - popup 发来的
START_CRAWL会进入handleBackgroundMessage()。 handleBackgroundMessage()调用:
handleBackgroundCommand(message)
- 如果执行成功,调用
sendResponse(result)把结果回给 popup。 - 如果执行失败,统一返回:
{ ok: false, error: messageText }
3.2 background 指令分发
文件:src/background/service.ts
作用:
export {
handleBackgroundCommand,
handleInstalled,
handleStartup,
handleWindowRemoved,
} from './service/lifecycle';
这个文件现在只是 re-export,真正逻辑在 src/background/service/lifecycle.ts。
文件:src/background/service/lifecycle.ts
触发方法:
handleBackgroundCommand
分发关系:
START_CRAWL->startCrawlGET_CRAWL_STATE->getCrawlTaskStateCANCEL_CRAWL->cancelCrawlRESUME_CRAWL->resumeCrawl
这些消息类型定义在 src/background/types.ts。
4. 创建爬取任务和新窗口
文件:src/background/service/crawlTask.ts
触发方法:
startCrawlcreateCrawlWindowrunCrawlSteps
执行过程:
startCrawl(platformId)先调用getPlatformById(platformId)。getPlatformById来自src/config/platforms.ts,用于找到当前平台配置。- 读取平台配置中的第一个 step:
const firstStep = platform.steps[0];
- 创建初始任务状态
CrawlTaskState:idplatformIdplatformNamestartedAtstatus: 'running'currentStepIndex: 0steps
- 调用
setCrawlTaskState(nextState)写入chrome.storage.local。 - 调用
createCrawlWindow(firstStep.url)打开新的普通浏览器窗口。 - 窗口创建成功后,把
windowId写回任务状态。 - 调用:
void runCrawlSteps(platform, stateWithWindow);
这里使用 void,表示后台任务异步继续跑;startCrawl 会先把初始状态返回给 popup。
5. 爬取状态保存在哪里
文件:src/background/service/taskState.ts
核心方法:
getCrawlTaskStatesetCrawlTaskStateupdateCrawlTaskState
保存位置:
chrome.storage.local
保存 key:
crawlTaskState
当前项目没有把爬取结果单独保存到数据库、文件或独立 result key。每一步的结果直接保存在:
CrawlTaskState.steps[index].result
也就是说,popup 和 content 看到的进度、暂停信息、最终结果,都来自 chrome.storage.local 中的 crawlTaskState。
6. background 如何逐步爬取页面
文件:src/background/service/crawlTask.ts
核心方法:
runCrawlStepsgetWindowActiveTabIdwaitForTabLoadedscrapeStepInContentsendPageRunnerMessagepauseForInterruptwaitUntilResumed
执行过程:
runCrawlSteps(platform, initialState)按platform.steps顺序循环。- 每进入一个 step,会调用
updateCrawlTaskState:- 更新
currentStepIndex。 - 把当前 step 标记为
running。 - 清空当前 step 的旧 message。
- 更新
- 调用
getWindowActiveTabId(windowId)找到爬取窗口里的当前 tab。 - 调用:
chrome.tabs.update(tabId, { url: step.url, active: true })
跳转到当前 step 配置的页面地址。
- 调用
waitForTabLoaded(tabId)等待 Chrome 的 tab 状态变成complete。 - 调用
scrapeStepInContent(tabId, step),让目标页面里的 content script 开始检查页面和抓取 DOM。
7. background 怎么通知目标网页抓取
文件:src/background/service/crawlTask.ts
触发方法:
scrapeStepInContentsendPageRunnerMessage
发送消息:
{
action: 'SCRAPE_STEP',
payload: {
fields: step.fields,
checkSelector: step.checkSelector,
},
}
实际调用:
chrome.tabs.sendMessage(tabId, message)
这里不是 popup 发给 background,而是 background 发给目标网页 tab 里的 content script。
scrapeStepInContent 还有一个容错逻辑:如果 content script 还没注入完成,出现类似 Could not establish connection. Receiving end does not exist. 的错误,会在 20 秒内每 500ms 重试一次。
8. content script 如何接住抓取消息
8.1 content script 挂载
文件:src/content/main.ts
触发方法:
mountAppsetupPageRunner
执行过程:
- 目标页面加载时,Chrome 根据
manifest.config.ts注入src/content/main.ts。 main.ts等待 DOM 可用后执行mountApp()。mountApp()创建#dianshan-crx-root容器。- 挂载
src/content/App.vue,用于显示右下角爬取计时按钮和进度面板。 - 调用
setupPageRunner()注册页面消息监听器。
8.2 页面执行器接收 SCRAPE_STEP
文件:src/content/pageRunner.ts
触发方法:
setupPageRunnerhandlePageRunnerMessagedetectPageInterruptwaitForStableSelectorprocessFields
执行过程:
setupPageRunner()注册:
chrome.runtime.onMessage.addListener(...)
- background 发送
SCRAPE_STEP后,进入handlePageRunnerMessage()。 - 先调用
detectPageInterrupt()判断是否遇到:- 登录页:
reauth - 验证码或风控:
shield - 页面不存在:
not_found
- 登录页:
- 如果检测到中断,返回:
{ ok: false, interrupt }
- 如果没有中断,调用
waitForStableSelector(checkSelector, 18000)。 checkSelector来自src/config/platforms.ts的 step 配置,用于判断页面关键 DOM 是否出现并可见。- 如果 18 秒内关键 DOM 仍未稳定出现,返回
page_not_ready中断。 - 如果页面可用,调用:
processFields(message.payload.fields, document.body)
开始真正的 DOM 字段采集。
9. DOM 数据怎么提取
文件:src/background/domScraper.ts
虽然文件在 background 目录下,但它被 src/content/pageRunner.ts import 后,实际是在目标网页的 content script 环境里执行 DOM 查询。
核心方法:
processFieldsprocessListprocessTableautoClickextractValuewaitForElement
执行逻辑:
processFields(columns, rootDom)遍历当前 step 的fields。- 每个字段先执行
autoClick(item, rootDom):- 如果字段配置了
condition.list,就按顺序点击对应选择器。 - 每次点击后等待
condition.time。
- 如果字段配置了
- 再根据
item.className查询元素。 - 普通字段:
- 没有
keys时,调用extractValue(element, item)。 - 有
keys时,递归调用processFields(item.keys, element)。
- 没有
- 列表字段:
type === 1时进入processList。- 按
config.className找到列表项。 - 对每个列表项递归执行
processFields(config.keys, element)。 - 如果配置了
pagination,会点击下一页继续采集。
- 表格字段:
type === 2时进入processTable。- 按
tableParts找到不同 table 片段。 - 以第一个 part 的行数为准,按行拼接不同 part 的字段。
- 如果配置了
pagination,会点击下一页继续采集。
extractValue的取值规则:- 配置了
attr就取指定属性。 IMG默认取src。A默认取href,相对路径会拼上当前 origin。- 其他元素默认取
textContent。
- 配置了
10. 采集结果怎么回传和保存
数据流向:
processFields返回当前 step 的 DOM 采集结果。src/content/pageRunner.ts返回:
{ ok: true, data }
src/background/service/crawlTask.ts的scrapeStepInContent接到 response。runCrawlSteps调用updateCrawlTaskState:
steps[index].result = response.data
steps[index].status = 'success'
- 最新任务状态写回
chrome.storage.local的crawlTaskState。 - popup 和 content 浮窗每秒发送
GET_CRAWL_STATE,读取到最新结果并展示。
所以当前项目的数据传递是:
目标网页 DOM
-> content/pageRunner.ts
-> background/crawlTask.ts
-> chrome.storage.local:crawlTaskState
-> popup/App.vue 和 content/App.vue 轮询展示
11. popup 进度如何刷新
文件:src/popup/App.vue
触发方法:
refreshCrawlStatesendBackgroundMessageupdateElapsedSeconds
执行过程:
- popup 打开后每秒调用一次
refreshCrawlState()。 refreshCrawlState()发送:
{ action: 'GET_CRAWL_STATE' }
- background 的
handleBackgroundCommand收到后调用getCrawlTaskState()。 - popup 拿到
CrawlTaskState后:- 展示平台名。
- 展示运行时间。
- 展示每个 step 的状态。
- 如果 step 有
result,用JSON.stringify(step.result, null, 2)打印出来。
12. 目标网页右下角按钮如何刷新
文件:src/content/App.vue
触发方法:
onMountedrefreshCrawlStateupdateElapsedSecondshandleResumeCrawl
执行过程:
- content script 挂载后,
src/content/App.vue每秒调用refreshCrawlState()。 - 它同样发送:
{ action: 'GET_CRAWL_STATE' }
- 如果任务状态是
running或paused,显示右下角计时按钮。 - 点击按钮会展开进度面板。
- 如果任务是
paused,面板里显示暂停原因,并提供“我已处理,继续”按钮。 - 点击继续时,调用
handleResumeCrawl(),发送:
{ action: 'RESUME_CRAWL' }
13. 暂停和继续流程
触发原因主要来自 src/content/pageRunner.ts:
isLoginPage()检测到登录页。isShieldPage()检测到验证码或风控。isNotFoundPage()检测到页面不存在。waitForStableSelector()超时,认为页面关键内容没准备好。
流程:
- content 返回:
{ ok: false, interrupt }
- background 的
runCrawlSteps检测到response.interrupt。 - 调用
pauseForInterrupt(taskId, stepIndex, interrupt)。 pauseForInterrupt把状态写成:status: 'paused'pause: interrupt- 当前 step 保持
running - 当前 step 的
message设置为中断提示
- popup 和 content 每秒轮询到 paused 状态后展示提示。
- 用户处理完登录或验证码后,点击继续。
- popup 或 content 发送
RESUME_CRAWL。 - background 调用
resumeCrawl():status改回running- 清空
pause - 当前 step 的
message清空
runCrawlSteps中的waitUntilResumed()发现状态恢复为running,重新执行当前 step。
14. 取消和关闭窗口流程
14.1 用户点击取消
文件:
src/popup/App.vuesrc/background/service/lifecycle.tssrc/background/service/crawlTask.ts
方法:
handleCancelCrawlhandleBackgroundCommandcancelCrawl
流程:
- popup 发送:
{ action: 'CANCEL_CRAWL' }
- background 分发到
cancelCrawl()。 cancelCrawl()把当前任务状态改成canceled。- 当前 step 被标记为
failed,message 为用户已取消。 - 如果存在
windowId,调用chrome.windows.remove(windowId)关闭爬取窗口。
14.2 用户直接关闭爬取窗口
文件:
src/background/index.tssrc/background/service/lifecycle.tssrc/background/service/crawlTask.ts
方法:
chrome.windows.onRemoved.addListenerhandleWindowRemovedcancelCrawlWhenWindowRemoved
流程:
- Chrome 触发
windows.onRemoved。 handleWindowRemoved(windowId)被调用。cancelCrawlWhenWindowRemoved(windowId)检查关闭的是否是当前爬取窗口。- 如果匹配且任务还在 running,就把任务改成
canceled。 - 当前 step 标记为
failed,message 为爬取窗口已关闭。
15. 平台配置如何驱动爬取
文件:src/config/platforms.ts
核心导出:
PLATFORM_CONFIGSgetPlatformById
平台配置结构来自 src/types/platform.ts:
PlatformConfigPlatformStepConfigPlatformFieldConfigPlatformPaginationConfigPlatformTablePartConfigPlatformClickCondition
关键字段:
steps:平台要按顺序爬取的页面。step.url:当前步骤要打开的页面地址。step.checkSelector:判断页面是否可抓取的关键元素。step.fields:当前页面要采集的字段。field.className:字段选择器。field.keys:子字段,支持递归。field.type:字段类型,默认普通字段,1是列表,2是表格。field.condition:采集字段前要自动点击的元素。field.pagination:列表或表格翻页配置。field.tableParts:表格分段配置,用来拼接多个 table 片段。
16. 类型结构在哪里看
主要类型文件:
src/background/types.ts:background 接收的消息类型和统一响应类型。src/types/crawl.ts:爬取任务状态、步骤状态、暂停原因。src/types/platform.ts:平台配置、字段配置、分页配置、表格配置。src/types/index.ts:统一导出类型。
最关键的运行时状态是 CrawlTaskState:
interface CrawlTaskState {
id: string;
platformId: string;
platformName: string;
windowId?: number;
startedAt: number;
status: 'running' | 'paused' | 'completed' | 'failed' | 'canceled';
pause?: CrawlPauseInfo;
currentStepIndex: number;
steps: CrawlProgressStep[];
}
每个 step 的结果保存在:
interface CrawlProgressStep {
name: string;
uniqueKey: string;
status: 'pending' | 'running' | 'success' | 'failed';
message?: string;
result?: unknown;
}
17. 整体数据流图
用户点击插件图标
-> src/popup/App.vue:onMounted
-> 读取 token 和 crawlTaskState
用户点击立即爬取
-> src/popup/App.vue:handleScan
-> chrome.runtime.sendMessage({ action: 'START_CRAWL' })
background 接收消息
-> src/background/index.ts:handleBackgroundMessage
-> src/background/service/lifecycle.ts:handleBackgroundCommand
-> src/background/service/crawlTask.ts:startCrawl
创建任务和窗口
-> src/background/service/taskState.ts:setCrawlTaskState
-> src/background/service/crawlTask.ts:createCrawlWindow
-> src/background/service/crawlTask.ts:runCrawlSteps
逐个页面跳转
-> chrome.tabs.update(tabId, { url: step.url })
-> src/background/service/crawlTask.ts:waitForTabLoaded
-> src/background/service/crawlTask.ts:scrapeStepInContent
目标页面执行抓取
-> chrome.tabs.sendMessage({ action: 'SCRAPE_STEP' })
-> src/content/pageRunner.ts:handlePageRunnerMessage
-> src/content/pageRunner.ts:waitForStableSelector
-> src/background/domScraper.ts:processFields
结果回到 background
-> src/background/service/crawlTask.ts:runCrawlSteps
-> src/background/service/taskState.ts:updateCrawlTaskState
-> 写入 chrome.storage.local:crawlTaskState
多端展示
-> src/popup/App.vue 每秒 GET_CRAWL_STATE
-> src/content/App.vue 每秒 GET_CRAWL_STATE
-> popup 展示完整进度和结果
-> 目标网页右下角展示计时按钮和进度面板
18. 推荐阅读代码顺序
如果要按最顺的方式读代码,可以这样看:
manifest.config.ts:先看扩展入口。src/popup/App.vue:看用户点按钮时发什么消息。src/background/index.ts:看 background 怎么接消息。src/background/service/lifecycle.ts:看消息怎么分发。src/background/service/crawlTask.ts:看任务创建、窗口打开、页面跳转、暂停恢复。src/background/service/taskState.ts:看状态怎么保存。src/content/main.ts:看 content script 怎么挂载。src/content/pageRunner.ts:看目标页面怎么接收 background 抓取指令。src/background/domScraper.ts:看 DOM 字段怎么递归提取。src/config/platforms.ts:结合抓取逻辑看配置如何控制页面和字段。src/types/crawl.ts、src/types/platform.ts:最后看数据结构。