Files
Nexus/docs/design/plans/2026-06-02-terminal-refactor.md
2026-07-08 22:31:31 +08:00

8.4 KiB
Raw Permalink Blame History

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 无需变更

实施步骤

T1markRaw 修复 + Uptime 计时器内嵌(Critical 修复)

目标:解决 C1Vue 代理 xterm/WebSocket)和 C3Uptime timer 外部 Map

步骤

  1. TerminalPage.vue 中,将 TermSession interface 拆分:
    • 新增 TermSessionNative type,字段:ws / term / fitAddon / resizeObserver / reconnectTimer / pingTimer / uptimeTimer
    • 新增 module-level const nativeMap = new Map<string, TermSessionNative>()
    • TermSession interface 中删除上述字段
  2. 所有原生对象创建时包裹 markRaw()
    • term = markRaw(new Terminal(...))
    • fitAddon = markRaw(new FitAddon())
    • resizeObserver = markRaw(new ResizeObserver(...))
    • WebSocket 无需 markRaw(已在 connectSession 内部使用,不进 reactive
  3. _uptimeTimers Map 删除,uptimeTimer 改存入 nativeMap[sessionId].uptimeTimer
  4. 更新 closeTabonBeforeUnmount 中的清理逻辑,从 nativeMap 获取 native 对象后清理

验收Vue DevTools → Components 面板,sessions ref 中不出现 Terminal/WebSocket/FitAddon 对象


T2Per-Session OverlayCritical 修复)

目标:解决 C2(全局 overlay 状态)

步骤

  1. TermSession interface 中新增字段:
    showOverlay: boolean       // 替代全局 showDisconnectOverlay
    overlayMsg: string         // 替代全局 disconnectMsg
    reconnectCountdown: string // 移入 session
    
  2. 删除全局 showDisconnectOverlaydisconnectMsgreconnectCountdown refs
  3. connectSessionstartReconnectdisconnectcloseTab 中,将原来操作全局 ref 的代码改为操作 s.showOverlay / s.overlayMsg / s.reconnectCountdown
  4. 模板中的 <v-overlay> 绑定改为 activeSession?.showOverlay
  5. 关闭最后一个 Tab 时,不显示 overlay,让空状态区域自然显示(删除 showDisconnectOverlay = true 的 closeTab 逻辑)

验收

  • 打开 2 个会话,让会话 2 断开,切换到会话 2,overlay 显示
  • 关闭最后一个会话,出现空状态服务器选择器,不出现 overlay

T3:提取 Composables

目标:解决 M1(单文件过大),提取 4 个 Composable

T3-AuseTerminalSettings

// 仅管理 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-BuseTerminalCmdBar

// 管理命令输入、历史记录
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-CuseTerminalSessions

核心:多 Tab 管理,包含 sessions / activeSessionId / activeSession / newSession / closeTab / switchTab / applyRouteQuery

T3-DuseTerminalSession

核心:单会话连接 + 重连 + 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.vueprops: mode: 'inline' | 'dialog'
  • 空状态区域使用 <TerminalServerPicker mode="inline">
  • 新建会话对话框内容使用 <TerminalServerPicker mode="dialog">
  • 删除模板中的重复列表代码

T6Minor 体验修复

E1 修复(向同一服务器新建会话):

  • newSession 中检测到同 serverId 时,弹出 snackbar:「会话已存在,已切换过去。如需新建请关闭原会话。」
  • 提供一个可选 opts.forceNew: boolean 参数供未来扩展

E2 修复(初始 cd 指令时序):

  • sendCdToSessionsetTimeout(200) 改为在 TERMINAL_INIT 消息处理中直接发送,不依赖超时

E3 修复(重复快捷键):

  • 删除 onKeydown 中重复的 e.key === '=' 分支,只保留 e.key === '+'(标准 Chromium 行为)

E4 修复(快捷命令加载失败提示):

  • loadQuickCmds catch 块改为: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 回滚到重构前。


执行顺序建议

T1markRaw + timer, 1h → T2per-session overlay, 1h
→ T6minor fixes, 0.5h
→ T3composables, 2h)→ T4components, 2h)→ T5dom + picker, 0.5h
→ 全量测试 → changelog → 部署

建议 T1+T2+T6 作为第一批次(安全稳定性),T3+T4+T5 作为第二批次(架构重构)分两次 PR 合并。