8.4 KiB
Terminal 页重构技术计划
日期: 2026-06-02
对应设计文档: docs/design/specs/2026-06-02-terminal-refactor-design.md
阶段编号: T1 ~ T6
涉及文件清单
新建文件
| 文件 | 说明 |
|---|---|
frontend/src/composables/terminal/useTerminalSession.ts |
单会话 xterm + WS 生命周期 |
frontend/src/composables/terminal/useTerminalSessions.ts |
多会话 Tab 管理 |
frontend/src/composables/terminal/useTerminalSettings.ts |
fontSize/scrollback localStorage 持久化 |
frontend/src/composables/terminal/useTerminalCmdBar.ts |
命令输入 + 历史记录 |
frontend/src/components/terminal/TerminalTabBar.vue |
Tab 条 |
frontend/src/components/terminal/TerminalToolbar.vue |
工具栏(状态/字号/断开) |
frontend/src/components/terminal/TerminalArea.vue |
xterm 挂载区 + per-session overlay |
frontend/src/components/terminal/TerminalCmdBar.vue |
命令栏 + 快捷键面板 |
frontend/src/components/terminal/TerminalQuickBar.vue |
快捷命令栏 |
frontend/src/components/terminal/TerminalServerPicker.vue |
服务器选择(inline + dialog 两种模式) |
修改文件
| 文件 | 改动 |
|---|---|
frontend/src/pages/TerminalPage.vue |
瘦身至 ~130 行,仅组合上述组件 |
不改动文件
| 文件 | 原因 |
|---|---|
server/api/webssh.py |
后端逻辑已完善,协议不变 |
frontend/src/composables/useTerminalQuickCommands.ts |
已结构良好,T6 仅小调整 |
frontend/src/components/ShellHighlightField.vue |
无需变更 |
frontend/src/utils/wsUrl.ts |
无需变更 |
实施步骤
T1:markRaw 修复 + Uptime 计时器内嵌(Critical 修复)
目标:解决 C1(Vue 代理 xterm/WebSocket)和 C3(Uptime timer 外部 Map)
步骤:
- 在
TerminalPage.vue中,将TermSessioninterface 拆分:- 新增
TermSessionNativetype,字段:ws / term / fitAddon / resizeObserver / reconnectTimer / pingTimer / uptimeTimer - 新增 module-level
const nativeMap = new Map<string, TermSessionNative>() - 从
TermSessioninterface 中删除上述字段
- 新增
- 所有原生对象创建时包裹
markRaw():term = markRaw(new Terminal(...))fitAddon = markRaw(new FitAddon())resizeObserver = markRaw(new ResizeObserver(...))- WebSocket 无需 markRaw(已在 connectSession 内部使用,不进 reactive)
- 将
_uptimeTimersMap 删除,uptimeTimer 改存入nativeMap[sessionId].uptimeTimer - 更新
closeTab和onBeforeUnmount中的清理逻辑,从nativeMap获取 native 对象后清理
验收:Vue DevTools → Components 面板,sessions ref 中不出现 Terminal/WebSocket/FitAddon 对象
T2:Per-Session Overlay(Critical 修复)
目标:解决 C2(全局 overlay 状态)
步骤:
- 在
TermSessioninterface 中新增字段:showOverlay: boolean // 替代全局 showDisconnectOverlay overlayMsg: string // 替代全局 disconnectMsg reconnectCountdown: string // 移入 session - 删除全局
showDisconnectOverlay、disconnectMsg、reconnectCountdownrefs - 在
connectSession、startReconnect、disconnect、closeTab中,将原来操作全局 ref 的代码改为操作s.showOverlay / s.overlayMsg / s.reconnectCountdown - 模板中的
<v-overlay>绑定改为activeSession?.showOverlay - 关闭最后一个 Tab 时,不显示 overlay,让空状态区域自然显示(删除
showDisconnectOverlay = true的 closeTab 逻辑)
验收:
- 打开 2 个会话,让会话 2 断开,切换到会话 2,overlay 显示
- 关闭最后一个会话,出现空状态服务器选择器,不出现 overlay
T3:提取 Composables
目标:解决 M1(单文件过大),提取 4 个 Composable
T3-A:useTerminalSettings
// 仅管理 fontSize / scrollback 的 localStorage 持久化
export function useTerminalSettings() {
const fontSize = ref(...)
const scrollback = ref(...)
function changeFontSize(delta: number): void
function changeScrollback(val: number): void
return { fontSize, scrollback, scrollbackOptions, changeFontSize, changeScrollback }
}
T3-B:useTerminalCmdBar
// 管理命令输入、历史记录
export function useTerminalCmdBar(getActiveSession: () => TermSession | null) {
const cmdInput = ref('')
function sendCmd(): void
function cmdHistoryUp(): void
function cmdHistoryDown(): void
function sendCtrl(key: string): void
function sendKey(key: string): void
return { cmdInput, sendCmd, cmdHistoryUp, cmdHistoryDown, sendCtrl, sendKey }
}
T3-C:useTerminalSessions
核心:多 Tab 管理,包含 sessions / activeSessionId / activeSession / newSession / closeTab / switchTab / applyRouteQuery
T3-D:useTerminalSession
核心:单会话连接 + 重连 + ping + uptime,参数为 sessionData ref 和 nativeMap
注意:T3-C/D 是最大改动,需完整测试后再提交
T4:拆分 Components
目标:消除模板耦合,每个组件职责单一
| 组件 | Props | Emits |
|---|---|---|
TerminalTabBar |
sessions, activeSessionId |
switch, close, new |
TerminalToolbar |
session, fontSize, scrollback |
font-change, scrollback-change, disconnect, fullscreen |
TerminalArea |
sessions, activeSessionId |
context-menu |
TerminalCmdBar |
disabled |
send, ctrl, key |
TerminalQuickBar |
commands |
exec, manage |
TerminalServerPicker |
servers, mode |
select |
TerminalPage.vue 仅负责组合,约 80 行模板 + 30 行 setup。
T5:修复 M2(DOM 查询) + M3(服务器选择器去重)
M2 修复:
TerminalCmdBar.vue组件内部声明const inputRef = ref<HTMLTextAreaElement | null>(null)- 将
<ShellHighlightField>替换为绑定 ref 的原生 textarea(或 ShellHighlightField 补充 expose) - 删除
getCmdInputEl()函数
M3 修复:
- 新建
TerminalServerPicker.vue,props:mode: 'inline' | 'dialog' - 空状态区域使用
<TerminalServerPicker mode="inline"> - 新建会话对话框内容使用
<TerminalServerPicker mode="dialog"> - 删除模板中的重复列表代码
T6:Minor 体验修复
E1 修复(向同一服务器新建会话):
newSession中检测到同 serverId 时,弹出 snackbar:「会话已存在,已切换过去。如需新建请关闭原会话。」- 提供一个可选
opts.forceNew: boolean参数供未来扩展
E2 修复(初始 cd 指令时序):
- 将
sendCdToSession从setTimeout(200)改为在TERMINAL_INIT消息处理中直接发送,不依赖超时
E3 修复(重复快捷键):
- 删除
onKeydown中重复的e.key === '='分支,只保留e.key === '+'(标准 Chromium 行为)
E4 修复(快捷命令加载失败提示):
loadQuickCmdscatch 块改为:snackbar('快捷命令加载失败,使用内置命令', 'warning')后再赋值内置
依赖与配置变更
- 无新 npm 包依赖
- 无 Vue Router / Pinia 结构变更
- xterm 版本不变(@xterm/xterm)
测试要点
| 场景 | 验证方式 |
|---|---|
| markRaw 生效 | Vue DevTools sessions ref 无 Terminal 实例 |
| 多 Tab 断开 overlay | 手动断网,切换 Tab,overlay 正确显示 |
| 最后 Tab 关闭 | 显示空状态选择器,不显示 overlay |
| xterm resize | 拖拽侧栏/改字号,xterm 正确填充容器 |
| reconnect 指数退避 | 断连后等待,验证 1s/2s/4s 重连间隔 |
| 命令历史 | 发送命令,↑/↓ 正确回溯 |
| 快捷命令 | 点击执行,命令发送到活动终端 |
| pendingPath 跳转 | /terminal?server_id=1&path=/var/log 打开后自动 cd |
| T1-T6 全部功能回归 | 手动端到端测试所有功能 |
回滚方式
全程在功能分支 refactor/terminal 开发,main 分支不受影响。
任何阶段出现严重问题,直接 git checkout main -- frontend/src/pages/TerminalPage.vue 回滚到重构前。
执行顺序建议
T1(markRaw + timer, 1h) → T2(per-session overlay, 1h)
→ T6(minor fixes, 0.5h)
→ T3(composables, 2h)→ T4(components, 2h)→ T5(dom + picker, 0.5h)
→ 全量测试 → changelog → 部署
建议 T1+T2+T6 作为第一批次(安全稳定性),T3+T4+T5 作为第二批次(架构重构)分两次 PR 合并。