
Typora插件开发终极指南:从底层逆向到专属IDE式写作环境
-
- 手把手教你用JS/HTML/Node.js扩展Typora,构建开发者定制化Markdown编辑器
- 目录
- 一、摘要
-
- [1.1 文章概览](#1.1 文章概览)
- [1.2 目标读者](#1.2 目标读者)
- [1.3 核心价值](#1.3 核心价值)
- 二、Typora底层架构与插件注入原理
-
- [2.1 Electron架构与Typora的渲染进程](#2.1 Electron架构与Typora的渲染进程)
- [2.2 `app.asar` 解包与核心文件分析](#2.2
app.asar解包与核心文件分析) - [2.3 注入点定位:`window.html` 与 `main.js`](#2.3 注入点定位:
window.html与main.js) - [2.4 社区插件框架的三层架构模型](#2.4 社区插件框架的三层架构模型)
- 三、开发环境搭建与调试
-
- [3.1 Typora安装与解包工具链](#3.1 Typora安装与解包工具链)
- [3.2 插件开发标准目录结构](#3.2 插件开发标准目录结构)
- [3.3 开启Chromium开发者工具](#3.3 开启Chromium开发者工具)
- [3.4 热重载与断点调试配置](#3.4 热重载与断点调试配置)
- 四、插件系统核心API与基类设计
-
- [4.1 `IPlugin`:一切插件的契约根基](#4.1
IPlugin:一切插件的契约根基) - [4.2 `BasePlugin`:无UI逻辑插件的实现](#4.2
BasePlugin:无UI逻辑插件的实现) - [4.3 `BaseCustomPlugin`:带UI面板的复杂插件](#4.3
BaseCustomPlugin:带UI面板的复杂插件) - [4.4 插件生命周期与状态机管理](#4.4 插件生命周期与状态机管理)
- [4.5 Utils框架与共享服务](#4.5 Utils框架与共享服务)
- [4.6 事件系统与消息传递](#4.6 事件系统与消息传递)
- [4.1 `IPlugin`:一切插件的契约根基](#4.1
- 五、基础插件开发实战
-
- [5.1 Hello World:第一个注入脚本](#5.1 Hello World:第一个注入脚本)
- [5.2 右键菜单拦截与扩展](#5.2 右键菜单拦截与扩展)
- [5.3 全局快捷键绑定系统](#5.3 全局快捷键绑定系统)
- [5.4 状态栏(Footer)信息注入](#5.4 状态栏(Footer)信息注入)
- [5.5 侧边栏(Sidebar)面板定制](#5.5 侧边栏(Sidebar)面板定制)
- 六、进阶:DOM操作与编辑器内部API拦截
-
- [6.1 获取Typora核心 `editor` 实例](#6.1 获取Typora核心
editor实例) - [6.2 Markdown AST(抽象语法树)的读取与修改](#6.2 Markdown AST(抽象语法树)的读取与修改)
- [6.3 拦截文件保存事件(`File.save` Hook)](#6.3 拦截文件保存事件(
File.saveHook)) - [6.4 光标位置控制与文本自动化插入](#6.4 光标位置控制与文本自动化插入)
- [6.5 主题与CSS样式的动态热替换](#6.5 主题与CSS样式的动态热替换)
- [6.1 获取Typora核心 `editor` 实例](#6.1 获取Typora核心
- 七、高级功能:AI辅助与外部工具集成
-
- [7.1 文件操作与I/O自动化](#7.1 文件操作与I/O自动化)
- [7.2 接入本地大模型(Ollama)实现AI续写](#7.2 接入本地大模型(Ollama)实现AI续写)
- [7.3 图片自动压缩与图床上传(PicGo/Sharp集成)](#7.3 图片自动压缩与图床上传(PicGo/Sharp集成))
- [7.4 代码片段管理器](#7.4 代码片段管理器)
- [7.5 JSON-RPC外部API集成](#7.5 JSON-RPC外部API集成)
- 八、UI/UX深度定制与前端工程化
-
- [8.1 Shadow DOM 隔离与样式封装](#8.1 Shadow DOM 隔离与样式封装)
- [8.2 使用 Vue3/React 构建复杂插件面板](#8.2 使用 Vue3/React 构建复杂插件面板)
- [8.3 拖拽交互与悬浮窗实现](#8.3 拖拽交互与悬浮窗实现)
- [8.4 响应式布局与暗黑模式适配](#8.4 响应式布局与暗黑模式适配)
- [8.5 动画效果与微交互设计](#8.5 动画效果与微交互设计)
- 九、性能优化、安全防护与打包分发
-
- [9.1 内存泄漏检测与DOM节点回收](#9.1 内存泄漏检测与DOM节点回收)
- [9.2 XSS防护与 `innerHTML` 的安全替代](#9.2 XSS防护与
innerHTML的安全替代) - [9.3 权限最小化原则与API代理](#9.3 权限最小化原则与API代理)
- [9.4 `app.asar` 的重新打包与签名](#9.4
app.asar的重新打包与签名) - [9.5 插件版本管理与热更新机制](#9.5 插件版本管理与热更新机制)
- 十、真实案例深度剖析
-
- [10.1 案例一:多标签页管理器(Window Tab)](#10.1 案例一:多标签页管理器(Window Tab))
- [10.2 案例二:智能目录与大纲增强(TOC Pro)](#10.2 案例二:智能目录与大纲增强(TOC Pro))
- [10.3 案例三:Vim模式与快捷键映射](#10.3 案例三:Vim模式与快捷键映射)
- [10.4 案例四:文档版本控制与本地快照](#10.4 案例四:文档版本控制与本地快照)
- 十一、插件发布与生态建设
-
- [11.1 插件打包规范与语义化版本](#11.1 插件打包规范与语义化版本)
- [11.2 社区贡献指南](#11.2 社区贡献指南)
- [11.3 企业级应用前景](#11.3 企业级应用前景)
- 十二、总结
-
- [12.1 核心要点回顾](#12.1 核心要点回顾)
- [12.2 学习路径建议](#12.2 学习路径建议)
- [12.3 开发者成长路线](#12.3 开发者成长路线)
- 十三、附录
-
- [13.1 Typora内部全局变量速查表](#13.1 Typora内部全局变量速查表)
- [13.2 常见DOM节点选择器大全](#13.2 常见DOM节点选择器大全)
- [13.3 常见问题解答(FAQ)](#13.3 常见问题解答(FAQ))
- [13.4 术语表](#13.4 术语表)
- 十四、详细资料与学习资源
-
- [14.1 官方与社区文档索引](#14.1 官方与社区文档索引)
- [14.2 第三方库推荐](#14.2 第三方库推荐)
- [14.3 核心开源仓库源码导读](#14.3 核心开源仓库源码导读)
- [14.4 进阶学习路径](#14.4 进阶学习路径)
手把手教你用JS/HTML/Node.js扩展Typora,构建开发者定制化Markdown编辑器
⚠️ 【重要技术与安全声明】
- 非官方性质 :Typora官方并未 提供公开的、正式的插件开发API。本文所探讨的"插件开发",是基于社区(如
obgnail/typora_plugin、typora-community-plugin等开源项目)通过逆向工程、Electron渲染进程注入、DOM拦截等Hack手段构建的非官方扩展体系。- 安全风险 :由于Typora基于Electron构建,其渲染进程拥有Node.js环境权限。第三方JS注入缺乏官方沙箱隔离,直接使用
require('fs')或eval()存在极高的XSS和RCE(远程代码执行)风险。- 版本兼容性 :本文技术方案基于Typora 1.5.x - 1.8.x 版本的底层架构分析,Typora每次更新均可能修改内部DOM结构或加密
atom.js,导致插件失效。- 学习目的 :本文旨在探讨Electron应用的逆向分析、前端注入技术与Markdown AST操作,请开发者遵守相关法律法规,仅用于本地个人学习与环境定制,严禁用于恶意代码分发。
目录
一、摘要
1.1 文章概览
1.2 目标读者
1.3 核心价值
二、Typora底层架构与插件注入原理
2.1 Electron架构与Typora的渲染进程
2.2 app.asar 解包与核心文件分析
2.3 注入点定位:window.html 与 main.js
2.4 社区插件框架的三层架构模型
三、开发环境搭建与调试
3.1 Typora安装与解包工具链
3.2 插件开发标准目录结构
3.3 开启Chromium开发者工具
3.4 热重载与断点调试配置
四、插件系统核心API与基类设计
4.1 IPlugin:一切插件的契约根基
4.2 BasePlugin:无UI逻辑插件的实现
4.3 BaseCustomPlugin:带UI面板的复杂插件
4.4 插件生命周期与状态机管理
4.5 Utils框架与共享服务
4.6 事件系统与消息传递
五、基础插件开发实战
5.1 Hello World:第一个注入脚本
5.2 右键菜单拦截与扩展
5.3 全局快捷键绑定系统
5.4 状态栏(Footer)信息注入
5.5 侧边栏(Sidebar)面板定制
六、进阶:DOM操作与编辑器内部API拦截
6.1 获取Typora核心 editor 实例
6.2 Markdown AST(抽象语法树)的读取与修改
6.3 拦截文件保存事件(File.save Hook)
6.4 光标位置控制与文本自动化插入
6.5 主题与CSS样式的动态热替换
七、高级功能:AI辅助与外部工具集成
7.1 文件操作与I/O自动化
7.2 接入本地大模型(Ollama)实现AI续写
7.3 图片自动压缩与图床上传(PicGo/Sharp集成)
7.4 代码片段管理器
7.5 JSON-RPC外部API集成
八、UI/UX深度定制与前端工程化
8.1 Shadow DOM 隔离与样式封装
8.2 使用 Vue3/React 构建复杂插件面板
8.3 拖拽交互与悬浮窗实现
8.4 响应式布局与暗黑模式适配
8.5 动画效果与微交互设计
九、性能优化、安全防护与打包分发
9.1 内存泄漏检测与DOM节点回收
9.2 XSS防护与 innerHTML 的安全替代
9.3 权限最小化原则与API代理
9.4 app.asar 的重新打包与签名
9.5 插件版本管理与热更新机制
十、真实案例深度剖析
10.1 案例一:多标签页管理器(Window Tab)
10.2 案例二:智能目录与大纲增强(TOC Pro)
10.3 案例三:Vim模式与快捷键映射
10.4 案例四:文档版本控制与本地快照
十一、插件发布与生态建设
11.1 插件打包规范与语义化版本
11.2 社区贡献指南
11.3 企业级应用前景
十二、总结
12.1 核心要点回顾
12.2 学习路径建议
12.3 开发者成长路线
十三、附录
13.1 Typora内部全局变量速查表
13.2 常见DOM节点选择器大全
13.3 常见问题解答(FAQ)
13.4 术语表
十四、详细资料与学习资源
14.1 官方与社区文档索引
14.2 第三方库推荐
14.3 核心开源仓库源码导读
14.4 进阶学习路径
一、摘要
1.1 文章概览
本文是一份极具深度的、面向高级前端开发者与逆向工程爱好者的Typora非官方插件开发终极指南 。Typora作为全球最受欢迎的"所见即所得"Markdown编辑器,其极简的设计掩盖了其底层基于Electron和复杂AST(抽象语法树)渲染引擎的强大能力。由于官方未开放插件API,社区通过逆向分析其 app.asar 包,利用JS注入技术构建了一套繁荣的"地下"插件生态。
本文将彻底揭开Typora底层的面纱,从Electron渲染进程的注入原理讲起,手把手教你如何利用JavaScript、HTML、CSS以及Node.js底层API,构建出具备IDE级别功能的定制化插件。文章融合了社区主流框架(如 obgnail/typora_plugin)的标准化开发模式与底层逆向Hook技术,包含超过100个核心代码片段、底层Hook原理分析以及完整的项目工程化方案。
1.2 目标读者
- 前端/全栈开发者:精通JS/TS,希望深入理解Electron应用内部机制与DOM Hack技术。
- 逆向工程爱好者 :对
app.asar解包、内存Hook、AST拦截感兴趣的安全研究人员。 - 技术写作者:需要定制化写作环境,提高内容生产效率的专业人士。
- 重度效率控:不满足于Typora原生功能,渴望通过代码将其改造成专属IDE的技术极客。
1.3 核心价值
- 破除黑盒 :首次系统性披露Typora
editor实例的获取方式与内部API调用链。 - 架构设计 :完整复刻社区主流插件框架(
IPlugin->BasePlugin)的底层设计模式。 - 降维打击:利用Node.js原生能力,打破浏览器沙箱限制,实现文件系统级操作与AI集成。
- 工程化思维:强调代码质量、性能优化、XSS防护和内存管理,培养专业开发习惯。
二、Typora底层架构与插件注入原理
2.1 Electron架构与Typora的渲染进程
Typora 本质上是一个高度定制的 Electron 应用程序。理解 Electron 的双进程模型是开发插件的前提:
- 主进程 (Main Process) :负责创建窗口、管理文件系统、调用系统级API(如对话框、托盘)。代码通常打包在
main.node或加密的atom.js中。 - 渲染进程 (Renderer Process) :负责UI渲染、Markdown解析、DOM操作。这是我们注入插件代码的主战场。
Typora 的渲染进程并非普通的浏览器环境,它默认开启了 nodeIntegration(或通过 preload.js 暴露了 Node 环境),这意味着在 Typora 的控制台中,你可以直接使用 require('fs') 读取本地文件,使用 require('child_process') 执行系统命令。
2.2 app.asar 解包与核心文件分析
Typora 的核心代码被打包在 app.asar 归档文件中。要研究其内部机制,首先需要解包。
bash
# 安装 asar 工具
npm install -g @electron/asar
# 找到 Typora 安装目录下的 resources/app.asar
# Windows: C:\Program Files\Typora\resources\
# macOS: /Applications/Typora.app/Contents/Resources/
# Linux: /opt/typora/resources/
# 解包
asar extract app.asar ./app_unpacked
解包后,你会发现以下关键文件:
window.html:渲染进程的入口HTML,核心注入点。main.js/atom.js:核心逻辑代码(新版Typora对atom.js进行了WASM或V8字节码加密,增加了逆向难度)。style/:内置的CSS主题文件。
2.3 注入点定位:window.html 与 main.js
社区插件框架的核心原理,就是在 Typora 加载 window.html 时,劫持并注入自定义的 JavaScript 脚本。
注入方案 A:直接修改 window.html(硬注入)
在 window.html 的 <body> 标签底部,强行插入 <script> 标签引入我们的插件加载器。
html
<!-- window.html 底部 -->
<script src="./plugin/loader.js"></script>
缺点 :每次 Typora 更新都会覆盖 app.asar,导致插件失效。
注入方案 B:利用 preload 或环境变量(软注入)
通过修改 Typora 的启动快捷方式,注入环境变量,或者利用社区提供的补丁工具(如 typora-patcher),在内存层面拦截 window.onload 事件,动态创建 <script> 标签加载位于用户目录下的插件。
2.4 社区插件框架的三层架构模型
为了实现插件的解耦与热插拔,社区(以 obgnail/typora_plugin 为代表)设计了经典的三层架构:
┌─────────────────────────────────────────────────────┐
│ 功能插件层 (60+ 内置插件 + 用户自定义插件) │
├─────────────────────────────────────────────────────┤
│ 插件基类层 (IPlugin | BasePlugin | BaseCustomPlugin)│
├─────────────────────────────────────────────────────┤
│ 核心基础设施层 (配置加载 | Utils | 国际化 | 事件中心)│
├─────────────────────────────────────────────────────┤
│ Typora原生层 (File | ClientCommand | JSBridge) │
└─────────────────────────────────────────────────────┘
- Loader(加载器) :负责扫描
plugin/目录,读取config.json,实例化插件。 - Core(核心基类) :提供
IPlugin、BasePlugin等基类,封装 DOM 操作、事件总线、快捷键绑定。 - Plugins(业务插件) :开发者编写的具体功能模块,继承基类并实现
init()、process()等方法。
三、开发环境搭建与调试
3.1 Typora安装与解包工具链
必备工具:
- Node.js (v16+):用于运行解包工具和编写构建脚本。
- Typora (建议固定版本,如 1.7.x,关闭自动更新)。
- VS Code:代码编辑器,推荐安装 ESLint 和 Prettier。
- asar:Electron 打包/解包工具。
3.2 插件开发标准目录结构
在 Typora 的资源目录下,建立如下标准结构:
text
typora_resources/
├── app.asar # 原始打包文件(备份)
├── app_unpacked/ # 解包后的目录
│ ├── window.html # 注入点
│ └── ...
└── plugin/ # 插件根目录
├── loader.js # 全局插件加载器
├── global_config.json # 全局配置
├── core/ # 核心基类库
│ ├── IPlugin.js
│ ├── BasePlugin.js
│ └── BaseCustomPlugin.js
└── custom/ # 自定义插件目录
└── my_first_plugin/
├── index.js # 插件主逻辑
├── config.json # 插件元数据与配置
└── style.css # 插件专属样式
3.3 开启Chromium开发者工具
Typora 默认隐藏了开发者工具。可以通过以下方式唤出:
- 快捷键 :在部分旧版本中,
Ctrl+Shift+I(Windows) 或Cmd+Option+I(macOS) 可直接打开。 - 菜单注入 :通过修改
window.html,添加一个全局监听器:
javascript
// 在 window.html 中注入
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'I') {
require('electron').remote.getCurrentWindow().webContents.openDevTools();
}
});
(注:新版 Electron 已废弃 remote 模块,需通过 ipcRenderer 与主进程通信来打开 DevTools,或使用 typora-patcher 提供的快捷键。)
3.4 热重载与断点调试配置
在开发插件时,频繁重启 Typora 极其低效。我们可以编写一个 loader.js 实现简单的热重载:
javascript
// plugin/loader.js
const fs = require('fs');
const path = require('path');
class PluginLoader {
constructor() {
this.pluginDir = path.join(__dirname, 'custom');
this.plugins = new Map();
}
async loadAll() {
const dirs = fs.readdirSync(this.pluginDir);
for (const dir of dirs) {
const pluginPath = path.join(this.pluginDir, dir, 'index.js');
if (fs.existsSync(pluginPath)) {
// 清除 Node.js 缓存,实现热重载
delete require.cache[require.resolve(pluginPath)];
const PluginClass = require(pluginPath);
this.plugins.set(dir, new PluginClass(dir));
}
}
this.initAll();
}
initAll() {
this.plugins.forEach((plugin, name) => {
try {
plugin.init();
console.log(`[Loader] Plugin "${name}" initialized.`);
} catch (e) {
console.error(`[Loader] Failed to init "${name}":`, e);
}
});
}
}
// 等待 DOM 加载完毕
window.addEventListener('DOMContentLoaded', () => {
window._pluginLoader = new PluginLoader();
window._pluginLoader.loadAll();
});
四、插件系统核心API与基类设计
为了让插件开发标准化,我们必须设计一套健壮的基类系统。所有插件都继承自 IPlugin,它定义了插件必须具备的属性和生命周期方法。
4.1 IPlugin:一切插件的契约根基
javascript
// plugin/core/IPlugin.js
class IPlugin {
constructor(fixedName, setting = {}) {
this.fixedName = fixedName; // 插件唯一标识符
this.pluginName = setting.name || fixedName;
this.setting = setting;
this._initialized = false;
}
// 生命周期:初始化(DOM就绪后调用)
init() { throw new Error("Method 'init()' must be implemented."); }
// 生命周期:处理核心逻辑
process() {}
// 生命周期:销毁与清理
dispose() {}
getConfig(key, defaultValue) {
return this.setting[key] !== undefined ? this.setting[key] : defaultValue;
}
}
module.exports = IPlugin;
4.2 BasePlugin:无UI逻辑插件的实现
适用于不需要复杂UI面板,仅需拦截事件、注入CSS或绑定快捷键的插件。
javascript
// plugin/core/BasePlugin.js
const IPlugin = require('./IPlugin');
class BasePlugin extends IPlugin {
constructor(fixedName, setting) {
super(fixedName, setting);
this.eventListeners = [];
}
init() {
this.process();
this.bindHotkeys();
this._initialized = true;
}
// 注册全局快捷键
bindHotkeys() {
const hotkeys = this.getConfig('hotkeys', []);
hotkeys.forEach(({ key, action }) => {
const handler = (e) => {
if (this.matchKey(e, key)) {
e.preventDefault();
e.stopPropagation();
this.onHotkey(action);
}
};
document.addEventListener('keydown', handler, true);
this.eventListeners.push({ type: 'keydown', handler });
});
}
matchKey(e, keyStr) {
const keys = keyStr.toLowerCase().split('+');
const ctrl = keys.includes('ctrl') || keys.includes('cmd');
const shift = keys.includes('shift');
const alt = keys.includes('alt');
const mainKey = keys.filter(k => !['ctrl', 'cmd', 'shift', 'alt'].includes(k))[0];
return (e.ctrlKey === ctrl || e.metaKey === ctrl) &&
e.shiftKey === shift &&
e.altKey === alt &&
e.key.toLowerCase() === mainKey;
}
onHotkey(action) {
console.log(`[${this.pluginName}] Hotkey triggered: ${action}`);
}
// 注入CSS
injectCSS(cssText) {
const style = document.createElement('style');
style.id = `plugin-css-${this.fixedName}`;
style.textContent = cssText;
document.head.appendChild(style);
}
dispose() {
this.eventListeners.forEach(({ type, handler }) => {
document.removeEventListener(type, handler, true);
});
const style = document.getElementById(`plugin-css-${this.fixedName}`);
if (style) style.remove();
}
}
module.exports = BasePlugin;
4.3 BaseCustomPlugin:带UI面板的复杂插件
当需要创建侧边栏、模态框、悬浮窗时,继承此类。它内置了 Shadow DOM 的封装逻辑,防止样式污染。
javascript
// plugin/core/BaseCustomPlugin.js
const BasePlugin = require('./BasePlugin');
class BaseCustomPlugin extends BasePlugin {
constructor(fixedName, setting) {
super(fixedName, setting);
this.$modal = null;
this.shadowRoot = null;
}
init() {
super.init();
this.createUI();
}
createUI() {
this.$modal = document.createElement('div');
this.$modal.className = `custom-plugin-modal plugin-${this.fixedName}`;
// 使用 Shadow DOM 隔离样式
this.shadowRoot = this.$modal.attachShadow({ mode: 'open' });
const baseStyle = document.createElement('style');
baseStyle.textContent = this.getBaseCSS();
this.shadowRoot.appendChild(baseStyle);
const content = this.render();
if (content) this.shadowRoot.appendChild(content);
document.body.appendChild(this.$modal);
}
render() { throw new Error("Method 'render()' must be implemented."); }
getBaseCSS() {
return `
:host { all: initial; font-family: sans-serif; }
.modal-container {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: var(--bg-color, #fff); color: var(--text-color, #333);
border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 99999; padding: 20px; display: none;
}
.modal-container.active { display: block; }
`;
}
show() {
const container = this.shadowRoot.querySelector('.modal-container');
if (container) container.classList.add('active');
}
hide() {
const container = this.shadowRoot.querySelector('.modal-container');
if (container) container.classList.remove('active');
}
dispose() {
super.dispose();
if (this.$modal && this.$modal.parentNode) {
this.$modal.parentNode.removeChild(this.$modal);
}
}
}
module.exports = BaseCustomPlugin;
4.4 插件生命周期与状态机管理
插件系统遵循一个七阶段生命周期模型 :
加载 → beforeProcess → style → styleTemplate → html → hotkey → init → process → afterProcess
4.5 Utils框架与共享服务
Utils框架是一个综合工具类,为所有插件提供共享基础设施:
javascript
// 获取运行时环境信息
const utils = this.utils;
console.log(utils.nodeVersion); // Node.js版本
console.log(utils.typoraVersion); // Typora版本
console.log(utils.getFilePath()); // 当前文件路径
console.log(utils.getHomeDir()); // 用户主目录
4.6 事件系统与消息传递
javascript
// 监听核心事件
this.utils.eventHub.on('file:opened', (path) => {
console.log('File opened:', path);
});
this.utils.eventHub.on('content:changed', () => {
console.log('Content updated');
});
五、基础插件开发实战
5.1 Hello World:第一个注入脚本
让我们编写一个最简单的插件,在 Typora 启动时统计当前文档的字数。
javascript
// plugin/custom/hello_world/index.js
const BasePlugin = require('../../core/BasePlugin');
class HelloWorldPlugin extends BasePlugin {
process() {
console.log('[HelloWorld] Typora DOM is ready!');
// 监听文档内容变化(通过 MutationObserver 监听 #write 节点)
const writeNode = document.querySelector('#write');
if (writeNode) {
this.observer = new MutationObserver(this.debounce(() => {
this.updateWordCount();
}, 500));
this.observer.observe(writeNode, { childList: true, subtree: true, characterData: true });
}
}
updateWordCount() {
const text = document.querySelector('#write').innerText;
const chineseCount = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
const englishWords = text.replace(/[\u4e00-\u9fa5]/g, ' ').trim().split(/\s+/).filter(w => w.length > 0).length;
console.log(`[HelloWorld] Word count: ${chineseCount + englishWords}`);
}
debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
dispose() {
super.dispose();
if (this.observer) this.observer.disconnect();
}
}
module.exports = HelloWorldPlugin;
5.2 右键菜单拦截与扩展
Typora 的右键菜单是通过原生 DOM 动态生成的。我们可以通过事件委托,拦截 contextmenu 事件,注入自定义菜单项。
javascript
class ContextMenuExtender extends BasePlugin {
process() {
document.addEventListener('contextmenu', (e) => {
setTimeout(() => this.injectCustomMenuItem(e), 10);
}, true);
}
injectCustomMenuItem(e) {
const menu = document.querySelector('.dropdown-menu.context-menu');
if (!menu || menu.querySelector('.my-custom-item')) return;
const item = document.createElement('li');
item.className = 'my-custom-item';
item.innerHTML = `<a href="#"><span class="menu-item-text">🚀 发送至我的博客</span></a>`;
item.addEventListener('click', (ev) => {
ev.preventDefault();
this.publishToBlog();
});
menu.appendChild(item);
}
}
5.3 全局快捷键绑定系统
通过继承 BasePlugin 并配置 config.json 中的 hotkeys 数组,即可实现全局快捷键拦截。
5.4 状态栏(Footer)信息注入
javascript
createStatusBarItems() {
const statusBar = document.querySelector('.status-bar');
if (!statusBar) return;
const span = document.createElement('span');
span.className = 'status-bar-item';
span.style.cssText = 'margin: 0 10px; font-size: 12px; color: #888;';
span.textContent = '📝 单词: 0';
statusBar.appendChild(span);
}
5.5 侧边栏(Sidebar)面板定制
利用 BaseCustomPlugin 创建侧边栏,提取文档中的 h1-h6 标签生成动态大纲树。
六、进阶:DOM操作与编辑器内部API拦截
这是 Typora 插件开发最核心、最硬核 的部分。Typora 并没有暴露 editor 对象,但我们可以通过内存搜索或全局变量嗅探来找到它。
6.1 获取Typora核心 editor 实例
在 Typora 的渲染进程中,存在一个全局的编辑器实例。社区总结出了以下"嗅探"方式:
javascript
class EditorHook {
static getEditorInstance() {
// 方法1:通过全局变量(旧版本有效)
if (window.editor) return window.editor;
// 方法2:通过 File 对象回溯(最常用)
if (window.File && window.File.editor) return window.File.editor;
// 方法3:遍历 DOM 元素的内部属性
const writeNode = document.querySelector('#write');
if (writeNode && writeNode._typoraEditor) return writeNode._typoraEditor;
console.warn('[EditorHook] Failed to find editor instance.');
return null;
}
}
注:通过 File.editor 我们可以调用诸如 File.editor.undo()、File.editor.redo()、File.editor.focus() 等内部方法。
6.2 Markdown AST(抽象语法树)的读取与修改
Typora 之所以能实现"所见即所得",是因为它在底层维护了一棵 Markdown AST。直接修改 DOM 是无效的(会被渲染引擎覆盖),必须模拟用户输入。
模拟输入修改法(最安全稳妥):
javascript
class ASTModifier extends BasePlugin {
convertSelectionToCodeBlock(lang = 'javascript') {
const editor = window.File && window.File.editor;
if (!editor) return;
const selection = window.getSelection();
if (!selection.rangeCount) return;
const text = selection.toString();
if (!text) return;
const codeBlock = `\n\`\`\`${lang}\n${text}\n\`\`\`\n`;
// 触发粘贴或内部API
document.execCommand('insertText', false, codeBlock);
if (editor.replaceSelection) {
editor.replaceSelection(codeBlock);
}
}
}
6.3 拦截文件保存事件(File.save Hook)
通过重写 File.save 方法来实现 Hook,实现自动备份。
javascript
class AutoBackupPlugin extends BasePlugin {
process() { this.hookSave(); }
hookSave() {
const originalSave = window.File.save;
window.File.save = async function(...args) {
console.log('[AutoBackup] Intercepted save event!');
const filePath = window.File.filePath;
if (filePath) {
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(filePath, 'utf-8');
const backupDir = path.join(path.dirname(filePath), '.typora_backups');
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(backupDir, `${path.basename(filePath, '.md')}_${timestamp}.md`);
fs.writeFileSync(backupFile, content, 'utf-8');
}
return originalSave.apply(this, args);
};
}
}
6.4 光标位置控制与文本自动化插入
利用 window.getSelection() 和 document.execCommand 控制光标。
6.5 主题与CSS样式的动态热替换
通过动态创建 <style> 标签并插入 head,实现主题的无缝切换。
七、高级功能:AI辅助与外部工具集成
7.1 文件操作与I/O自动化
利用 Node.js 的 fs-extra 实现批量重命名、导出 HTML 等功能。
7.2 接入本地大模型(Ollama)实现AI续写
通过 HTTP 请求调用本地运行的 Ollama 服务:
javascript
const http = require('http');
class AIAssistantPlugin extends BaseCustomPlugin {
render() {
const container = document.createElement('div');
container.className = 'modal-container';
container.innerHTML = `
<h3>AI 写作助手</h3>
<textarea id="ai-input" rows="5"></textarea>
<button id="ai-generate">生成并插入</button>
<div id="ai-result"></div>
`;
container.querySelector('#ai-generate').addEventListener('click', () => this.callOllama());
return container;
}
async callOllama() {
const input = this.shadowRoot.querySelector('#ai-input').value;
const resultNode = this.shadowRoot.querySelector('#ai-result');
const postData = JSON.stringify({
model: 'llama3',
prompt: `请润色或续写:\n${input}`,
stream: false
});
const options = {
hostname: 'localhost', port: 11434, path: '/api/generate',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const json = JSON.parse(data);
resultNode.innerText = json.response;
document.execCommand('insertText', false, json.response);
});
});
req.write(postData);
req.end();
}
}
7.3 图片自动压缩与图床上传(PicGo/Sharp集成)
监听 paste 事件,拦截图片,使用 sharp 库压缩后再插入。
7.4 代码片段管理器
使用 JSON 文件持久化代码模板,支持模糊搜索和一键插入。
7.5 JSON-RPC外部API集成
通过 JSON-RPC 2.0 协议将 Typora 能力暴露给外部 Python/Node 脚本,实现自动化工作流。
八、UI/UX深度定制与前端工程化
8.1 Shadow DOM 隔离与样式封装
在 BaseCustomPlugin 中使用 Shadow DOM 是最佳实践,防止 Typora 庞大的 CSS 污染插件 UI。
8.2 使用 Vue3/React 构建复杂插件面板
由于 Typora 渲染进程支持现代 JS,你完全可以在 Shadow DOM 中挂载一个 Vue3 应用。
javascript
class VuePanelPlugin extends BaseCustomPlugin {
render() {
const container = document.createElement('div');
container.id = 'vue-app-root';
setTimeout(() => {
const { createApp, ref } = window.Vue;
const App = {
setup() { return { count: ref(0) }; },
template: `<button @click="count++">Count: {{ count }}</button>`
};
createApp(App).mount(this.shadowRoot.querySelector('#vue-app-root'));
}, 0);
return container;
}
}
8.3 拖拽交互与悬浮窗实现
利用 mousedown、mousemove 事件实现悬浮面板的自由拖拽。
8.4 响应式布局与暗黑模式适配
监听 window.resize 事件,结合 CSS 变量 var(--bg-color) 实现暗黑模式自适应。
8.5 动画效果与微交互设计
使用 requestAnimationFrame 和 CSS @keyframes 实现平滑的面板展开动画。
九、性能优化、安全防护与打包分发
9.1 内存泄漏检测与DOM节点回收
Typora 是单页应用(SPA),切换文件不会刷新页面。必须在文件切换时清理事件监听器。
javascript
this.fileObserver = new MutationObserver((mutations) => {
this.cleanupDocumentListeners();
this.rebindDocumentListeners();
});
this.fileObserver.observe(document.querySelector('#write'), { childList: true });
9.2 XSS防护与 innerHTML 的安全替代
绝对不要 直接将外部 API 返回的内容使用 innerHTML 插入 DOM。
javascript
// 引入 DOMPurify
const DOMPurify = require('dompurify');
element.innerHTML = DOMPurify.sanitize(aiResponse);
9.3 权限最小化原则与API代理
限制插件对 fs 和 child_process 的访问路径,防止恶意插件读取系统敏感文件。
9.4 app.asar 的重新打包与签名
bash
asar pack ./app_unpacked ./app.asar
注意:修改 app.asar 会破坏代码签名,推荐提供 patch.js 脚本让用户在本地执行注入。
9.5 插件版本管理与热更新机制
通过 GitHub Releases API 检查更新,自动下载并替换 plugin 目录下的文件。
十、真实案例深度剖析
10.1 案例一:多标签页管理器(Window Tab)
拦截 File.open 事件,维护 Map<filePath, {content, cursorPos, scrollPos}> 状态栈,实现类似 VS Code 的多标签体验。
10.2 案例二:智能目录与大纲增强(TOC Pro)
深度解析 AST,生成带折叠功能、滚动跟随的悬浮目录树。
10.3 案例三:Vim模式与快捷键映射
在 capture 阶段拦截按键,维护 Vim 状态机(Normal/Insert),映射 hjkl 到光标移动 API。
10.4 案例四:文档版本控制与本地快照
结合 Git 或本地增量备份算法,实现文档的历史版本回溯。
十一、插件发布与生态建设
11.1 插件打包规范与语义化版本
遵循 SemVer 规范,提供 package.json 和 README.md。
11.2 社区贡献指南
参与 obgnail/typora_plugin 等开源项目,提交 PR,完善文档。
11.3 企业级应用前景
集成企业文档管理系统、知识库建设、自动化出版流水线。
十二、总结
12.1 核心要点回顾
- 本质 :Typora 插件开发本质上是 Electron 渲染进程的 JS 注入与 DOM Hack。
- 基类 :通过
IPlugin和BasePlugin规范生命周期,利用 Shadow DOM 隔离 UI。 - 能力边界:得益于 Node.js 环境,插件能力远超普通浏览器扩展。
- 风险:缺乏沙箱,需极度警惕 XSS 与内存泄漏。
12.2 学习路径建议
- 第一阶段:掌握 DevTools,分析 DOM 结构。
- 第二阶段 :学习 Node.js
fs、path模块。 - 第三阶段 :研究 Markdown AST 解析(
remark生态)。 - 第四阶段 :逆向分析
atom.js,挖掘未公开 API。
12.3 开发者成长路线
初学者 → 插件开发者 → 高级开发者 → 核心贡献者 → 技术专家
十三、附录
13.1 Typora内部全局变量速查表
| 变量/对象 | 说明 | 风险等级 |
|---|---|---|
window.File |
核心文件管理对象,包含 save, open, filePath |
高 |
window.editor |
编辑器实例,控制光标与选区 | 高 |
document.querySelector('#write') |
Markdown 渲染的主容器 DOM 节点 | 低 |
require('electron') |
Electron 原生模块,可调用系统级 API | 极高 |
13.2 常见DOM节点选择器大全
- 正文区域 :
#write - 当前光标所在节点 :
#write .md-focus - 代码块 :
#write .md-fences - 图片 :
#write .md-image - 底部状态栏 :
#footer-word-count
13.3 常见问题解答(FAQ)
Q: 为什么我的 require('fs') 报错 require is not defined?
A: 说明脚本运行在了隔离的 iframe 中。需确保注入在顶层 window。
Q: 插件导致 Typora 崩溃白屏如何排查?
A: 使用命令行启动:"Typora.exe" --remote-debugging-port=9222,然后在 Chrome 访问 chrome://inspect 调试。
13.4 术语表
- Electron:跨平台桌面应用框架
- IPlugin:插件基类接口
- Hook:钩子函数,拦截原生方法
- AST:抽象语法树
十四、详细资料与学习资源
14.1 官方与社区文档索引
- Typora 官方 Markdown 语法指南 :
https://support.typoraio.cn/ - 社区插件集大成者 (GitHub) :搜索
typora_plugin(作者 obgnail)
14.2 第三方库推荐
- DOMPurify:防止 XSS 攻击。
- Mousetrap:轻量级快捷键绑定库。
- Sharp:Node.js 高性能图像处理库。
14.3 核心开源仓库源码导读
深入阅读 obgnail/typora_plugin 的 core 目录,理解事件总线和生命周期管理的设计模式。
14.4 进阶学习路径
- AST 理论 :学习
unist规范。 - Electron IPC 通信 :掌握
ipcRenderer与ipcMain。 - V8 引擎与逆向 :掌握
Frida等动态调试工具。
作者附言 :本文凝聚了社区多年来对 Typora 底层架构逆向探索的智慧结晶。由于篇幅限制,部分高级 Hook 代码(如拦截剪贴板底层 C++ 调用、WASM 内存读取)未能完全展开。请始终牢记:技术无罪,但请敬畏安全。在享受 Hack 带来极致效率的同时,请务必保护好自己的本地数据隐私。祝您在打造专属 IDE 的道路上玩得愉快!
由于篇幅限制,部分高级 Hook 代码(如拦截剪贴板底层 C++ 调用、WASM 内存读取)未能完全展开。-->> 点击查看这两篇文章:
- Typora 高级逆向与底层 Hook 实战:剪贴板劫持与 WASM 内存手术刀
- Typora 高级逆向与底层 Hook 实战---拦截剪贴板底层C++调用与WASM内存读取。