Read OSS

Hyper 的架构:深入解析一个 Electron 终端模拟器代码库

中级

前置知识

  • Electron 基础知识(主进程与渲染进程的区别)
  • 熟悉 webpack 的基本概念
  • TypeScript 基础

Hyper 的架构:深入解析一个 Electron 终端模拟器代码库

Hyper 是一款终端模拟器,但它的出发点更像是一个 Web 应用,而非传统意义上的终端工具。它基于 Electron 构建,使用 React、Redux 和 xterm.js 渲染完整的终端体验,同时将几乎所有核心能力都开放给第三方插件。如果你好奇把 Electron 推向极限、用来做一个对性能要求极高的终端会是什么样,Hyper 的代码库值得深入研究。

本文是系列的第一篇,我们将对整个项目进行全面梳理:三个独立进程、出乎意料简洁的目录结构,以及一套以一些非常规方式使用 webpack 的构建系统——其中甚至包括一个用 null-loader 让 webpack "什么都不做,只负责复制文件"的技巧。

三进程架构概览

Hyper 以三个独立可执行体的形式运行,各自拥有独立的入口文件和构建目标:

  1. 主进程app/)—— Electron 主进程,负责管理窗口、PTY 会话、菜单、配置文件监听、插件安装以及自动更新。
  2. 渲染进程lib/)—— Electron 渲染进程,运行 React/Redux,渲染 xterm.js 终端,处理键盘快捷键,并管理分屏 UI。
  3. CLI 工具cli/)—— 一个独立的 Node.js 可执行文件(hyper),用于管理插件和从命令行启动应用。
flowchart TD
    subgraph "Main Process (app/)"
        A[BrowserWindow Manager] --> B[PTY Sessions via node-pty]
        A --> C[Config Watcher]
        A --> D[Plugin Installer]
        A --> E[Menu System]
    end

    subgraph "Renderer Process (lib/)"
        F[React + Redux] --> G[xterm.js Terminal]
        F --> H[Split Pane UI]
        F --> I[Keyboard Shortcuts]
    end

    subgraph "CLI (cli/)"
        J[Plugin Management]
        K[App Launcher]
    end

    A <-->|"RPC over IPC"| F
    J -->|"Edits hyper.json"| C

Hyper 架构的独特之处在于,主进程扮演着服务定位器的角色——app 对象在运行时被动态扩展,挂载了 configpluginscreateWindowgetLastFocusedWindow 等属性。这一模式在主进程入口文件的开头就能清晰看到:

app/index.ts#L42-L56

app 对象成为一个共享上下文,被传递给插件和内部子系统,统一承载配置、插件管理和窗口追踪的引用。

目录结构解读

Hyper 的仓库结构刻意保持扁平。项目没有引入 monorepo 工具,只有三个顶层目录分别对应三个进程,再加上一个共享类型定义目录:

目录 进程 用途
app/ 主进程 Electron 主进程:窗口、会话、配置、插件、菜单
app/config/ 主进程 配置加载、迁移、路径管理、JSON Schema
app/ui/ 主进程 窗口创建、右键菜单
app/plugins/ 主进程 扩展点定义、基于 yarn 的插件安装
app/menus/ 主进程 各平台的应用菜单
lib/ 渲染进程 React/Redux 应用:组件、容器、action、reducer
lib/components/ 渲染进程 React 组件:Term、TermGroup、Header、Tabs
lib/containers/ 渲染进程 与 Redux 连接的容器组件
lib/store/ 渲染进程 Redux store 配置与中间件
lib/actions/ 渲染进程 采用"副作用"模式的 Redux action creator
lib/reducers/ 渲染进程 Redux reducer:ui、sessions、termGroups
lib/utils/ 渲染进程 RPC 客户端、插件加载、配置访问
cli/ CLI 独立的插件管理工具
typings/ 共享 IPC、配置、状态的 TypeScript 类型定义

提示: 要理解 Hyper 的数据模型,从 typings/ 目录入手是最好的选择。typings/common.d.ts 定义了所有 IPC 事件,typings/config.d.ts 定义了完整的配置结构。这些类型定义是连接三个进程的契约所在。

主进程的 TypeScript 代码由 tsc 单独编译(而非 webpack),渲染进程则由 webpack 打包为 target/renderer/bundle.js。这种分工是刻意为之的:主进程运行在 Node.js 环境中,无需打包;渲染进程则需要打包以支持 V8 快照优化。

构建系统:三份 webpack 配置

webpack.config.ts 导出一个包含三项配置的数组,每一项都承担着截然不同的职责。

配置一:hyper-app——null-loader 技巧

webpack.config.ts#L10-L69

这是你可能见过的最非常规的 webpack 配置——它什么都不编译。所有 .ts.js 文件都经过 null-loader 处理,源代码全部被丢弃,输出文件甚至直接叫做 ignore_this.js

这份配置存在的唯一目的,是运行 CopyWebpackPlugin,将 HTML 文件、JSON 配置、键位映射、静态资源和补丁文件复制到 target/ 目录。这里的 webpack 只是一个文件复制的构建协调器。

为什么不用简单的 shell 脚本?因为这样可以接入开发阶段同样使用的 webpack -w 监听模式——当你修改 HTML 模板或键位映射 JSON 时,文件会自动同步到目标目录。

配置二:hyper——渲染进程 bundle

webpack.config.ts#L72-L154

渲染进程配置是一套标准的 electron-renderer 目标构建,但有一个显著特点:一个体量庞大的 externals 块,列出了约 30 个依赖项,每个外部依赖都映射到 require("./node_modules/...") 路径:

externals: {
  react: 'require("./node_modules/react/index.js")',
  redux: 'require("./node_modules/redux/lib/redux.js")',
  xterm: 'require("./node_modules/xterm/lib/xterm.js")',
  // ... 25+ more
}

这意味着这些模块不会被打包进 bundle.js,而是在运行时从应用内部的 node_modules 目录加载。这正是 Hyper V8 快照优化的基础所在。

配置三:hyper-cli——CLI 工具

webpack.config.ts#L155-L193

这是一份面向 Node.js 目标的常规配置,将 CLI 工具打包为 bin/cli.js。它使用 babel-loader 处理 TypeScript,并通过 shebang-loader 处理 rc 包中可执行脚本的 shebang 头。

flowchart LR
    subgraph "webpack.config.ts"
        A["hyper-app\n(null-loader + CopyPlugin)"] -->|"→ target/"| D[HTML + JSON + Static]
        B["hyper\n(babel-loader + externals)"] -->|"→ target/renderer/"| E[bundle.js]
        C["hyper-cli\n(babel-loader)"] -->|"→ bin/"| F[cli.js]
    end
    G["tsc --build"] -->|"→ target/"| H[Main process JS]

V8 快照与启动性能

终端模拟器对启动速度极为敏感。Hyper 通过 V8 快照来应对这一挑战——预编译的堆快照让 Electron 在加载重量级依赖时可以跳过解析和编译阶段。

package.json 中的 postinstall 脚本负责统筹整个流程:

yarn run v8-snapshot && webpack --config-name hyper-app && electron-builder install-app-deps

V8 快照的生成流程分为三个阶段:

sequenceDiagram
    participant P as postinstall
    participant M as mksnapshot
    participant W as webpack
    participant E as Electron

    P->>M: Run electron-mksnapshot
    M->>M: Pre-compile node_modules into snapshot blob
    P->>W: Build renderer bundle with externals
    Note over W: Dependencies excluded from bundle<br/>They'll come from the snapshot
    P->>E: Copy snapshot blobs to app directory
    E->>E: On startup, load snapshot instead of parsing JS

渲染进程 webpack 配置中的 externals 是关键的衔接点。通过将 React、Redux、xterm.js 等数十个库排除在 webpack bundle 之外,Hyper 确保它们在启动时从 V8 快照加载,而非从 JavaScript 源文件重新解析。externals 中那些特殊的 require("./node_modules/...") 路径,正是为了确保模块在快照上下文中能够正确解析。

主进程启动序列

主进程入口文件 app/index.ts 有一套经过精心设计的启动顺序。最开头的几行在任何模块导入执行之前,就处理了最早需要关注的事项:

app/index.ts#L1-L22

启动顺序如下:

  1. 处理 CLI 标志(第 5–12 行):若传入 --help--version,立即打印信息并退出,无需启动 Electron。
  2. 初始化 @electron/remote(第 16–17 行):必须在任何 BrowserWindow 创建之前完成。这让渲染进程可以同步调用主进程 API,插件系统大量依赖这一机制。
  3. 配置初始化(第 21–22 行):加载 hyper.json,启动 chokidar 文件监听,并检查已废弃的 CSS 配置。
sequenceDiagram
    participant OS as OS
    participant M as Main Process
    participant W as BrowserWindow
    participant R as Renderer

    OS->>M: Launch Electron
    M->>M: Check CLI flags (--help, --version)
    M->>M: Initialize @electron/remote
    M->>M: config.setup() — load + watch hyper.json
    M->>M: Extend app object (config, plugins, getWindows)
    M->>M: Wait for app.ready event
    M->>M: Install dev extensions (if dev mode)
    M->>W: createWindow() — BrowserWindow + RPC + sessions
    M->>W: loadURL(index.html)
    W->>R: Renderer starts
    M->>M: Set up menu, plugins.onApp, auto-updater
    M->>M: Register SSH protocol handler

app.ready 触发后,createWindow 函数以内联方式定义并立即调用,创建第一个窗口,同时挂载到 app.createWindow 以供插件后续调用。值得注意的是窗口的位置级联逻辑:每个新窗口相对于上一个聚焦窗口偏移 34px,并带有边界检查,防止窗口出现在屏幕可视区域之外。

渲染进程启动序列

渲染进程的入口文件 lib/index.tsx 首先初始化 V8 快照工具,随后立即创建 Redux store,并将核心对象暴露到全局:

lib/index.tsx#L1-L35

四个对象通过 Object.defineProperty 挂载到 window 上:storerpcconfigplugins。这是渲染进程版本的服务定位器模式——运行在渲染进程中的插件可以通过这些全局变量与系统进行交互。

接下来,渲染进程会注册约 30 个 RPC 事件监听器:

lib/index.tsx#L72-L234

每个 rpc.on(...) 处理函数都会 dispatch 一个 Redux action。'ready' 事件(第 72 行)在 RPC 通道建立时触发,启动初始的 Redux 状态设置。'session data' 事件(第 81 行)则是性能最关键的路径——它从数据字符串的前 36 个字符中提取 UUID,并 dispatch 给 xterm 进行渲染。

最后,React 应用完成挂载:

root.render(
  <Provider store={store_}>
    <HyperContainer />
  </Provider>
);

提示: 调试 Hyper 时,全局的 window.store 是你最好的工具。在运行中的 Hyper 实例里打开 DevTools,执行 store.getState(),即可查看完整的 Redux 状态树——会话、终端分组、UI 配置,一览无余。

下一步

梳理完整体架构之后,我们已经清楚每个部分的位置,以及三个进程之间的关联方式。但 Hyper 设计中最有意思的部分,是将这些进程粘合在一起的机制——一套有类型保障的 RPC 系统,负责跨 Electron IPC 边界传递终端数据、会话生命周期事件和各类指令。下一篇文章,我们将完整追踪一次终端会话的生命周期,从 PTY 创建,到数据批处理,再到 xterm 渲染的全过程。