一句话总结:CDP 的 BackgroundService 域提供了对浏览器后台服务的完整调试能力——你可以捕获 Background Sync 和 Background Fetch 事件、记录它们的参数详情、模拟离线场景,并获得 Service Worker 后台操作的完整可见性。


目录

  1. BackgroundService 域概览
  2. 启用后台服务监控
  3. Background Sync 事件捕获
  4. Background Fetch 事件捕获
  5. 后台事件参数详解
  6. 录制与回放后台事件
  7. 实战:Service Worker 离线调试
  8. 最佳实践与注意事项

BackgroundService 域概览

BackgroundService 是 CDP 中专门用于调试浏览器后台服务的域。它提供了对以下后台服务的透明访问:

服务名称 说明 典型场景
backgroundSync 后台同步 离线时队列化请求,在线时自动重试
backgroundFetch 后台获取 大文件/资源在后台下载进度管理
periodicBackgroundSync 定期后台同步 定期更新缓存内容
pushMessaging 推送消息 接收服务器推送通知
notifications 通知 显示和交互通知
paymentHandler 支付处理 Web Payment API 调用的调试

通过 BackgroundService 域,你可以:

  1. 监听事件——捕获后台服务触发的每个事件
  2. 记录详情——获取事件的所有参数和时间戳
  3. 录制/回放——记录后台事件序列以便重放分析
  4. 清除记录——在需要时清除已记录的事件

启用后台服务监控

基础连接与域启用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import asyncio
import websockets
import json
from datetime import datetime

CDP_URL = "ws://127.0.0.1:9222/devtools/browser/..."
CMD_ID = [0]

async def cdp(ws, method, params=None, session_id=None):
CMD_ID[0] += 1
msg = {"id": CMD_ID[0], "method": method, "params": params or {}}
if session_id:
msg["sessionId"] = session_id
await ws.send(json.dumps(msg))
async for resp in ws:
data = json.loads(resp)
if data.get("id") == CMD_ID[0]:
return data.get("result", {})


async def connect_and_enable_background_services(ws):
"""连接到页面并启用后台服务监控"""
targets = await cdp(ws, "Target.getTargets")
target_id = targets["targetInfos"][0]["targetId"]
result = await cdp(ws, "Target.attachToTarget", {
"targetId": target_id, "flatten": True
})
session_id = result["sessionId"]

# 启用 BackgroundService 域
await cdp(ws, "BackgroundService.startObserving", {
"service": "backgroundSync"
}, session_id)

await cdp(ws, "BackgroundService.startObserving", {
"service": "backgroundFetch"
}, session_id)

print(f"[后台服务] 监控已启动,会话: {session_id[:8]}...")
return session_id

监控所有后台服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class BackgroundServiceMonitor:
"""后台服务监控器——统一管理多个后台服务的监听"""

SERVICE_NAMES = [
"backgroundSync",
"backgroundFetch",
"periodicBackgroundSync",
"pushMessaging",
"notifications",
"paymentHandler",
]

def __init__(self, ws, session_id):
self.ws = ws
self.session_id = session_id
self.events = {name: [] for name in self.SERVICE_NAMES}
self._callbacks = {}

async def enable_all(self):
"""启用所有后台服务的监控"""
for service in self.SERVICE_NAMES:
try:
await cdp(self.ws, "BackgroundService.startObserving", {
"service": service
}, self.session_id)
print(f" [启用] {service}")
except Exception as e:
print(f" [失败] {service}: {e}")
await asyncio.sleep(0.1)
print(f"[后台服务] 已启用 {len(self.SERVICE_NAMES)} 个后台服务监控")

async def enable(self, service_name):
"""启用指定后台服务的监控"""
if service_name not in self.SERVICE_NAMES:
raise ValueError(f"未知后台服务: {service_name}")

await cdp(self.ws, "BackgroundService.startObserving", {
"service": service_name
}, self.session_id)
print(f"[启用] {service_name}")

async def disable(self, service_name):
"""停止监控指定后台服务"""
await cdp(self.ws, "BackgroundService.stopObserving", {
"service": service_name
}, self.session_id)
print(f"[停止] {service_name}")

async def disable_all(self):
"""停止所有后台服务的监控"""
for service in self.SERVICE_NAMES:
await self.disable(service)

def on_event(self, service_name=None):
"""注册事件回调"""
def decorator(handler):
if service_name:
if service_name not in self._callbacks:
self._callbacks[service_name] = []
self._callbacks[service_name].append(handler)
else:
for name in self.SERVICE_NAMES:
if name not in self._callbacks:
self._callbacks[name] = []
self._callbacks[name].append(handler)
return handler
return decorator

async def listen(self, duration=60):
"""监听后台服务事件"""
start = asyncio.get_event_loop().time()
event_count = 0

while (asyncio.get_event_loop().time() - start) < duration:
try:
msg = await asyncio.wait_for(self.ws.__anext__(), timeout=1)
data = json.loads(msg)
method = data.get("method", "")
params = data.get("params", {})

if method == "BackgroundService.backgroundServiceEventReceived":
event = params.get("backgroundServiceEvent", {})
service = event.get("serviceName", "")

if service in self.events:
self.events[service].append(event)
event_count += 1

# 触发回调
if service in self._callbacks:
for callback in self._callbacks[service]:
await callback(event)

# 全服务回调
if None in self._callbacks:
for callback in self._callbacks[None]:
await callback(event)

except asyncio.TimeoutError:
continue

return event_count

def get_events(self, service_name=None, clear=False):
"""获取记录的事件"""
if service_name:
events = self.events.get(service_name, [])
if clear:
self.events[service_name] = []
return events

result = {}
for name in self.SERVICE_NAMES:
result[name] = list(self.events[name])
if clear:
self.events[name] = []
return result

async def clear_events(self, service_name=None):
"""清除后台服务事件记录"""
if service_name:
await cdp(self.ws, "BackgroundService.clearEvents", {
"service": service_name
}, self.session_id)
self.events[service_name] = []
else:
for service in self.SERVICE_NAMES:
await cdp(self.ws, "BackgroundService.clearEvents", {
"service": service
}, self.session_id)
self.events[service] = []

Background Sync 事件捕获

理解 Background Sync

Background Sync 允许 Service Worker 在用户离线时延迟操作,待网络恢复后再执行。这对提升 PWA 的离线体验至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class BackgroundSyncTracker:
"""Background Sync 事件追踪器"""

def __init__(self, ws, session_id):
self.ws = ws
self.session_id = session_id
self.sync_events = []

async def enable(self):
"""启用 Background Sync 监控"""
await cdp(self.ws, "BackgroundService.startObserving", {
"service": "backgroundSync"
}, self.session_id)
print("[Background Sync] 监控已启用")

async def track_sync_events(self, duration=60):
"""追踪 Background Sync 事件"""
start = asyncio.get_event_loop().time()

while (asyncio.get_event_loop().time() - start) < duration:
try:
msg = await asyncio.wait_for(self.ws.__anext__(), timeout=1)
data = json.loads(msg)

if data.get("method") == "BackgroundService.backgroundServiceEventReceived":
event = data["params"].get("backgroundServiceEvent", {})

if event.get("serviceName") == "backgroundSync":
self._process_sync_event(event)

except asyncio.TimeoutError:
continue

return self._generate_report()

def _process_sync_event(self, event):
"""处理单个同步事件"""
event_name = event.get("eventName", "")
instance_id = event.get("instanceId", "")
timestamp = event.get("timestamp", 0)
origin = event.get("origin", "")

# 解析事件元数据
metadata = {}
for entry in event.get("eventMetadata", []):
metadata[entry.get("key", "")] = entry.get("value", "")

sync_event = {
"name": event_name,
"instance_id": instance_id,
"timestamp": datetime.fromtimestamp(timestamp / 1000).isoformat(),
"origin": origin,
"metadata": metadata,
}

self.sync_events.append(sync_event)

# 打印事件摘要
tag = metadata.get("tag", "(unknown)")
print(f"[Sync] {event_name} | tag: {tag} | origin: {origin}")

if event_name == "SyncRegistered":
print(f" 注册同步: {tag}")
elif event_name == "SyncFired":
print(f" 同步触发: {tag}")
elif event_name == "SyncCompleted":
outcome = metadata.get("outcome", "unknown")
print(f" 同步完成: {tag} | 结果: {outcome}")
elif event_name == "SyncFailed":
reason = metadata.get("failureReason", "unknown")
print(f" 同步失败: {tag} | 原因: {reason}")

def _generate_report(self):
"""生成追踪报告"""
by_name = {}
for event in self.sync_events:
name = event["name"]
if name not in by_name:
by_name[name] = {"count": 0, "instances": set()}
by_name[name]["count"] += 1
by_name[name]["instances"].add(event["instance_id"])

return {
"total_events": len(self.sync_events),
"by_event_name": {k: {"count": v["count"], "unique_instances": len(v["instances"])}
for k, v in by_name.items()},
"events": self.sync_events,
"sync_tags": list(set(
e["metadata"].get("tag", "")
for e in self.sync_events
if "tag" in e.get("metadata", {})
))
}

模拟 Background Sync 场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
async def simulate_and_capture_background_sync(ws, session_id):
"""模拟 Background Sync 并捕获事件"""
# 注册 Service Worker
sw_js = """
// 注册 sync 事件的 Service Worker
if ('serviceWorker' in navigator && 'SyncManager' in window) {
try {
const registration = await navigator.serviceWorker.ready;

// 注册一个后台同步
await registration.sync.register('sync-posts');
console.log('[Sync] 已注册: sync-posts');

await registration.sync.register('sync-messages');
console.log('[Sync] 已注册: sync-messages');

// 注册带标签的同步
await registration.sync.register('sync-analytics-v1');
console.log('[Sync] 已注册: sync-analytics-v1');

return 'Registered 3 sync events';
} catch (err) {
return 'Sync registration failed: ' + err.message;
}
}
return 'SyncManager not available';
"""

result = await cdp(ws, "Runtime.evaluate", {
"expression": f"({sw_js})()",
"awaitPromise": True,
"returnByValue": True
}, session_id)

print(f"[模拟] {result.get('result', {}).get('value', '')}")

# 等待事件传播
await asyncio.sleep(2)

# 在 Service Worker 中触发 sync 事件的处理
trigger_sync_handler = """
navigator.serviceWorker.ready.then(reg => {
// 模拟触发 sync 事件(实际中由浏览器网络恢复时触发)
console.log('[Sync] Service Worker ready, waiting for sync events...');
});
"""

await cdp(ws, "Runtime.evaluate", {
"expression": trigger_sync_handler,
"returnByValue": True
}, session_id)

Background Fetch 事件捕获

理解 Background Fetch

Background Fetch 允许在用户关闭页面后继续下载大文件(如视频、PDF 等)。它通过 Service Worker 管理下载进度,并且用户可以随时暂停和恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class BackgroundFetchTracker:
"""Background Fetch 事件追踪器"""

def __init__(self, ws, session_id):
self.ws = ws
self.session_id = session_id
self.fetch_events = []

async def enable(self):
"""启用 Background Fetch 监控"""
await cdp(self.ws, "BackgroundService.startObserving", {
"service": "backgroundFetch"
}, self.session_id)
print("[Background Fetch] 监控已启用")

async def track_fetch_events(self, duration=60):
"""追踪 Background Fetch 事件"""
start = asyncio.get_event_loop().time()

while (asyncio.get_event_loop().time() - start) < duration:
try:
msg = await asyncio.wait_for(self.ws.__anext__(), timeout=1)
data = json.loads(msg)

if data.get("method") == "BackgroundService.backgroundServiceEventReceived":
event = data["params"].get("backgroundServiceEvent", {})

if event.get("serviceName") == "backgroundFetch":
self._process_fetch_event(event)

except asyncio.TimeoutError:
continue

return self._generate_fetch_report()

def _process_fetch_event(self, event):
"""处理单个 Fetch 事件"""
event_name = event.get("eventName", "")
instance_id = event.get("instanceId", "")
timestamp = event.get("timestamp", 0)
origin = event.get("origin", "")

metadata = {}
for entry in event.get("eventMetadata", []):
metadata[entry.get("key", "")] = entry.get("value", "")

fetch_event = {
"name": event_name,
"instance_id": instance_id,
"timestamp": datetime.fromtimestamp(timestamp / 1000).isoformat(),
"origin": origin,
"metadata": metadata,
}

self.fetch_events.append(fetch_event)

# 打印事件摘要
tag = metadata.get("tag", metadata.get("uniqueId", "(unknown)"))

if event_name == "FetchRegistered":
url = metadata.get("url", "")
total = metadata.get("totalDownloadSize", "0")
print(f"[Fetch] 注册 | tag: {tag} | URL: {url[:80]}")
print(f" 总大小: {total} 字节")

elif event_name == "FetchFired":
print(f"[Fetch] 触发 | tag: {tag}")

elif event_name == "FetchProgress":
downloaded = metadata.get("downloadedSize", "0")
total = metadata.get("totalDownloadSize", "0")
pct = metadata.get("percentComplete", "0")
print(f"[Fetch] 进度 | tag: {tag} | {downloaded}/{total} ({pct}%)")

elif event_name == "FetchCompleted":
print(f"[Fetch] 完成 | tag: {tag}")

elif event_name == "FetchFailed":
reason = metadata.get("failureReason", "unknown")
print(f"[Fetch] 失败 | tag: {tag} | 原因: {reason}")

elif event_name == "FetchAborted":
print(f"[Fetch] 中止 | tag: {tag}")

elif event_name == "FetchPaused":
print(f"[Fetch] 暂停 | tag: {tag}")

elif event_name == "FetchResumed":
print(f"[Fetch] 恢复 | tag: {tag}")

def _generate_fetch_report(self):
"""生成 Fetch 追踪报告"""
by_name = {}
total_downloaded = 0

for event in self.fetch_events:
name = event["name"]
if name not in by_name:
by_name[name] = 0
by_name[name] += 1

if "downloadedSize" in event.get("metadata", {}):
try:
total_downloaded += int(event["metadata"]["downloadedSize"])
except ValueError:
pass

instances = set(e["instance_id"] for e in self.fetch_events)

return {
"total_events": len(self.fetch_events),
"unique_instances": len(instances),
"by_event_name": by_name,
"events": self.fetch_events,
"total_downloaded_bytes": total_downloaded,
}

模拟 Background Fetch 场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
async def simulate_and_capture_background_fetch(ws, session_id):
"""模拟 Background Fetch 并捕获事件"""
fetch_js = """
(async () => {
if (!('serviceWorker' in navigator) || !('BackgroundFetchManager' in window)) {
return 'BackgroundFetchManager not available';
}

try {
const registration = await navigator.serviceWorker.ready;

// 注册一个后台下载
const fetch = await registration.backgroundFetch.fetch(
'download-manual-v2',
['/api/data.json', '/api/image.png'],
{
title: 'Manual Download',
icons: [],
downloadTotal: 1000000
}
);

console.log('[Fetch] Background fetch registered:', fetch);
return 'Background fetch registered: download-manual-v2';
} catch (err) {
return 'Background fetch failed: ' + err.message;
}
})();
"""

result = await cdp(ws, "Runtime.evaluate", {
"expression": fetch_js,
"awaitPromise": True,
"returnByValue": True
}, session_id)

print(f"[模拟] {result.get('result', {}).get('value', '')}")
await asyncio.sleep(2)

后台事件参数详解

BackgroundServiceEvent 结构解析

理解 BackgroundServiceEvent 的完整结构是准确解析后台事件的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class BackgroundServiceEventParser:
"""BackgroundServiceEvent 解析器——提取所有事件字段"""

@staticmethod
def parse(event):
"""解析完整的后台服务事件"""
if not event:
return {}

return {
"service_name": event.get("serviceName", ""),
"event_name": event.get("eventName", ""),
"instance_id": event.get("instanceId", ""),
"origin": event.get("origin", ""),
"timestamp": BackgroundServiceEventParser.parse_timestamp(
event.get("timestamp", 0)
),
"event_metadata": BackgroundServiceEventParser.parse_metadata(
event.get("eventMetadata", [])
),
"from_log": event.get("fromLog", False),
}

@staticmethod
def parse_timestamp(timestamp_ms):
"""解析时间戳(毫秒 -> ISO 格式)"""
if not timestamp_ms:
return {"raw": 0, "iso": "unknown"}

try:
dt = datetime.fromtimestamp(timestamp_ms / 1000)
return {
"raw": timestamp_ms,
"iso": dt.isoformat(),
"readable": dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
}
except (OSError, ValueError, OverflowError):
return {"raw": timestamp_ms, "iso": "invalid_timestamp"}

@staticmethod
def parse_metadata(metadata_list):
"""解析事件元数据列表"""
parsed = {}
for entry in metadata_list:
key = entry.get("key", "")
value = entry.get("value", "")
parsed[key] = BackgroundServiceEventParser.infer_value(value)
return parsed

@staticmethod
def infer_value(raw_value):
"""推断元数据的值类型"""
# 尝试数字
try:
if "." in raw_value:
return {"raw": raw_value, "parsed": float(raw_value), "type": "number"}
return {"raw": raw_value, "parsed": int(raw_value), "type": "integer"}
except (ValueError, TypeError):
pass

# 尝试布尔
if raw_value.lower() in ("true", "false"):
return {"raw": raw_value, "parsed": raw_value.lower() == "true", "type": "boolean"}

# 尝试 JSON
if raw_value.startswith(("{", "[")):
try:
return {"raw": raw_value, "parsed": json.loads(raw_value), "type": "json"}
except json.JSONDecodeError:
pass

return {"raw": raw_value, "parsed": raw_value, "type": "string"}

@staticmethod
def format_event_readable(parsed_event):
"""格式化为可读的事件详情"""
lines = []
lines.append("=" * 60)
lines.append(f"后台服务事件 | {parsed_event['service_name']} | {parsed_event['event_name']}")
lines.append("=" * 60)
lines.append(f" 实例 ID: {parsed_event['instance_id']}")
lines.append(f" 来源: {parsed_event['origin']}")
lines.append(f" 时间: {parsed_event['timestamp']['readable']}")
lines.append(f" 来自日志: {parsed_event['from_log']}")

if parsed_event['event_metadata']:
lines.append("")
lines.append(" 元数据:")
for key, value_info in parsed_event['event_metadata'].items():
vtype = value_info['type']
vparsed = value_info['parsed']
if vtype in ("integer", "number"):
lines.append(f" {key}: {vparsed} ({vtype})")
elif vtype == "boolean":
lines.append(f" {key}: {vparsed}")
elif vtype == "json":
lines.append(f" {key}: {json.dumps(vparsed, indent=2, ensure_ascii=False)}")
else:
lines.append(f" {key}: {value_info['raw']}")

lines.append("=" * 60)
return "\n".join(lines)

特定服务的事件名称参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class BackgroundServiceEventsReference:
"""后台服务事件名称参考"""

SYNC_EVENTS = {
"SyncRegistered": "后台同步已注册",
"SyncFired": "同步事件已触发",
"SyncCompleted": "同步成功完成",
"SyncFailed": "同步执行失败",
}

FETCH_EVENTS = {
"FetchRegistered": "后台获取已注册",
"FetchFired": "获取事件已触发",
"FetchProgress": "下载进度更新",
"FetchCompleted": "下载成功完成",
"FetchFailed": "下载失败",
"FetchAborted": "下载被中止",
"FetchPaused": "下载已暂停",
"FetchResumed": "下载已恢复",
}

PERIODIC_SYNC_EVENTS = {
"PeriodicSyncRegistered": "定期同步已注册",
"PeriodicSyncFired": "定期同步已触发",
"PeriodicSyncCompleted": "定期同步完成",
"PeriodicSyncFailed": "定期同步失败",
}

PUSH_EVENTS = {
"PushMessageQueued": "推送消息已排队",
"PushMessageSent": "推送消息已发送",
"PushMessageReceived": "推送消息已接收",
"PushNotificationShown": "推送通知已显示",
"PushNotificationClicked": "推送通知已点击",
"PushNotificationDismissed": "推送通知已关闭",
}

@classmethod
def get_event_description(cls, service_name, event_name):
"""获取事件的中文描述"""
mapping = {
"backgroundSync": cls.SYNC_EVENTS,
"backgroundFetch": cls.FETCH_EVENTS,
"periodicBackgroundSync": cls.PERIODIC_SYNC_EVENTS,
"pushMessaging": cls.PUSH_EVENTS,
}
service_events = mapping.get(service_name, {})
return service_events.get(event_name, event_name)

录制与回放后台事件

事件录制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import json
import os
from datetime import datetime

class BackgroundEventRecorder:
"""后台服务事件录制器——记录事件序列以便回放分析"""

def __init__(self, ws, session_id):
self.ws = ws
self.session_id = session_id
self.recording = False
self.recorded_events = []
self.current_session_id = None

async def start_recording(self, service_names=None):
"""开始录制后台事件"""
if service_names is None:
service_names = BackgroundServiceMonitor.SERVICE_NAMES

for service in service_names:
await cdp(self.ws, "BackgroundService.startObserving", {
"service": service
}, self.session_id)

self.recording = True
self.recorded_events = []
self.current_session_id = datetime.now().strftime("%Y%m%d_%H%M%S")

print(f"[录制] 开始录制后台事件 (会话: {self.current_session_id})")
return self.current_session_id

async def record(self, duration=60):
"""录制指定时长的事件"""
if not self.recording:
raise RuntimeError("请先调用 start_recording()")

start = asyncio.get_event_loop().time()

while (asyncio.get_event_loop().time() - start) < duration:
try:
msg = await asyncio.wait_for(self.ws.__anext__(), timeout=1)
data = json.loads(msg)

if data.get("method") == "BackgroundService.backgroundServiceEventReceived":
event = data["params"].get("backgroundServiceEvent", {})

recorded = {
"capture_time": datetime.now().isoformat(),
"event": BackgroundServiceEventParser.parse(event)
}

self.recorded_events.append(recorded)
service = event.get("serviceName", "?")
ename = event.get("eventName", "?")
print(f"[录制] {service}.{ename} #{len(self.recorded_events)}")

except asyncio.TimeoutError:
continue

print(f"[录制] 录制完成,共 {len(self.recorded_events)} 个事件")
return self.recorded_events

def stop_recording(self):
"""停止录制"""
self.recording = False
print(f"[录制] 已停止")

def save_recording(self, filepath):
"""保存录制结果到文件"""
recording_data = {
"session_id": self.current_session_id,
"recorded_at": datetime.now().isoformat(),
"total_events": len(self.recorded_events),
"events": self.recorded_events,
}

with open(filepath, "w", encoding="utf-8") as f:
json.dump(recording_data, f, indent=2, ensure_ascii=False)

print(f"[录制] 已保存到 {filepath} ({len(self.recorded_events)} 个事件)")
return filepath

@staticmethod
def load_recording(filepath):
"""加载录制文件"""
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)

print(f"[回放] 加载录制: {data['session_id']} ({data['total_events']} 个事件)")
return data

事件回放分析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class BackgroundEventReplayAnalyzer:
"""后台事件回放分析器——分析录制的事件序列"""

def __init__(self, recording_data):
self.events = recording_data.get("events", [])
self.session_id = recording_data.get("session_id", "")

def analyze_sequence(self):
"""分析事件序列"""
by_service = {}
by_event = {}
timeline = []

for recorded in self.events:
event = recorded["event"]
service = event["service_name"]
event_name = event["event_name"]

if service not in by_service:
by_service[service] = 0
by_service[service] += 1

key = f"{service}.{event_name}"
if key not in by_event:
by_event[key] = 0
by_event[key] += 1

timeline.append({
"time": recorded["capture_time"],
"service": service,
"event": event_name,
"instance": event["instance_id"][:12],
"summary": BackgroundServiceEventParser.format_event_readable(event).split("\n")[1]
})

return {
"session_id": self.session_id,
"total_events": len(self.events),
"by_service": by_service,
"by_event": by_event,
"timeline": timeline
}

def find_instance_lifecycle(self, instance_id):
"""查找特定实例的完整生命周期"""
lifecycle = []

for recorded in self.events:
event = recorded["event"]
if event["instance_id"] == instance_id or event["instance_id"].startswith(instance_id):
lifecycle.append({
"time": recorded["capture_time"],
"event": event["event_name"],
"metadata": event["event_metadata"]
})

return lifecycle

def generate_report(self):
"""生成分析报告"""
analysis = self.analyze_sequence()

report_lines = []
report_lines.append("=" * 60)
report_lines.append("后台事件回放分析报告")
report_lines.append("=" * 60)
report_lines.append(f"会话: {analysis['session_id']}")
report_lines.append(f"事件总数: {analysis['total_events']}")
report_lines.append("")

report_lines.append("按服务分布:")
for service, count in sorted(analysis["by_service"].items(), key=lambda x: -x[1]):
report_lines.append(f" {service}: {count}")

report_lines.append("")
report_lines.append("按事件分布:")
for key, count in sorted(analysis["by_event"].items(), key=lambda x: -x[1]):
report_lines.append(f" {key}: {count}")

report_lines.append("")
report_lines.append("事件时间线:")
for entry in analysis["timeline"]:
report_lines.append(f" [{entry['time']}] {entry['service']}.{entry['event']} ({entry['instance']})")

report_lines.append("=" * 60)
return "\n".join(report_lines)

实战:Service Worker 离线调试

完整调试工作流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class ServiceWorkerOfflineDebugger:
"""Service Worker 离线调试器——结合 Network 和 BackgroundService 域"""

def __init__(self, ws):
self.ws = ws
self.session_id = None
self.bg_monitor = None

async def initialize(self):
"""初始化调试环境"""
targets = await cdp(self.ws, "Target.getTargets")

# 找到 Service Worker 目标
sw_target = None
page_target = None

for target in targets.get("targetInfos", []):
if target["type"] == "service_worker":
sw_target = target
elif target["type"] == "page" and not page_target:
page_target = target

# 附加到页面目标
if page_target:
result = await cdp(self.ws, "Target.attachToTarget", {
"targetId": page_target["targetId"],
"flatten": True
})
self.session_id = result["sessionId"]

# 附加到 Service Worker
if sw_target:
sw_result = await cdp(self.ws, "Target.attachToTarget", {
"targetId": sw_target["targetId"],
"flatten": True
})
self.sw_session_id = sw_result["sessionId"]
print(f"[调试] 已附加到 Service Worker: {sw_target.get('url', '')[:60]}")

# 启用必要域
await cdp(self.ws, "Page.enable", {}, self.session_id)
await cdp(self.ws, "Runtime.enable", {}, self.session_id)
await cdp(self.ws, "Network.enable", {}, self.session_id)

# 初始化后台服务监控
self.bg_monitor = BackgroundServiceMonitor(self.ws, self.session_id)
await self.bg_monitor.enable_all()

return self.session_id

async def simulate_offline_scenario(self):
"""模拟离线场景并观察后台行为"""
print("\n" + "=" * 60)
print("模拟离线场景")
print("=" * 60)

# 1. 先注册一个后台同步
print("\n[步骤 1] 注册后台同步...")
register_sync = """
(async () => {
if ('serviceWorker' in navigator) {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('offline-data-sync');
return 'Sync registered: offline-data-sync';
}
return 'Service Worker not available';
})();
"""

result = await cdp(self.ws, "Runtime.evaluate", {
"expression": register_sync,
"awaitPromise": True,
"returnByValue": True
}, self.session_id)
print(f" {result.get('result', {}).get('value', '')}")
await asyncio.sleep(1)

# 2. 设置离线
print("\n[步骤 2] 模拟离线...")
await cdp(self.ws, "Network.emulateNetworkConditions", {
"offline": True,
"latency": 0,
"downloadThroughput": 0,
"uploadThroughput": 0
}, self.session_id)
print(" 网络状态: 离线")
await asyncio.sleep(1)

# 3. 发起网络请求(会在离线时排队)
print("\n[步骤 3] 在离线状态下发起请求...")
make_request = """
fetch('/api/data.json')
.then(r => r.json())
.then(d => console.log('[Online] Data received:', d))
.catch(e => console.log('[Offline] Fetch failed (expected):', e.message));
"""

await cdp(self.ws, "Runtime.evaluate", {
"expression": make_request,
"returnByValue": True
}, self.session_id)
await asyncio.sleep(2)

# 4. 恢复在线
print("\n[步骤 4] 恢复在线...")
await cdp(self.ws, "Network.emulateNetworkConditions", {
"offline": False,
"latency": 0,
"downloadThroughput": -1,
"uploadThroughput": -1
}, self.session_id)
print(" 网络状态: 在线")

# 等待同步触发
print("\n等待后台同步事件...")
await asyncio.sleep(5)

# 5. 查看后台事件
print("\n[步骤 5] 收集后台事件...")
events = self.bg_monitor.get_events("backgroundSync")
print(f" 捕获到 {len(events)} 个 Background Sync 事件:")
for event in events:
ename = event.get("eventName", "?")
meta = {
e.get("key", ""): e.get("value", "")
for e in event.get("eventMetadata", [])
}
print(f" - {ename}: {meta}")

return events

async def cleanup(self):
"""清理调试环境"""
if self.bg_monitor:
await self.bg_monitor.disable_all()

await cdp(self.ws, "Network.emulateNetworkConditions", {
"offline": False,
"latency": 0,
"downloadThroughput": -1,
"uploadThroughput": -1
}, self.session_id)

print("[调试] 环境已清理")

完整使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
async def background_service_debugging_workflow():
"""完整的 BackgroundService 调试工作流"""
async with websockets.connect(CDP_URL) as ws:
# 1. 初始化离线调试器
debugger = ServiceWorkerOfflineDebugger(ws)
await debugger.initialize()

# 2. 模拟离线场景
events = await debugger.simulate_offline_scenario()

# 3. 录制后台事件
recorder = BackgroundEventRecorder(ws, debugger.session_id)
await recorder.start_recording()

print("\n录制后台事件 (10 秒)...")
recorded = await recorder.record(10)

# 4. 保存录制
recording_file = f"bg_recording_{recorder.current_session_id}.json"
recorder.save_recording(recording_file)

# 5. 回放分析
recording_data = BackgroundEventRecorder.load_recording(recording_file)
analyzer = BackgroundEventReplayAnalyzer(recording_data)
report = analyzer.generate_report()
print("\n" + report)

# 6. 清理
await debugger.cleanup()

return {
"events_captured": len(events),
"recorded_events": len(recorded),
"recording_file": recording_file
}


# asyncio.run(background_service_debugging_workflow())

最佳实践与注意事项

后台服务调试技巧

场景 建议 说明
首次启用 逐个启用服务 一次性启用所有服务可能产生大量事件
长时间监控 使用事件缓冲和分页存储 避免内存溢出
离线测试 结合 Network.emulateNetworkConditions 模拟真实网络变化
事件分析 按 instanceId 分组 追踪单个任务的完整生命周期
生产调试 使用录制/回放模式 先记录再离线分析

常见问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class BackgroundServiceTroubleshooting:
"""后台服务调试常见问题"""

@staticmethod
def sw_not_registered():
"""Service Worker 未注册"""
# 检查 Service Worker 是否已注册
# 使用 Runtime.evaluate 检查 navigator.serviceWorker.controller
pass

@staticmethod
def sync_manager_unavailable():
"""SyncManager 不可用"""
# 检查:
# 1. 页面是否通过 HTTPS 加载
# 2. Service Worker 是否正确安装
# 3. 浏览器是否支持 SyncManager
pass

@staticmethod
def events_not_received():
"""事件未收到"""
# 检查:
# 1. BackgroundService.startObserving 是否正确调用
# 2. Session ID 是否正确
# 3. 后台服务是否需要用户手势触发
pass

@staticmethod
def cross_origin_limitations():
"""跨域限制"""
# BackgroundService 事件仅来自页面源
# 跨域 iframe 的后台事件不会被捕获
pass

关键参数参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Background Sync 事件元数据字段
SYNC_METADATA_FIELDS = {
"tag": "同步标签",
"outcome": "同步结果 (Success/Failure)",
"failureReason": "失败原因",
"lastAttemptTime": "最后尝试时间",
"numberOfAttempts": "尝试次数",
"maxAttempts": "最大尝试次数",
}

# Background Fetch 事件元数据字段
FETCH_METADATA_FIELDS = {
"tag": "下载标签",
"uniqueId": "唯一标识",
"title": "下载标题",
"url": "下载 URL",
"totalDownloadSize": "总下载大小",
"downloadedSize": "已下载大小",
"percentComplete": "完成百分比",
"failureReason": "失败原因",
"result": "最终结果",
}

扩展思考:BackgroundService 域填补了浏览器后台行为调试的最后一块拼图。结合之前学习的 Network、Runtime、Storage 等域,你现在拥有了对浏览器完整生命周期的编程控制能力——从前端 JS 执行到网络请求、从存储管理到后台服务。这种全方位的可见性是构建高级浏览器自动化工具和 PWA 调试器的基石。

上一篇回顾:CDP 错误追踪指南:用 Python 捕获页面异常。