从输入 URL 到页面加载完成的全链路
整个链路可分为 6 个阶段,跨越 浏览器主线程 → SharedWorker → SSE → HUB → WebRTC → Worker。
第一阶段:路由匹配与布局嵌套
-
SolidStart 文件路由 匹配到
routes/dev/browse/index.tsx -
布局嵌套链(每个
(name)是一个无路径分段的 route group):textapp.tsx Layout └─ MetaProvider + StorageProvider + ThemeProvider + Suspense/dev/browse是独立路由,不在(connected)/(live)组下,所以它自己包了自己的ApiProvider和ConnectionManagerProvider -
ConnectionManagerProvider(连接管理器) 在onMount时启动三项关键工作:registerSharedWorkerPort(tabId)— 建立与 SharedWorker 的MessagePort通道- 向 SharedWorker 发送
SSE_CREATE_REQUEST请求建立 SSE 长连接 - 初始化心跳检测
第二阶段: SharedWorker 启动 & SSE 长连接建立
- 注册 SharedWorker 端口 (
eventManager.ts:registerSharedWorkerPort):- 创建
SharedWorker实例(connection/worker/SharedWorker.ts) - 通过
port.postMessage发送注册消息 - MessageRouter 收到后记录该 tab 信息并回复
PORT_REGISTER_RESPONSE - 页面收到后启动
ClientHeartbeat(心跳检测)
- 创建
- SSE 长连接建立 (
ConnectionManagerProvider.initSSEConnection):- 页面通过
port.postMessage发送SSE_CREATE_REQUEST(含 URL 和 token) - MessageRouter 将其交给
SSEManager(运行在 SharedWorker 线程) - SSEManager 创建
EventSource连接到 hub 的 SSE 端点 - 成功连接后 hub 推送
{"type":"connected","peer_uuid":"..."} - SSEManager 广播
SSE_CREATE_RESPONSE状态回所有页面 - SSE 通道成为信令通道,后续 WebRTC 的 offer/answer/ICE candidate 都通过它中转
- 页面通过
第三阶段: WebRTC 连接建立(按需触发)
- 当页面第一次调用
createPeerToPeerLink(workerId)时(如useFileService或useTerminalService),Peer 类构造函数被执行:- 向 SharedWorker 发送
RTC_CREATE_REQUEST(含 workerId) - MessageRouter 收到后:
- 检查该 worker 是否已有 RTCProviderTab,有则复用
- 没有则选举一个 RTCProviderTab(当前可见页面)
- 检查 SSE 状态,未就绪则入队等待
- 发送
RTC_CREATE_REQUEST到选中的 provider tab
- 向 SharedWorker 发送
- 主线程 RTCManager 收到
RTC_CREATE_REQUEST:- 创建
RTCPeerConnection实例 - 创建默认 DataChannel(
control+stream) - 生成 SDP Offer,通过 Hub 的信令 API 转发给 Worker
- Hub 通过 SSE 推送回 Worker 的 SDP Answer + ICE candidates
- WebRTC
peerConnection.onconnectionstatechange → "connected" - 发射
RTC_CREATE_RESPONSE,通知页面 RTC 就绪 commandQueueManager.notifyCanSend()— 命令队列开始发包
- 创建
- Worker 端收到
termSpawnRequest等指令后:- 解析 CBOR 信封
- 授权验证(MAC 签名校验)
- 执行对应操作
第四阶段: 页面组件渲染
-
Browse页面渲染内容(结构布局):text┌──────────────────────────────────────────────────────┐ │ Header: "Navbar" │ ├───────────┬──────────────────────────────────────────┤ │ Left 25% │ Right 75% │ │ ┌───────┐ │ ┌──────────────────┬───────────────────┐│ │ │Parent │ │ │ FileList (60%) │ Preview (40%) ││ │ │Folder │ │ │ [breadcrumb] │ (空/图片/子目录) ││ │ │ │ │ │ [搜索] │ ││ │ │ 67% │ │ │ [文件列表] │ ││ │ │ │ │ └──────────────────┴───────────────────┘│ │ ├───────┤ │ ┌──────────────────────────────────────┐│ │ │Pinboard│ │ │ XtermTerminal (33%) ││ │ │ 33% │ │ │ [Logo] [shell选择] [连接按钮] ││ │ └───────┘ │ └──────────────────────────────────────┘│ └───────────┴──────────────────────────────────────────┘ -
FileListPanel和ParentFolderPanel各自调用useFileService(workerId):useFileService→useConnectionManager().createPeerToPeerLink(workerId)- 即触发第三阶段的 WebRTC 连接流程
- 创建成功后,通过
peer.fsRootsRequest()获取磁盘根路径列表 - 通过
peer.readdirRequest({path})读取目录
-
文件请求的数据路径:
textuseFileService.fsRoots() / readdir(path) → peer().fsRootsRequest() / readdirRequest({path}) → emitPeerCommand() → 生成 [cmd, seqId, params] 信封 → port.postMessage(RTC_SEND_DATA_REQUEST) → SharedWorker → MessageRouter → port.postMessage() → RTCProviderTab → RTCManager.handlePeerCommand() → 1. getCertificate() → HTTP POST 到 hub 获取权限证书 → 2. validate + CBOR 编码 → 3. commandQueueManager.add() → RTCDataChannel.send(cborEnvelope) → Worker 收到 → 执行 → 回复 [cmd, null, result] 通过 DataChannel → RTCManager.onControlChannelMessage() → handleCommandSuccessResponse → emit(RTC_SEND_DATA_RESPONSE) → port.postMessage() → SharedWorker → MessageRouter.handleSendRTCDataResult → port.postMessage() → 主线程 Peer.handleReceivedEnvelope → sendQueue 中对应 Promise 的 resolve(payload) -
XtermTerminal渲染:- 此时显示空闲状态(
service.state() === "idle") - 叠加层显示 ASCII-NLAKE Logo + shell 选择下拉框 + "连接" 按钮
- xterm.js 终端已初始化,但未连接 PTY
- 此时显示空闲状态(
第五阶段: 终端连接(用户点击"连接"后)
-
用户选择 shell(默认/ bash / zsh / powershell / fish),点击连接
-
doSpawn()→service.spawn(cols, rows, shell, handlers):- 注册
peer.onTermOutput和peer.onTermExit监听器 - 设置
state → "connecting" - 发送
peer.termSpawnRequest({shell, cols, rows}) - 经过 RTC 链路到达 Worker
- Worker 创建 PTY session(含 shell injection 脚本注入)
- 返回
{session_id, control_token} - 设置
state → "connected"
- 注册
-
进入 connected 状态后,
createEffect触发:text发送彩色 prompt 配置命令: prompt $E[38;2;122;126;133m[$T]$E[0m ... & cls 设置 initDone = true- 此后 PTY 输出开始写入 xterm
-
用户输入 →
term.onData→service.sendInput(data)→peer.termInput(...)→ DataChannel → Worker → PTY stdin -
PTY 输出 → Worker → DataChannel
onTermOutput→ 回调 →term.write(data)
第六阶段: 页面完全加载
-
所有面板数据就绪:
- 文件列表填充完毕(
loading=false) - 预览面板为空(等待用户点击文件)
- 终端面板显示欢迎界面,等待用户点击连接
- 文件列表填充完毕(
-
SharedWorker 心跳持续运行,检测页面健康状态
-
SSE 长连接保持,用于信令中转和后续 RTC 切换
关键架构要点
| 层 | 技术 | 职责 |
|---|---|---|
| 页面 UI | SolidJS + SolidStart | 文件路由、响应式渲染、组件树 |
| 通信中枢 | SharedWorker (MessagePort) | 跨 tab 消息路由、心跳、SSE 管理 |
| 数据通道 | WebRTC (DataChannel) | 浏览器 ↔ Worker 全双工通信 |
| 信令 | Server-Sent Events (SSE) | WebRTC offer/answer/candidate 中转 |
| 权限 | 临时证书 (MAC) | 每条指令带有时效性签名 |
| 序列化 | CBOR | 二进制高效编码 |
| 服务端 | Hub + Worker (Rust) | 信令转发 / PTY 创建 + 文件操作 |
最复杂的一段路径
FileListPanel 首次请求文件时:Solid Effect → useFileService → Peer → MessagePort → SharedWorker(MessageRouter) → MessagePort → RTCManager → Hub(权限证书) → DataChannel → Worker — 然后原路返回。这条链路涉及 6 个进程间/网络跳转,每条指令都可能经过队列重试和 MAC 签名验证。