文档/TuffTransport 技术内幕
通用开发

TuffTransport 技术内幕

TuffTransport 技术内幕

概述

本文档深入介绍 TuffTransport 的架构设计,解释技术决策和实现细节。

介绍

适合需要理解 IPC 与传输层实现的开发者,建议结合 TuffTransport API 一起阅读。

技术原理

架构概览

EXAMPLE.VUE
┌─────────────────────────────────────────────────────────────────────────┐
│                        TuffTransport 架构                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   插件渲染进程                               主进程                       │
│   ┌─────────────────────┐                  ┌─────────────────────┐      │
│   │  useTuffTransport() │                  │  TuffTransportMain  │      │
│   │  ┌───────────────┐  │                  │  ┌───────────────┐  │      │
│   │  │ Event Builder │  │                  │  │ Event Router  │  │      │
│   │  └───────┬───────┘  │                  │  └───────┬───────┘  │      │
│   │          │          │                  │          │          │      │
│   │  ┌───────▼───────┐  │   ipc.invoke    │  ┌───────▼───────┐  │      │
│   │  │ BatchManager  │──┼──────────────────┼──│ BatchHandler  │  │      │
│   │  └───────────────┘  │                  │  └───────────────┘  │      │
│   │                     │                  │                     │      │
│   │  ┌───────────────┐  │   MessagePort   │  ┌───────────────┐  │      │
│   │  │ StreamClient  │◄─┼──────────────────┼─►│ StreamServer  │  │      │
│   │  └───────────────┘  │                  │  └───────────────┘  │      │
│   └─────────────────────┘                  └─────────────────────┘      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

0. Port / 子端口模型(新增)

TuffTransport 抽象为“传输核心 + Port”。Port 是在同一传输层之上的逻辑通道,用于隔离不同能力域(如 storage、corebox、plugin、worker)。

EXAMPLE.VUE
TuffTransport
  ├─ port('storage')     // 配置/订阅
  ├─ port('corebox')     // 搜索/渲染
  └─ port('plugin:xxx')  // 插件隔离

要点:

  • Port 只负责 onMessage/onStream 与路由规则,不关心 IPC 实现细节。
  • 具体传输(Main/Renderer/Plugin/Worker)由 impl 层负责,Port 作为协议层统一收敛。
  • Port 支持独立生命周期(注册/销毁),便于模块卸载时自动清理事件与流。

1. 事件系统设计

为什么不用字符串?

传统 Channel API 使用字符串事件名:

EXAMPLE.TS
// 字符串事件的问题:
channel.send('core-box:serch:query', data)  // 拼写错误:"serch" - 没有报错!
channel.send('core-box:search:query', { txt: 'hi' })  // 字段错误 - 没有报错!

问题:

  1. 无自动补全 - 必须记住准确的事件名
  2. 无类型检查 - 编译时不知道参数类型
  3. 重构风险 - 重命名事件需要手动查找替换
  4. 运行时错误 - 拼写错误只能在运行时发现

TuffEvent 解决方案

TuffEvent 使用 TypeScript 类型系统强制正确性:

EXAMPLE.TS
// TuffEvent 定义(简化版)
interface TuffEvent<TRequest, TResponse, TNamespace, TModule, TAction> {
  readonly __brand: 'TuffEvent'  // 用于运行时检查的品牌标识
  readonly namespace: TNamespace
  readonly module: TModule
  readonly action: TAction
  readonly _request: TRequest    // 请求的幻影类型
  readonly _response: TResponse  // 响应的幻影类型
  toString(): string
}

关键设计决策:

  1. 品牌类型 - __brand: 'TuffEvent' 支持运行时类型检查
  2. 幻影类型 - _request_response 仅存在于类型层面
  3. 不可变 - 事件使用 Object.freeze() 冻结
  4. 字符串转换 - toString() 返回事件名用于 IPC

Event Builder 模式

构建器模式确保事件正确构造:

EXAMPLE.TS
defineEvent('namespace')     // 返回 TuffEventBuilder<'namespace'>
  .module('module')          // 返回 TuffModuleBuilder<'namespace', 'module'>
  .event('action')           // 返回 TuffActionBuilder<'namespace', 'module', 'action'>
  .define<Req, Res>(opts)    // 返回 TuffEvent<Req, Res, 'namespace', 'module', 'action'>

为什么用构建器?

  • 强制完整的事件定义
  • 提供清晰、可读的 API
  • 每一步都支持 IDE 自动补全
  • 编译时验证

2. 批量系统设计

问题

每次 IPC 调用都有开销(约 1-5ms)。多次顺序调用会累积:

EXAMPLE.TS
// 无批量:3 次 IPC 调用 = 3-15ms 开销
const a = await channel.send('storage:get', { key: 'a' })  // IPC #1
const b = await channel.send('storage:get', { key: 'b' })  // IPC #2
const c = await channel.send('storage:get', { key: 'c' })  // IPC #3

批量流程

EXAMPLE.VUE
请求 1 ─┐
请求 2 ─┼─► BatchManager ─► [窗口期 50ms] ─► 单次 IPC
请求 3 ─┘       │                              │
                │                              ▼
         windowMs 定时器              主进程处理器
                │                              │
                ▼                              ▼
         强制刷新条件:                处理所有请求
         - 定时器到期                       │
         - 达到最大数量                     ▼
         - 调用 flush()               返回所有结果
                                            │
响应 1 ◄─┐                                  │
响应 2 ◄─┼─ 分发结果 ◄─────────────────────┘
响应 3 ◄─┘

BatchManager 实现

EXAMPLE.TS
class BatchManager {
  private groups: Map<string, BatchGroup> = new Map()
  
  async add<TReq, TRes>(event: TuffEvent<TReq, TRes>, payload: TReq): Promise<TRes> {
    const config = event._batch
    
    // 如果未启用批量,直接发送
    if (!config?.enabled) {
      return this.sendSingle(event, payload)
    }
    
    return new Promise((resolve, reject) => {
      const group = this.getOrCreateGroup(event)
      
      // 应用合并策略
      this.applyStrategy(group, { payload, resolve, reject }, config)
      
      // 检查刷新条件
      if (group.requests.length >= config.maxSize) {
        this.flush(event.toString())
      } else if (!group.timer) {
        group.timer = setTimeout(() => this.flush(event.toString()), config.windowMs)
      }
    })
  }
}

合并策略

1. Queue(默认) 所有请求按顺序保留并处理:

EXAMPLE.JSON
[{key:'a'}, {key:'b'}, {key:'a'}] → 处理全部 3 个

2. Dedupe 相同参数共享一个请求:

EXAMPLE.JSON
[{key:'a'}, {key:'b'}, {key:'a'}] → 处理 2 个,两个 'a' 获得相同结果

3. Latest 相同 key 只保留最新请求:

EXAMPLE.JSON
[{key:'a',v:1}, {key:'b'}, {key:'a',v:2}] → 处理 [{key:'a',v:2}, {key:'b'}]

3. 流式系统设计

为什么用 MessagePort?

常规 IPC 对流式传输有限制:

  • 请求-响应模式不适合连续数据
  • 大数据包阻塞 IPC 通道
  • 无背压处理

MessagePort 优势:

  • 每个流独立通道
  • 非阻塞数据传输
  • 原生背压支持
  • 对二进制数据高效

流式流程

EXAMPLE.VUE
渲染进程                                主进程
   │                                       │
   │─── 1. 请求流 ─────────────────────►  │
   │    (通过 ipc.invoke)                  │
   │                                       │
   │◄── 2. 返回 { streamId, port2 } ──────│
   │    (port2 被传递)                     │
   │                                       │
   │◄══ 3. 数据块 ═════════════════════════│
   │    (通过 MessagePort)                 │
   │                                       │
   │◄══ 4. 更多数据... ════════════════════│
   │                                       │
   │◄══ 5. 结束信号 ═══════════════════════│
   │                                       │
   │─── 6. 关闭端口 ──────────────────────►│

StreamServer(主进程)

EXAMPLE.TS
class StreamServer {
  async handleStreamRequest(eventName: string, payload: any, webContents: WebContents) {
    const { port1, port2 } = new MessageChannelMain()
    const streamId = generateId()
    
    // 发送 port2 给渲染进程
    webContents.postMessage('@tuff:stream:port', { streamId }, [port2])
    
    // 创建处理器上下文
    const context: StreamContext = {
      emit: (chunk) => port1.postMessage({ type: 'data', chunk }),
      error: (err) => port1.postMessage({ type: 'error', message: err.message }),
      end: () => port1.postMessage({ type: 'end' }),
      isCancelled: () => this.cancelled.has(streamId)
    }
    
    // 执行处理器
    await this.handlers.get(eventName)?.(payload, context)
    
    return { streamId }
  }
}

背压处理

当消费者处理不过来时:

EXAMPLE.TS
const config: StreamConfig = {
  enabled: true,
  bufferSize: 100,
  backpressure: 'buffer' // 'drop' | 'buffer' | 'error'
}
  • drop - 缓冲区满时丢弃新数据
  • buffer - 缓冲数据(有内存风险)
  • error - 缓冲区满时抛出错误

4. 插件安全

Key 机制

插件运行在隔离的 WebContentsView 中。为防止未授权访问:

EXAMPLE.VUE
┌─────────────────────────────────────────────────────────────────┐
│                    插件安全流程                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 插件加载                                                      │
│     │                                                            │
│     ▼                                                            │
│  2. 主进程生成唯一 key                                            │
│     key = randomString() → 存入 keyToNameMap                     │
│     │                                                            │
│     ▼                                                            │
│  3. Key 注入插件的 preload                                        │
│     window.$plugin.uniqueKey = key                               │
│     │                                                            │
│     ▼                                                            │
│  4. 所有插件消息在 header 中包含 key                              │
│     { header: { uniqueKey: key }, ... }                          │
│     │                                                            │
│     ▼                                                            │
│  5. 主进程验证 key                                                │
│     pluginName = keyToNameMap.get(key)                          │
│     if (!pluginName) reject()                                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

PluginKeyManager

EXAMPLE.TS
interface PluginKeyManager {
  requestKey(pluginName: string): string   // 生成新 key
  revokeKey(key: string): boolean          // 使 key 失效
  resolveKey(key: string): string | undefined  // 获取插件名
  isValidKey(key: string): boolean         // 验证 key
}

安全上下文

每个处理器都接收安全上下文:

EXAMPLE.TS
transport.on(SomeEvent, (payload, context) => {
  if (context.plugin) {
    console.log(`请求来自插件: ${context.plugin.name}`)
    console.log(`Key 已验证: ${context.plugin.verified}`)
  }
})

5. 错误处理

错误流程

EXAMPLE.VUE
渲染进程                                主进程
   │                                       │
   │─── 请求 ─────────────────────────────►│
   │                                       │
   │                               处理器抛出错误
   │                                       │
   │◄── TuffTransportError ────────────────│
   │    { code, message, eventName }       │
   │                                       │
   ▼
catch (err) {
  if (err instanceof TuffTransportError) {
    // 结构化错误处理
  }
}

错误序列化

错误被序列化用于 IPC:

EXAMPLE.TS
class TuffTransportError extends Error {
  toJSON() {
    return {
      name: 'TuffTransportError',
      code: this.code,
      message: this.message,
      eventName: this.eventName,
      timestamp: this.timestamp
    }
  }
  
  static fromJSON(obj) {
    return new TuffTransportError(obj.code, obj.message, {
      eventName: obj.eventName
    })
  }
}

6. 性能考量

IPC 开销

操作大约耗时
单次 IPC 调用1-5ms
序列化(小数据)0.1ms
序列化(大数据)1-10ms
MessagePort 建立2-5ms
MessagePort 消息0.1-0.5ms

优化策略

  1. 默认批量 - 为频繁事件启用批量
  2. 大数据用流 - >100KB 用 MessagePort
  3. 尽量去重 - 相同请求共享响应
  4. 延迟求值 - 只在刷新批量时序列化

内存管理

EXAMPLE.TS
// 清理模式
onUnmounted(() => {
  // 取消待处理请求
  controller.cancel()
  
  // 移除处理器
  cleanup()
  
  // 刷新批量
  transport.flush()
})

7. 与已退役 Channel 对比

方面已退役 ChannelTuffTransport
事件定义字符串TuffEvent 对象
类型安全完整 TypeScript
自动补全完整 IDE 支持
批量处理手动自动
流式传输不支持MessagePort
错误类型通用 ErrorTuffTransportError
插件安全uniqueKey headerPluginKeyManager
当前支持仅历史参考受支持入口

迁移路径

EXAMPLE.TS
// 已退役 Channel 写法仅用于迁移审阅
channel.send('event', data)

// 当前代码使用 TuffTransport
transport.send(TuffEvent, data)

最佳实践

  • 高频事件优先走批量队列,减少 IPC 调用次数。
  • 大体积数据使用流式传输,避免阻塞主线程。
  • 迁移时先替换核心链路,再覆盖边缘能力。

总结

TuffTransport 提供:

  1. 类型安全 - 通过 TuffEvent 进行编译时事件验证
  2. 性能 - 自动批量处理减少 IPC 开销
  3. 流式传输 - MessagePort 用于大/连续数据
  4. 安全 - 通过 key 机制进行插件隔离
  5. 人体工学 - 简洁 API 配合完整 IDE 支持
  6. 当前 API - 插件开发受支持的 IPC 面