个案研究*Electron

如何 Notion 复制
文本和HTML。

跟踪剪贴板调用从Notion的页面API,通过预加载和主进程,到Electron的剪贴板编写器。

Notion桌面7.6.1·JavaScript*REA

Notion 桌面包中的桥接代码

  1. 01 · 面向页面的API clipboard.write(data) 预加载公开了一个可调用的方法。
  2. 02 · 预加载包装器 ipcRenderer.invoke 包装器发送通道和参数。
  3. 03 · 主进程处理程序 ipcMain.handle 接收器检查哪个页面发送它。
  4. 04 · 系统剪贴板 clipboard.write(data) Electron 写入提供的文本和HTML。

一个通道连接双方: notion:clipboard:write

预加载脚本和主进程使用同一个通道。解析包装函数后,就能看出这些名称背后实际调用的 Electron 函数。

复制操作去哪里?

页面可以调用名为 clipboard.write 的方法。它在预加载脚本中的实现是一个包装函数。关键问题是:它发送了什么,又由哪段代码接收?

你的编程助手

使用REA跟踪Notion如何将文本和HTML复制到系统剪贴板。 在每个步骤显示代码。

一个示例提示与Notion的本地应用程序包可供你的编程助手.

REA查找从哪里开始阅读

用 REA 分析标签页的预加载脚本,可以找到暴露给页面的 API,以及发送 IPC 请求的调用。两个结果都给出了在压缩代码中的位置:

REA 分析结果 → 两个源码位置

公开页面API
api_key: "__electronApi"
api_status: "dynamic"
source: preload.js:1:22764–22828
渲染器IPC调用
operation: "invoke"
channel_expression: "e"
source: preload.js:1:6486–6522
这是新一次 analyze_javascript_application 分析结果中的部分字段,来自 Notion 标签页的预加载脚本,使用 REA 4.1.0 记录。位置对应原始压缩包中的行号和从零开始的列号。

助手打开这些位置,在组装的 API 中找到剪贴板成员,再读取 invokerInMain。调用处传入 notion:clipboard:write,让助手知道要在主进程代码中搜索哪个名称。下面四段代码展示这条调用链。

四段简短代码

从打包的JavaScript简化,具有可读的变量名称。 不相关的成员被省略。

  1. 从公开到页面的API开始

    tab_browser_view/preload.js · API 成员
    const clipboardApi = {
      write: invokerInMain("notion:clipboard:write")
    };
    
    // Included in the API exposed through contextBridge:
    // window.__electronApi.clipboard

    此成员提供通道名称。 的实施 invokerInMain 告诉我们请求如何离开页面的进程。

  2. 解析预加载的包装器

    tab_browser_view/preload.js · IPC 辅助函数
    function invokerInMain(channel) {
      return (...args) => invokeInMainAndReturnResult(channel, ...args);
    }
    
    function invokeInMainAndReturnResult(channel, ...args) {
      return ipcRenderer.invoke(channel, ...args);
    }

    返回的函数通过 Electron 的请求/响应 IPC 转发参数。接下来可以在主进程中查找 notion:clipboard:write。

  3. 检查接收器是如何注册的

    主要/index.js · 处理程序助手
    const handleRequestFromRenderer = {
      addListener(channel, handler) {
        ipcMain.handle(channel, handler);
      }
    };

    主进程使用另一个包装器。 其 addListener 方法注册一个 ipcMain.handle 那个频道的接收器。

  4. 查找写入及其发件人检查

    主要/index.js · 剪贴板接收器
    handleRequestFromRenderer.addListener(
      "notion:clipboard:write",
      (event, data) =>
        isNotionWebContents(event.sender)
          ? clipboard.write(data)
          : undefined
    );

    这就是调用的终点。处理函数检查发送方后,把收到的数据交给 Electron 的剪贴板 API。通道名和两个解析后的包装函数,把暴露给页面的方法连接到了这里。

复制的块如何保持其结构?

Notion 的网页编辑器通过 electronApi.clipboard.write 发送文本和 HTML。它在本地保留内容块数据,再在复制的 HTML 中放入一个 ID,将二者关联起来。

保存的块数据的HTML链接

发送到系统剪贴板
text: "Project notes"
html: "<p>Project notes</p>"
    + "<!-- notionvc: <copy-id> -->"
Notion本地保存
id: <copy-id>
mimeTypes:
  text/_notion-blocks-v3-production
    → structured block data
  1. 读取HTML ID
  2. 查找相同的本地ID
  3. 恢复Notion块数据
文本和 ID 为示意值。已记录的模块测试显示:ID 匹配时可以取回内容块数据;条目失效时只返回文本和 HTML。

在粘贴时,Notion查找HTML ID并根据保存的复制进行检查。 匹配的Id恢复所述块数据。

从2026年7月13日缓存的web资产中记录的剪贴板格式。

更多示例和来源

另一个例子:Markdown 表格

Notion将Markdown解析为块数据。 此示例显示表如何成为标题和数据行。

通过Notion的Markdown解析器的表格

Markdown 输入
| A | B |
| - | - |
| 1 | 2 |
结构化解析器输出 · 摘录
table · headerRow: true
  table_row
    cells: ["A"], ["B"]
  table_row
    cells: ["1"], ["2"]
从提取的Markdown解析器记录输出。 表变为行和单元格。
另一条路径:脱机SQLite连接

Notion 的渲染进程通过 IPC 请求 getSqliteMeta。主进程创建消息通道,把一个端口交给渲染进程,另一个交给 SQLite 工具进程。后续的语句批次和结果直接通过这个端口传输。

追踪这次交接,就能知道数据库操作在哪个进程执行,也能解释为什么只跟踪最初的 IPC 请求会漏掉后续通信。

资料来源及如何阅读摘录

剪贴板桥接代码来自 Notion Desktop 7.6.1 的 app.asar,包括标签页预加载脚本和 main/index.js。节选中展开了压缩后的变量名,并省略无关代码;发送方检查辅助函数展示了底层的 isNotionWebContents 调用。

虚拟剪贴板和 Markdown 示例来自 2026 年 7 月 13 日缓存的网页资源,包括 747001、58169、736810 和 422457 模块。这些资源有独立版本。保存的测试执行提取出的模块和小型测试程序;输出反映的是这些测试,没有记录桌面 UI 操作。

这里的 REA 结果是对保存的标签页预加载脚本进行的专项分析,使用已发布的 REA 4.1.0 记录。它定位了 API 和调用操作。读取包中的包装函数实现后,就能把剪贴板成员与匹配的接收方连接起来。

Markdown 示例使用解析器模块 422457。保存的模块测试显示,页面和聊天过滤器都会保留这张表格。粘贴/导入、结构化标记和 AI/聊天解析器各自有不同入口与中间格式。

顶部