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