
Typora插件开发指南:打造专属IDE式写作环境
- 底层原理、逆向分析、从零构建插件框架、Hook内部API等硬核技术
- 手把手教你用JS/HTML扩展Typora,构建开发者定制化Markdown编辑器
- 目录
- 一、摘要
-
- [1.1 文章概览](#1.1 文章概览)
- [1.2 目标读者](#1.2 目标读者)
- [1.3 核心价值](#1.3 核心价值)
- 二、Typora插件系统概述
-
- [2.1 Typora插件架构解析](#2.1 Typora插件架构解析)
- [2.2 插件系统核心概念](#2.2 插件系统核心概念)
-
- [2.2.1 插件基类体系](#2.2.1 插件基类体系)
- [2.2.2 IPlugin接口](#2.2.2 IPlugin接口)
- [2.2.3 BaseCustomPlugin选择器模式](#2.2.3 BaseCustomPlugin选择器模式)
- [2.3 插件生命周期管理](#2.3 插件生命周期管理)
- [2.4 插件安全机制](#2.4 插件安全机制)
- 三、开发环境搭建
-
- [3.1 Typora安装与配置](#3.1 Typora安装与配置)
-
- [3.1.1 版本要求](#3.1.1 版本要求)
- [3.1.2 获取插件框架源码](#3.1.2 获取插件框架源码)
- [3.1.3 安装插件框架](#3.1.3 安装插件框架)
- [3.2 插件开发目录结构](#3.2 插件开发目录结构)
-
- [3.2.1 标准目录结构](#3.2.1 标准目录结构)
- [3.2.2 自定义插件目录](#3.2.2 自定义插件目录)
- [3.3 开发工具链配置](#3.3 开发工具链配置)
-
- [3.3.1 基础工具](#3.3.1 基础工具)
- [3.3.2 推荐VS Code扩展](#3.3.2 推荐VS Code扩展)
- [3.4 调试环境设置](#3.4 调试环境设置)
-
- [3.4.1 开启调试模式](#3.4.1 开启调试模式)
- [3.4.2 打开开发者工具](#3.4.2 打开开发者工具)
- [3.4.3 控制台调试](#3.4.3 控制台调试)
- 四、插件核心API详解
-
- [4.1 IPlugin基类深度剖析](#4.1 IPlugin基类深度剖析)
-
- [4.1.1 构造函数](#4.1.1 构造函数)
- [4.1.2 生命周期方法详解](#4.1.2 生命周期方法详解)
- [4.2 BasePlugin扩展类实战](#4.2 BasePlugin扩展类实战)
- [4.3 BaseCustomPlugin选择器模式](#4.3 BaseCustomPlugin选择器模式)
-
- [4.3.1 高级选择器模式](#4.3.1 高级选择器模式)
- [4.4 Utils框架与共享服务](#4.4 Utils框架与共享服务)
-
- [4.4.1 系统信息](#4.4.1 系统信息)
- [4.4.2 插件管理](#4.4.2 插件管理)
- [4.4.3 文件系统操作](#4.4.3 文件系统操作)
- [4.4.4 DOM操作](#4.4.4 DOM操作)
- [4.5 事件系统与消息传递](#4.5 事件系统与消息传递)
-
- [4.5.1 事件中心](#4.5.1 事件中心)
- [4.5.2 核心事件](#4.5.2 核心事件)
- [4.5.3 动态动作系统](#4.5.3 动态动作系统)
- [4.6 DOM操作与UI扩展](#4.6 DOM操作与UI扩展)
-
- [4.6.1 注入自定义UI](#4.6.1 注入自定义UI)
- [4.6.2 操作按钮插件](#4.6.2 操作按钮插件)
- 五、基础插件开发实战
-
- [5.1 Hello World插件](#5.1 Hello World插件)
-
- [5.1.1 创建插件文件](#5.1.1 创建插件文件)
- [5.1.2 配置插件](#5.1.2 配置插件)
- [5.1.3 测试插件](#5.1.3 测试插件)
- [5.2 菜单扩展插件](#5.2 菜单扩展插件)
-
- [5.2.1 添加自定义菜单项](#5.2.1 添加自定义菜单项)
- [5.3 快捷键绑定插件](#5.3 快捷键绑定插件)
-
- [5.3.1 使用hotkey方法注册快捷键](#5.3.1 使用hotkey方法注册快捷键)
- [5.3.2 使用HotkeysPlugin配置快捷键](#5.3.2 使用HotkeysPlugin配置快捷键)
- [5.4 状态栏扩展插件](#5.4 状态栏扩展插件)
-
- [5.4.1 添加状态栏信息](#5.4.1 添加状态栏信息)
- [5.5 侧边栏面板插件](#5.5 侧边栏面板插件)
-
- [5.5.1 创建侧边栏面板](#5.5.1 创建侧边栏面板)
- 六、高级功能插件开发
-
- [6.1 文件操作与I/O插件](#6.1 文件操作与I/O插件)
-
- [6.1.1 文件系统访问](#6.1.1 文件系统访问)
- [6.2 代码片段管理插件](#6.2 代码片段管理插件)
-
- [6.2.1 代码片段数据结构](#6.2.1 代码片段数据结构)
- [6.3 实时预览增强插件](#6.3 实时预览增强插件)
- [6.4 语法高亮扩展插件](#6.4 语法高亮扩展插件)
- [6.5 AI辅助写作插件](#6.5 AI辅助写作插件)
-
- [6.5.1 AI插件基础结构](#6.5.1 AI插件基础结构)
- [6.6 JSON-RPC外部API集成](#6.6 JSON-RPC外部API集成)
-
- [6.6.1 JSON-RPC API方法](#6.6.1 JSON-RPC API方法)
- [6.6.2 外部调用示例](#6.6.2 外部调用示例)
- [6.6.3 安全警告](#6.6.3 安全警告)
- 七、UI/UX深度定制
-
- [7.1 自定义主题与样式](#7.1 自定义主题与样式)
-
- [7.1.1 通过custom.css定制](#7.1.1 通过custom.css定制)
- [7.1.2 通过base.user.css定制](#7.1.2 通过base.user.css定制)
- [7.2 动态组件开发](#7.2 动态组件开发)
-
- [7.2.1 创建可交互组件](#7.2.1 创建可交互组件)
- [7.3 拖拽交互实现](#7.3 拖拽交互实现)
- [7.4 响应式布局设计](#7.4 响应式布局设计)
- [7.5 动画效果集成](#7.5 动画效果集成)
- 八、性能优化与调试
-
- [8.1 插件性能监控](#8.1 插件性能监控)
- [8.2 内存泄漏检测](#8.2 内存泄漏检测)
- [8.3 调试技巧与工具](#8.3 调试技巧与工具)
-
- [8.3.1 使用Chrome DevTools调试](#8.3.1 使用Chrome DevTools调试)
- [8.3.2 日志系统](#8.3.2 日志系统)
- [8.4 错误处理与日志](#8.4 错误处理与日志)
-
- [8.4.1 全局错误捕获](#8.4.1 全局错误捕获)
- [8.4.2 插件中使用错误处理](#8.4.2 插件中使用错误处理)
- [8.5 性能优化策略](#8.5 性能优化策略)
-
- [8.5.1 优化清单](#8.5.1 优化清单)
- [8.5.2 防抖/节流工具](#8.5.2 防抖/节流工具)
- 九、插件发布与分发
-
- [9.1 插件打包规范](#9.1 插件打包规范)
-
- [9.1.1 插件文件结构](#9.1.1 插件文件结构)
- [9.1.2 package.json配置](#9.1.2 package.json配置)
- [9.2 版本管理策略](#9.2 版本管理策略)
-
- [9.2.1 语义化版本](#9.2.1 语义化版本)
- [9.2.2 版本更新日志](#9.2.2 版本更新日志)
- [9.3 插件市场发布](#9.3 插件市场发布)
-
- [9.3.1 发布到GitHub](#9.3.1 发布到GitHub)
- [9.3.2 发布到社区](#9.3.2 发布到社区)
- [9.4 用户反馈收集](#9.4 用户反馈收集)
- [9.5 持续集成部署](#9.5 持续集成部署)
-
- [9.5.1 GitHub Actions配置](#9.5.1 GitHub Actions配置)
- 十、安全与最佳实践
-
- [10.1 安全编码规范](#10.1 安全编码规范)
-
- [10.1.1 输入验证](#10.1.1 输入验证)
- [10.1.2 安全配置](#10.1.2 安全配置)
- [10.2 权限最小化原则](#10.2 权限最小化原则)
-
- [10.2.1 权限声明](#10.2.1 权限声明)
- [10.3 XSS防护策略](#10.3 XSS防护策略)
-
- [10.3.1 内容安全策略](#10.3.1 内容安全策略)
- [10.3.2 安全DOM操作](#10.3.2 安全DOM操作)
- [10.4 数据隐私保护](#10.4 数据隐私保护)
-
- [10.4.1 敏感数据处理](#10.4.1 敏感数据处理)
- [10.5 代码审查要点](#10.5 代码审查要点)
-
- [10.5.1 审查清单](#10.5.1 审查清单)
- [10.5.2 代码质量检查](#10.5.2 代码质量检查)
- 十一、真实案例分析
-
- [11.1 代码片段管理器插件](#11.1 代码片段管理器插件)
-
- [11.1.1 功能需求](#11.1.1 功能需求)
- [11.1.2 技术实现](#11.1.2 技术实现)
- [11.2 实时协作编辑插件](#11.2 实时协作编辑插件)
-
- [11.2.1 架构设计](#11.2.1 架构设计)
- [11.3 智能目录生成插件](#11.3 智能目录生成插件)
-
- [11.3.1 功能实现](#11.3.1 功能实现)
- [11.4 图片优化处理插件](#11.4 图片优化处理插件)
-
- [11.4.1 功能实现](#11.4.1 功能实现)
- [11.5 多语言翻译插件](#11.5 多语言翻译插件)
-
- [11.5.1 功能实现](#11.5.1 功能实现)
- 十二、未来展望与生态
-
- [12.1 Typora插件生态现状](#12.1 Typora插件生态现状)
- [12.2 新版本API展望](#12.2 新版本API展望)
- [12.3 跨平台兼容性](#12.3 跨平台兼容性)
- [12.4 社区贡献指南](#12.4 社区贡献指南)
-
- [12.4.1 如何贡献](#12.4.1 如何贡献)
- [12.4.2 贡献规范](#12.4.2 贡献规范)
- [12.5 企业级应用前景](#12.5 企业级应用前景)
- 十三、总结
-
- [13.1 核心要点回顾](#13.1 核心要点回顾)
- [13.2 学习路径建议](#13.2 学习路径建议)
- [13.3 开发者成长路线](#13.3 开发者成长路线)
- 十四、附录
-
- [14.1 API参考手册](#14.1 API参考手册)
-
- [14.1.1 IPlugin生命周期方法](#14.1.1 IPlugin生命周期方法)
- [14.1.2 Utils框架核心方法](#14.1.2 Utils框架核心方法)
- [14.1.3 事件列表](#14.1.3 事件列表)
- [14.2 快捷键大全](#14.2 快捷键大全)
-
- [14.2.1 常用快捷键](#14.2.1 常用快捷键)
- [14.2.2 自定义快捷键配置](#14.2.2 自定义快捷键配置)
- [14.3 常见问题解答](#14.3 常见问题解答)
-
- [Q1: 插件安装后不生效?](#Q1: 插件安装后不生效?)
- [Q2: 如何调试插件?](#Q2: 如何调试插件?)
- [Q3: 插件更新后不生效?](#Q3: 插件更新后不生效?)
- [Q4: 如何获取当前文档内容?](#Q4: 如何获取当前文档内容?)
- [Q5: 如何插入文本到编辑器?](#Q5: 如何插入文本到编辑器?)
- [14.4 学习资源推荐](#14.4 学习资源推荐)
-
- [14.4.1 官方资源](#14.4.1 官方资源)
- [14.4.2 社区资源](#14.4.2 社区资源)
- [14.4.3 推荐阅读](#14.4.3 推荐阅读)
- [14.5 术语表](#14.5 术语表)
- 十五、详细资料
-
- [15.1 官方文档索引](#15.1 官方文档索引)
- [15.2 第三方库推荐](#15.2 第三方库推荐)
- [15.3 示例代码仓库](#15.3 示例代码仓库)
- [15.4 社区论坛资源](#15.4 社区论坛资源)
- [15.5 进阶学习路径](#15.5 进阶学习路径)
-
- [15.5.1 前端技术栈](#15.5.1 前端技术栈)
- [15.5.2 桌面应用开发](#15.5.2 桌面应用开发)
- [15.5.3 插件工程化](#15.5.3 插件工程化)
底层原理、逆向分析、从零构建插件框架、Hook内部API等硬核技术
手把手教你用JS/HTML扩展Typora,构建开发者定制化Markdown编辑器
目录
一、摘要
- 1.1 文章概览
- 1.2 目标读者
- 1.3 核心价值
二、Typora插件系统概述
- 2.1 Typora插件架构解析
- 2.2 插件系统核心概念
- 2.3 插件生命周期管理
- 2.4 插件安全机制
三、开发环境搭建
- 3.1 Typora安装与配置
- 3.2 插件开发目录结构
- 3.3 开发工具链配置
- 3.4 调试环境设置
四、插件核心API详解
- 4.1 IPlugin基类深度剖析
- 4.2 BasePlugin扩展类实战
- 4.3 BaseCustomPlugin选择器模式
- 4.4 Utils框架与共享服务
- 4.5 事件系统与消息传递
- 4.6 DOM操作与UI扩展
五、基础插件开发实战
- 5.1 Hello World插件
- 5.2 菜单扩展插件
- 5.3 快捷键绑定插件
- 5.4 状态栏扩展插件
- 5.5 侧边栏面板插件
六、高级功能插件开发
- 6.1 文件操作与I/O插件
- 6.2 代码片段管理插件
- 6.3 实时预览增强插件
- 6.4 语法高亮扩展插件
- 6.5 AI辅助写作插件
- 6.6 JSON-RPC外部API集成
七、UI/UX深度定制
- 7.1 自定义主题与样式
- 7.2 动态组件开发
- 7.3 拖拽交互实现
- 7.4 响应式布局设计
- 7.5 动画效果集成
八、性能优化与调试
- 8.1 插件性能监控
- 8.2 内存泄漏检测
- 8.3 调试技巧与工具
- 8.4 错误处理与日志
- 8.5 性能优化策略
九、插件发布与分发
- 9.1 插件打包规范
- 9.2 版本管理策略
- 9.3 插件市场发布
- 9.4 用户反馈收集
- 9.5 持续集成部署
十、安全与最佳实践
- 10.1 安全编码规范
- 10.2 权限最小化原则
- 10.3 XSS防护策略
- 10.4 数据隐私保护
- 10.5 代码审查要点
十一、真实案例分析
- 11.1 代码片段管理器插件
- 11.2 实时协作编辑插件
- 11.3 智能目录生成插件
- 11.4 图片优化处理插件
- 11.5 多语言翻译插件
十二、未来展望与生态
- 12.1 Typora插件生态现状
- 12.2 新版本API展望
- 12.3 跨平台兼容性
- 12.4 社区贡献指南
- 12.5 企业级应用前景
十三、总结
- 13.1 核心要点回顾
- 13.2 学习路径建议
- 13.3 开发者成长路线
十四、附录
- 14.1 API参考手册
- 14.2 快捷键大全
- 14.3 常见问题解答
- 14.4 学习资源推荐
- 14.5 术语表
十五、详细资料
- 15.1 官方文档索引
- 15.2 第三方库推荐
- 15.3 示例代码仓库
- 15.4 社区论坛资源
- 15.5 进阶学习路径
一、摘要
1.1 文章概览
本文是一份全面深入的Typora插件开发指南,旨在帮助开发者从零开始掌握Typora插件开发的完整技术栈。通过系统化的知识结构和丰富的实战案例,读者将学会如何利用JavaScript、HTML和CSS技术栈,为Typora构建功能强大、用户体验优秀的定制化插件。文章涵盖从基础环境搭建到高级功能实现的全过程,包含超过100个代码示例和50个最佳实践建议,总字数约40000字。
Typora是一款以"所见即所得"著称的Markdown编辑器,其简洁优雅的设计赢得了大量开发者和写作者的青睐。Typora原生支持100多种编程语言的语法高亮、LaTeX数学公式渲染、Mermaid流程图绘制、Emoji表情、目录生成、脚注、表格等丰富的Markdown扩展功能。然而,Typora官方并未提供正式的插件API,当前社区插件方案主要围绕一个核心开源项目obgnail/typora_plugin展开。该项目为Typora提供了超过60个内置插件,涵盖标签页管理、多关键词搜索、代码增强、图表支持等功能。
1.2 目标读者
- 前端开发者:希望扩展Typora功能,提升写作体验的技术人员
- 技术写作者:需要定制化写作环境,提高内容生产效率的专业人士
- 开源贡献者:有意参与Typora插件生态建设,为社区贡献价值的开发者
- 工具链爱好者:喜欢深度定制开发工具,追求极致工作效率的技术极客
1.3 核心价值
- 技术深度:深入剖析Typora插件系统架构,掌握底层实现原理
- 实践导向:提供可直接运行的代码示例,降低学习门槛
- 系统完整:覆盖插件开发全生命周期,从开发到发布完整流程
- 前沿技术:集成AI、实时协作等现代技术,打造下一代写作体验
- 工程化思维:强调代码质量、性能优化和安全防护,培养专业开发习惯
二、Typora插件系统概述
2.1 Typora插件架构解析
Typora基于Electron框架构建。Electron是一个使用JavaScript、HTML和CSS构建跨平台桌面应用程序的开源框架,它结合了Chromium渲染引擎和Node.js运行时。Electron采用双进程架构:主进程负责创建窗口和管理应用程序生命周期,每个应用程序只有一个主进程;渲染进程负责渲染网页界面,每个窗口运行独立的渲染进程,本质上是一个Chromium浏览器实例。
这套架构为插件开发提供了天然支撑。渲染进程暴露了完整的DOM操作能力,开发者可以通过开发者工具访问和修改页面环境,注入自定义代码。Node.js集成使得插件可以访问文件系统、网络、进程等系统级资源。
Typora插件系统采用三层架构设计:
┌─────────────────────────────────────────────────────┐
│ 功能插件层 │
│ 60+ 内置插件 + 用户自定义插件 │
├─────────────────────────────────────────────────────┤
│ 插件基类层 │
│ IPlugin | BasePlugin | BaseCustomPlugin │
├─────────────────────────────────────────────────────┤
│ 核心基础设施层 │
│ 配置加载 | 插件加载 | Utils | 国际化 | 事件中心 │
├─────────────────────────────────────────────────────┤
│ Typora原生层 │
│ File | ClientCommand | JSBridge | File.editor │
└─────────────────────────────────────────────────────┘
核心基础设施层 :位于plugin/global/core/,提供插件运行时的核心基础设施,包括事件中心、国际化支持、样式模板引擎等基础服务。
插件基类层 :定义了IPlugin、BasePlugin、BaseCustomPlugin三大基类。
功能插件层:包括60+内置插件和用户自定义插件。
2.2 插件系统核心概念
2.2.1 插件基类体系
Typora插件系统提供了两个主要的基类,分别适用于不同的扩展需求:
| 基类 | 适用场景 | 关键方法 | 集成方式 |
|---|---|---|---|
| BasePlugin | 复杂核心插件,需要完整的系统访问权限 | call(action, meta) |
手动处理事件和UI |
| BaseCustomPlugin | 用户定义的上下文感知扩展 | selector(), hint(), callback() |
自动集成到"自定义插件"菜单 |
2.2.2 IPlugin接口
所有插件都继承自IPlugin,它定义了标准的生命周期钩子:
javascript
class IPlugin {
constructor(fixedName, setting, i18n) {
this.fixedName = fixedName; // 插件唯一标识符
this.pluginName = setting.NAME; // 插件显示名称
this.config = setting; // 插件配置
this.utils = utils; // 工具框架
this.i18n = i18n; // 国际化
}
async beforeProcess() {} // 预处理阶段
style() {} // 样式注入
styleTemplate() {} // 样式模板
html() {} // HTML元素注入
hotkey() {} // 快捷键注册
init() {} // 初始化阶段
process() {} // 主处理逻辑
afterProcess() {} // 清理阶段
}
2.2.3 BaseCustomPlugin选择器模式
BaseCustomPlugin实现了一个selector/hint/callback模式,专门为上下文敏感的操作设计:
- selector:返回CSS选择器字符串,只有当光标位于匹配该选择器的元素内部时,插件的菜单项才会显示
- hint:返回用作菜单提示的字符串
- callback :当菜单项被点击或快捷键被按下时执行的函数,接收
anchorNode(匹配选择器的DOM元素)作为参数
2.3 插件生命周期管理
插件系统遵循一个七阶段生命周期模型:
加载 → beforeProcess → style → styleTemplate → html → hotkey → init → process → afterProcess
| 阶段 | 方法 | 用途 | 是否必须 |
|---|---|---|---|
| 1 | beforeProcess() |
预初始化检查,异步数据加载 | 否 |
| 2 | style() |
返回CSS字符串用于内联注入 | 否 |
| 3 | styleTemplate() |
返回模板参数用于动态CSS | 否 |
| 4 | html() |
返回HTML字符串用于DOM注入 | 否 |
| 5 | hotkey() |
注册键盘快捷键 | 否 |
| 6 | init() |
初始化插件状态和实体 | 否 |
| 7 | process() |
绑定事件监听器,执行主要逻辑 | 否 |
LoadPlugins函数(定义在plugin/global/core/plugin.js中)作为引擎,管理每个插件的初始化序列。
2.4 插件安全机制
插件系统通过以下机制保障安全性:
- 沙箱隔离:插件运行在独立的渲染进程中,与主进程隔离
- 权限控制:通过配置系统控制插件可访问的API范围
- 配置校验 :
settings.user.toml配置文件经过schema验证 - 动态加载:插件在启动时动态加载,便于管理和禁用
三、开发环境搭建
3.1 Typora安装与配置
3.1.1 版本要求
- Typora ≥ 0.9.98(推荐使用最新稳定版)
- 支持插件系统的Typora版本(≥ 1.0)
3.1.2 获取插件框架源码
社区插件方案主要围绕obgnail/typora_plugin项目展开:
bash
# 克隆项目仓库
git clone https://github.com/obgnail/typora_plugin.git
# 或使用GitCode镜像(访问更稳定)
git clone https://gitcode.com/obgnail/typora_plugin.git
3.1.3 安装插件框架
将克隆的插件框架部署到Typora的插件目录:
Windows :C:\Program Files\Typora\resources\app\plugin\
macOS :/Applications/Typora.app/Contents/Resources/app/plugin/
Linux :/opt/typora/resources/app/plugin/
3.2 插件开发目录结构
3.2.1 标准目录结构
typora_plugin/
├── plugin/
│ ├── global/
│ │ ├── core/ # 核心基础设施
│ │ │ ├── plugin.js # 插件加载器
│ │ │ ├── index.js # 入口点
│ │ │ └── utils/ # 工具函数
│ │ └── settings/ # 配置系统
│ ├── custom/ # 自定义插件
│ │ ├── plugins/ # 用户插件存放目录 ⭐
│ │ └── README.md
│ ├── hotkeys.js # 快捷键插件
│ ├── action_buttons.js # 操作按钮插件
│ └── json_rpc/ # JSON-RPC插件
├── config/ # 配置文件
└── code/ # 代码片段
3.2.2 自定义插件目录
用户自定义插件必须放在./plugin/custom/plugins/目录下:
plugin/custom/plugins/
├── myFirstPlugin.js
├── codeSnippetManager.js
├── aiAssistant.js
└── ...
3.3 开发工具链配置
3.3.1 基础工具
| 工具 | 用途 | 说明 |
|---|---|---|
| Typora | 目标编辑器 | 建议版本 ≥ 0.9.98 |
| Node.js | JavaScript运行时 | 插件开发的基础环境 |
| npm / yarn | 包管理工具 | 用于依赖管理 |
| VS Code | 代码编辑器 | 推荐用于插件开发 |
| Git | 版本控制 | 获取源码和管理版本 |
3.3.2 推荐VS Code扩展
- ESLint:JavaScript代码检查
- Prettier:代码格式化
- GitLens:Git增强
- Debugger for Chrome:调试支持
3.4 调试环境设置
3.4.1 开启调试模式
- 打开Typora
- 进入 文件 → 偏好设置
- 在 通用 中找到 高级设置
- 勾选 "开启调试模式"
3.4.2 打开开发者工具
- Windows/Linux :
Ctrl + Shift + I - macOS :
Cmd + Option + I - 或者右键 → 检查元素
3.4.3 控制台调试
在开发者工具中选择 Console(控制台) 标签,可以:
- 查看插件日志输出
- 执行JavaScript代码测试
- 检查DOM元素状态
- 监控网络请求
四、插件核心API详解
4.1 IPlugin基类深度剖析
4.1.1 构造函数
IPlugin构造函数初始化五个实例属性:
javascript
class IPlugin {
constructor(fixedName, setting, i18n) {
this.fixedName = fixedName; // 插件唯一标识符
this.pluginName = setting.NAME; // 显示名称(来自配置或i18n)
this.config = setting; // 插件配置对象
this.utils = utils; // Utils框架实例
this.i18n = i18n; // 国际化实例
}
}
4.1.2 生命周期方法详解
javascript
class MyPlugin extends IPlugin {
// 1. 预处理 - 用于异步数据加载和前置检查
async beforeProcess() {
// 可以返回 stopLoadPluginError 来阻止插件加载
const data = await this.loadData();
if (!data) {
return this.utils.stopLoadPluginError;
}
this.data = data;
}
// 2. 样式注入 - 返回CSS字符串
style() {
return `
.my-plugin-container {
border: 1px solid #ccc;
padding: 10px;
}
`;
}
// 3. 样式模板 - 支持动态CSS
styleTemplate() {
return {
background: this.config.BACKGROUND_COLOR || '#ffffff'
};
}
// 4. HTML注入 - 返回HTML字符串
html() {
return `<div id="my-plugin-panel">Hello World</div>`;
}
// 5. 快捷键注册
hotkey() {
return {
'Ctrl+Shift+M': this.handleHotkey.bind(this)
};
}
// 6. 初始化
init() {
this.state = {};
this.registerEventListeners();
}
// 7. 主处理逻辑
process() {
// 绑定事件、启动服务等
this.utils.eventHub.on('typora:ready', this.onReady.bind(this));
}
// 8. 后处理(清理)
afterProcess() {
// 清理资源
}
}
4.2 BasePlugin扩展类实战
BasePlugin适用于需要完整系统访问权限的复杂插件:
javascript
// plugin/custom/plugins/myBasePlugin.js
const { BasePlugin } = require('../global/core/plugin');
class MyBasePlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.actions = {
'do-something': this.doSomething.bind(this),
'get-data': this.getData.bind(this)
};
}
// BasePlugin使用call(action, meta)模式
call(action, meta) {
if (this.actions[action]) {
return this.actions[action](meta);
}
throw new Error(`Unknown action: ${action}`);
}
doSomething(meta) {
console.log('Doing something with:', meta);
// 执行复杂逻辑
return { success: true };
}
getData(meta) {
return this.utils.getFilePath();
}
// 完整生命周期支持
process() {
// 注册到插件系统
this.utils.eventHub.on('file:opened', this.onFileOpened.bind(this));
}
}
module.exports = { plugin: MyBasePlugin };
4.3 BaseCustomPlugin选择器模式
BaseCustomPlugin实现了selector/hint/callback模式,专为上下文敏感操作设计:
javascript
// plugin/custom/plugins/myCustomPlugin.js
const { BaseCustomPlugin } = require('../global/core/plugin');
class MyCustomPlugin extends BaseCustomPlugin {
// 必需:选择器 - 决定插件何时可用
selector = (context) => {
// 返回CSS选择器,当光标在匹配元素内时显示菜单项
return 'pre code, .code-block';
}
// 必需:主逻辑
callback = (anchorNode) => {
// anchorNode是匹配selector的DOM元素
const code = anchorNode.textContent;
// 执行操作,如格式化、翻译等
this.formatCode(code);
}
// 可选:提示文本
hint = (context) => {
return '格式化代码';
}
formatCode(code) {
// 代码格式化逻辑
console.log('Formatting:', code);
}
}
module.exports = { plugin: MyCustomPlugin };
4.3.1 高级选择器模式
javascript
class AdvancedCustomPlugin extends BaseCustomPlugin {
// 动态选择器 - 根据上下文返回不同选择器
selector = (context) => {
const { editor } = context;
const selection = editor.getSelection();
if (selection && selection.length > 0) {
return '.selected-text';
}
return 'p, h1, h2, h3, h4, h5, h6';
}
// 动态提示
hint = (context) => {
const { anchorNode } = context;
if (anchorNode && anchorNode.tagName === 'PRE') {
return '处理代码块';
}
return '处理文本';
}
callback = (anchorNode) => {
// 根据节点类型执行不同操作
if (anchorNode.tagName === 'PRE') {
this.handleCodeBlock(anchorNode);
} else {
this.handleText(anchorNode);
}
}
}
4.4 Utils框架与共享服务
Utils框架是一个综合工具类,为所有插件提供共享基础设施:
4.4.1 系统信息
javascript
// 获取运行时环境信息
const utils = this.utils;
console.log(utils.nodeVersion); // Node.js版本
console.log(utils.electronVersion); // Electron版本
console.log(utils.chromeVersion); // Chrome版本
console.log(utils.typoraVersion); // Typora版本
console.log(utils.isBetaVersion); // 是否Beta版本
console.log(utils.separator); // 路径分隔符
console.log(utils.tempFolder); // 临时目录
4.4.2 插件管理
javascript
// 获取插件实例
const basePlugin = utils.getBasePlugin('pluginName');
const customPlugin = utils.getCustomPlugin('pluginName');
const allBasePlugins = utils.getAllBasePlugins();
const allCustomPlugins = utils.getAllCustomPlugins();
// 调用其他插件的方法
const result = utils.callPluginFunction('pluginName', 'methodName', arg1, arg2);
4.4.3 文件系统操作
javascript
// 文件操作
const { Path, Fs, FsExtra } = utils.Package;
// 获取当前文件路径
const currentFile = utils.getFilePath();
// 获取用户主目录
const homeDir = utils.getHomeDir();
// 读取文件
const content = FsExtra.readFileSync('/path/to/file', 'utf-8');
4.4.4 DOM操作
javascript
// DOM工具
const $ = utils.$; // jQuery风格选择器
// 获取锚点节点
const anchorNode = utils.getAnchorNode('pre code');
// 注入样式
utils.registerStyle('myPlugin', `
.my-class { color: red; }
`);
4.5 事件系统与消息传递
4.5.1 事件中心
插件系统通过事件中心管理各种生命周期事件:
javascript
// 监听事件
this.utils.eventHub.on('event:name', (data) => {
console.log('Event triggered:', data);
});
// 触发事件
this.utils.eventHub.emit('event:name', { key: 'value' });
// 一次性监听
this.utils.eventHub.once('plugin:loaded', () => {
console.log('Plugin loaded once');
});
// 移除监听
this.utils.eventHub.off('event:name', handler);
4.5.2 核心事件
| 事件名称 | 触发时机 | 数据 |
|---|---|---|
typora:ready |
Typora完全加载 | - |
file:opened |
文件打开 | 文件路径 |
file:saved |
文件保存 | 文件路径 |
file:closed |
文件关闭 | 文件路径 |
selection:changed |
选区变化 | 选区信息 |
content:changed |
内容变化 | 变更数据 |
plugin:loaded |
插件加载完成 | 插件名称 |
allPluginsHadInjected |
所有插件注入完成 | - |
4.5.3 动态动作系统
动态动作支持上下文感知的菜单项和命令:
javascript
// 更新动态动作
this.utils.updatePluginDynamicActions('pluginName', anchorNode, false);
// 执行动态动作
this.utils.callPluginDynamicAction('pluginName', 'actionName');
// 组合更新和执行
this.utils.updateAndCallPluginDynamicAction(
'pluginName',
'actionName',
anchorNode,
false
);
4.6 DOM操作与UI扩展
4.6.1 注入自定义UI
javascript
class UIPlugin extends BaseCustomPlugin {
html() {
return `
<div id="my-panel" style="position: fixed; right: 20px; top: 100px;
background: white; border: 1px solid #ccc; padding: 15px;
border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<h4>我的面板</h4>
<button id="my-action-btn">执行操作</button>
</div>
`;
}
process() {
// 注入完成后绑定事件
const panel = document.getElementById('my-panel');
const btn = document.getElementById('my-action-btn');
if (btn) {
btn.addEventListener('click', this.handleClick.bind(this));
}
}
handleClick() {
const content = this.utils.getFilePath();
console.log('Current file:', content);
}
}
4.6.2 操作按钮插件
action_buttons插件可在编辑器界面添加可配置的UI按钮:
javascript
// 在配置中定义按钮
const BUTTONS = [
{
name: '格式化',
icon: '🔧',
evil: 'utils.formatCode()',
tooltip: '格式化当前代码'
},
{
name: '导出',
icon: '📤',
plugin: 'exportPlugin',
func: 'exportToPDF',
tooltip: '导出为PDF'
}
];
五、基础插件开发实战
5.1 Hello World插件
5.1.1 创建插件文件
在plugin/custom/plugins/目录下创建helloWorld.js:
javascript
// plugin/custom/plugins/helloWorld.js
const { BaseCustomPlugin } = require('../global/core/plugin');
class HelloWorldPlugin extends BaseCustomPlugin {
// 选择器 - 在任意位置可用
selector = () => {
return 'body';
}
// 提示文本
hint = () => {
return 'Hello World! 点击显示问候';
}
// 主逻辑
callback = (anchorNode) => {
// 使用Utils框架显示通知
this.utils.showNotification('Hello World!', '欢迎使用Typora插件开发!');
// 在控制台输出
console.log('Hello World from Typora plugin!');
console.log('Current node:', anchorNode);
// 获取当前文件信息
const filePath = this.utils.getFilePath();
console.log('Current file:', filePath);
}
}
module.exports = { plugin: HelloWorldPlugin };
5.1.2 配置插件
在配置文件中启用插件:
toml
# plugin/global/settings/settings.user.toml
[custom_plugins]
helloWorld = true
5.1.3 测试插件
- 重启Typora
- 在编辑区域右键点击
- 在自定义插件菜单中找到"Hello World! 点击显示问候"
- 点击查看效果
5.2 菜单扩展插件
5.2.1 添加自定义菜单项
javascript
// plugin/custom/plugins/customMenu.js
const { BaseCustomPlugin } = require('../global/core/plugin');
class CustomMenuPlugin extends BaseCustomPlugin {
selector = () => 'body';
hint = () => '显示自定义菜单';
callback = (anchorNode) => {
// 创建自定义上下文菜单
this.showCustomMenu(anchorNode);
}
showCustomMenu(anchorNode) {
const menuItems = [
{ label: '插入时间戳', action: this.insertTimestamp.bind(this) },
{ label: '转换为大写', action: this.convertToUpper.bind(this) },
{ label: '统计字数', action: this.countWords.bind(this) },
{ label: '---', separator: true },
{ label: '打开设置', action: this.openSettings.bind(this) }
];
// 使用Utils框架显示菜单
this.utils.showContextMenu(menuItems);
}
insertTimestamp() {
const timestamp = new Date().toLocaleString();
this.utils.insertText(timestamp);
}
convertToUpper() {
const selection = this.utils.getSelection();
if (selection) {
this.utils.replaceSelection(selection.toUpperCase());
}
}
countWords() {
const content = this.utils.getEditorContent();
const words = content.split(/\s+/).filter(w => w.length > 0).length;
this.utils.showNotification(`总字数: ${words}`);
}
openSettings() {
// 打开设置面板
this.utils.openSettings();
}
}
module.exports = { plugin: CustomMenuPlugin };
5.3 快捷键绑定插件
5.3.1 使用hotkey方法注册快捷键
javascript
// plugin/custom/plugins/hotkeyPlugin.js
const { BasePlugin } = require('../global/core/plugin');
class HotkeyPlugin extends BasePlugin {
// 注册快捷键
hotkey() {
return {
'Ctrl+Shift+F': this.formatDocument.bind(this),
'Ctrl+Shift+S': this.saveAndExport.bind(this),
'Ctrl+Alt+T': this.insertTable.bind(this),
'F12': this.openDevTools.bind(this)
};
}
formatDocument() {
// 格式化整个文档
const content = this.utils.getEditorContent();
const formatted = this.formatMarkdown(content);
this.utils.setEditorContent(formatted);
this.utils.showNotification('文档已格式化');
}
saveAndExport() {
this.utils.saveFile();
this.exportToHTML();
}
insertTable() {
const table = `
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| 数据1 | 数据2 | 数据3 |
| 数据4 | 数据5 | 数据6 |
`;
this.utils.insertText(table);
}
openDevTools() {
// 打开开发者工具
require('electron').remote.getCurrentWindow().webContents.openDevTools();
}
formatMarkdown(content) {
// 简单的Markdown格式化
return content
.replace(/\n{3,}/g, '\n\n')
.replace(/[ \t]+$/gm, '');
}
}
module.exports = { plugin: HotkeyPlugin };
5.3.2 使用HotkeysPlugin配置快捷键
通过hotkeys插件声明式绑定快捷键:
javascript
// 在配置中定义自定义快捷键
const CUSTOM_HOTKEYS = [
{
name: '插入日期',
keys: 'Ctrl+Shift+D',
evil: 'utils.insertText(new Date().toISOString().split("T")[0])'
},
{
name: '运行脚本',
keys: 'Ctrl+Shift+R',
plugin: 'myScriptPlugin',
func: 'run',
closestSelector: 'pre code'
}
];
5.4 状态栏扩展插件
5.4.1 添加状态栏信息
javascript
// plugin/custom/plugins/statusBar.js
const { BasePlugin } = require('../global/core/plugin');
class StatusBarPlugin extends BasePlugin {
process() {
// 等待Typora完全加载
this.utils.eventHub.on('typora:ready', () => {
this.createStatusBarItems();
});
// 监听文件变化更新状态
this.utils.eventHub.on('file:opened', this.updateStatus.bind(this));
this.utils.eventHub.on('content:changed', this.updateWordCount.bind(this));
}
createStatusBarItems() {
// 获取状态栏容器
const statusBar = document.querySelector('.status-bar');
if (!statusBar) return;
// 创建状态项
this.statusItems = {
wordCount: this.createStatusItem('单词: 0'),
fileInfo: this.createStatusItem('未命名'),
lineCol: this.createStatusItem('行: 1 列: 1')
};
// 添加到状态栏
Object.values(this.statusItems).forEach(item => {
statusBar.appendChild(item);
});
// 初始更新
this.updateStatus();
}
createStatusItem(text) {
const span = document.createElement('span');
span.className = 'status-bar-item';
span.style.cssText = 'margin: 0 10px; font-size: 12px; color: #888;';
span.textContent = text;
return span;
}
updateStatus() {
const filePath = this.utils.getFilePath();
const fileName = filePath ? filePath.split(/[\\/]/).pop() : '未命名';
this.statusItems.fileInfo.textContent = `📄 ${fileName}`;
this.updateWordCount();
}
updateWordCount() {
const content = this.utils.getEditorContent();
const words = content.split(/\s+/).filter(w => w.length > 0).length;
this.statusItems.wordCount.textContent = `📝 单词: ${words}`;
// 更新行列信息
const selection = window.getSelection();
if (selection && selection.anchorNode) {
const range = selection.getRangeAt(0);
// 计算行列...
}
}
}
module.exports = { plugin: StatusBarPlugin };
5.5 侧边栏面板插件
5.5.1 创建侧边栏面板
javascript
// plugin/custom/plugins/sidebarPanel.js
const { BasePlugin } = require('../global/core/plugin');
class SidebarPanelPlugin extends BasePlugin {
html() {
return `
<div id="my-sidebar-panel" style="
position: fixed;
left: 0;
top: 0;
width: 280px;
height: 100vh;
background: #f5f5f5;
border-right: 1px solid #ddd;
padding: 20px;
z-index: 999;
transform: translateX(-100%);
transition: transform 0.3s ease;
overflow-y: auto;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">📋 面板</h3>
<button id="close-sidebar" style="background: none; border: none; font-size: 20px; cursor: pointer;">✕</button>
</div>
<div id="sidebar-content">
<p>加载中...</p>
</div>
<button id="toggle-sidebar" style="
position: absolute;
right: -40px;
top: 50%;
transform: translateY(-50%);
background: #f5f5f5;
border: 1px solid #ddd;
border-left: none;
padding: 10px 8px;
cursor: pointer;
border-radius: 0 4px 4px 0;
">◀</button>
</div>
`;
}
process() {
this.utils.eventHub.on('typora:ready', () => {
this.setupPanel();
});
}
setupPanel() {
const panel = document.getElementById('my-sidebar-panel');
if (!panel) return;
// 显示面板
setTimeout(() => {
panel.style.transform = 'translateX(0)';
}, 500);
// 关闭按钮
const closeBtn = document.getElementById('close-sidebar');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
panel.style.transform = 'translateX(-100%)';
});
}
// 切换按钮
const toggleBtn = document.getElementById('toggle-sidebar');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const current = panel.style.transform;
panel.style.transform = current === 'translateX(0)' ?
'translateX(-100%)' : 'translateX(0)';
toggleBtn.textContent = current === 'translateX(0)' ? '▶' : '◀';
});
}
// 加载内容
this.loadContent();
}
loadContent() {
const content = document.getElementById('sidebar-content');
if (!content) return;
// 显示文档结构
const structure = this.getDocumentStructure();
content.innerHTML = this.renderStructure(structure);
}
getDocumentStructure() {
const editor = document.querySelector('.typora-edit');
if (!editor) return [];
const headings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');
return Array.from(headings).map(h => ({
level: parseInt(h.tagName[1]),
text: h.textContent.trim(),
id: h.id || `heading-${Date.now()}`
}));
}
renderStructure(items) {
if (items.length === 0) {
return '<p style="color: #999;">暂无标题</p>';
}
let html = '<ul style="list-style: none; padding: 0;">';
items.forEach(item => {
const indent = (item.level - 1) * 20;
html += `
<li style="padding-left: ${indent}px; margin: 5px 0; cursor: pointer;
color: #333; hover: color: #007acc;"
onclick="document.getElementById('${item.id}')?.scrollIntoView()">
${'#'.repeat(item.level)} ${item.text}
</li>
`;
});
html += '</ul>';
return html;
}
}
module.exports = { plugin: SidebarPanelPlugin };
六、高级功能插件开发
6.1 文件操作与I/O插件
6.1.1 文件系统访问
javascript
// plugin/custom/plugins/fileIO.js
const { BasePlugin } = require('../global/core/plugin');
const fs = require('fs-extra');
const path = require('path');
class FileIOPlugin extends BasePlugin {
process() {
this.registerCommands();
}
registerCommands() {
// 注册到命令系统
this.utils.commandHub.register('file:batch-rename', this.batchRename.bind(this));
this.utils.commandHub.register('file:export-all', this.exportAll.bind(this));
this.utils.commandHub.register('file:backup', this.createBackup.bind(this));
}
async batchRename(pattern) {
const files = await this.getMarkdownFiles();
const renamed = [];
for (const file of files) {
const dir = path.dirname(file);
const ext = path.extname(file);
const base = path.basename(file, ext);
// 应用重命名模式
const newName = this.applyPattern(pattern, base);
const newPath = path.join(dir, newName + ext);
if (newPath !== file) {
await fs.move(file, newPath);
renamed.push({ from: file, to: newPath });
}
}
this.utils.showNotification(`已重命名 ${renamed.length} 个文件`);
return renamed;
}
async getMarkdownFiles() {
const currentDir = path.dirname(this.utils.getFilePath());
const files = await fs.readdir(currentDir);
return files
.filter(f => f.endsWith('.md'))
.map(f => path.join(currentDir, f));
}
applyPattern(pattern, base) {
// 支持变量替换: {date}, {time}, {index}等
const now = new Date();
return pattern
.replace(/{date}/g, now.toISOString().split('T')[0])
.replace(/{time}/g, now.toTimeString().slice(0, 8).replace(/:/g, '-'))
.replace(/{base}/g, base);
}
async exportAll() {
const files = await this.getMarkdownFiles();
const exportDir = path.join(path.dirname(files[0]), 'export');
await fs.ensureDir(exportDir);
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
const html = this.convertToHTML(content);
const name = path.basename(file, '.md');
await fs.writeFile(path.join(exportDir, `${name}.html`), html);
}
this.utils.showNotification(`已导出 ${files.length} 个文件到 ${exportDir}`);
}
convertToHTML(markdown) {
// 调用Typora的转换功能
return this.utils.convertMarkdownToHTML(markdown);
}
async createBackup() {
const file = this.utils.getFilePath();
if (!file) return;
const backupDir = path.join(path.dirname(file), '.backup');
await fs.ensureDir(backupDir);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `${path.basename(file, '.md')}_${timestamp}.md`;
const backupPath = path.join(backupDir, backupName);
await fs.copy(file, backupPath);
this.utils.showNotification(`已创建备份: ${backupName}`);
}
}
module.exports = { plugin: FileIOPlugin };
6.2 代码片段管理插件
6.2.1 代码片段数据结构
javascript
// plugin/custom/plugins/snippetManager.js
const { BaseCustomPlugin } = require('../global/core/plugin');
class SnippetManager extends BaseCustomPlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.snippets = this.loadSnippets();
}
selector = () => 'body';
hint = () => '📋 代码片段管理';
callback = (anchorNode) => {
this.showSnippetPanel();
}
loadSnippets() {
// 从配置文件或本地存储加载
const defaultSnippets = [
{
id: 'snippet-1',
name: 'React组件',
language: 'javascript',
prefix: 'rc',
body: `import React from 'react';
const ${1:ComponentName} = () => {
return (
<div>
${2:/* 内容 */}
</div>
);
};
export default ${1:ComponentName};`,
description: 'React函数组件模板'
},
{
id: 'snippet-2',
name: 'Python函数',
language: 'python',
prefix: 'def',
body: `def ${1:function_name}(${2:params}):
"""${3:文档字符串}"""
${4:pass}`,
description: 'Python函数定义'
},
{
id: 'snippet-3',
name: 'SQL查询',
language: 'sql',
prefix: 'sel',
body: `SELECT ${1:columns}
FROM ${2:table}
WHERE ${3:condition}
ORDER BY ${4:column} ${5:DESC};`,
description: 'SQL SELECT查询'
}
];
// 从存储加载自定义片段
const custom = this.loadCustomSnippets();
return [...defaultSnippets, ...custom];
}
loadCustomSnippets() {
try {
const path = this.utils.getMountFolder();
const filePath = path + '/snippets.json';
if (this.utils.isExists(filePath)) {
return JSON.parse(this.utils.getString(filePath));
}
} catch (e) {
console.error('加载自定义片段失败:', e);
}
return [];
}
showSnippetPanel() {
// 创建面板
const panel = document.createElement('div');
panel.id = 'snippet-panel';
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
width: 500px;
max-height: 70vh;
overflow-y: auto;
z-index: 10000;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
`;
// 标题
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">📋 代码片段</h3>
<button id="close-snippet-panel" style="background: none; border: none; font-size: 20px; cursor: pointer;">✕</button>
</div>
<div style="margin-bottom: 15px;">
<input id="snippet-search" placeholder="搜索片段..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div id="snippet-list"></div>
`;
document.body.appendChild(panel);
// 渲染片段列表
this.renderSnippetList();
// 事件绑定
document.getElementById('close-snippet-panel').addEventListener('click', () => {
panel.remove();
});
document.getElementById('snippet-search').addEventListener('input', (e) => {
this.filterSnippets(e.target.value);
});
}
renderSnippetList(filter = '') {
const container = document.getElementById('snippet-list');
if (!container) return;
const filtered = filter ?
this.snippets.filter(s =>
s.name.toLowerCase().includes(filter.toLowerCase()) ||
s.description.toLowerCase().includes(filter.toLowerCase()) ||
s.language.includes(filter.toLowerCase())
) : this.snippets;
container.innerHTML = filtered.map(snippet => `
<div class="snippet-item" data-id="${snippet.id}" style="
padding: 10px;
margin: 5px 0;
border: 1px solid #eee;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
hover: background: #f0f0f0;
">
<div style="display: flex; justify-content: space-between;">
<span><strong>${snippet.name}</strong></span>
<span style="color: #888; font-size: 12px;">${snippet.language}</span>
</div>
<div style="color: #666; font-size: 13px;">${snippet.description}</div>
<div style="color: #999; font-size: 12px;">前缀: ${snippet.prefix}</div>
</div>
`).join('');
// 绑定点击事件
container.querySelectorAll('.snippet-item').forEach(el => {
el.addEventListener('click', () => {
const id = el.dataset.id;
const snippet = this.snippets.find(s => s.id === id);
if (snippet) {
this.insertSnippet(snippet);
document.getElementById('snippet-panel')?.remove();
}
});
});
}
filterSnippets(query) {
this.renderSnippetList(query);
}
insertSnippet(snippet) {
// 处理占位符
let code = snippet.body;
const placeholders = code.match(/\$\{\d+:([^}]*)\}/g) || [];
// 插入代码
this.utils.insertText(code);
// 如果有占位符,移动到第一个占位符位置
if (placeholders.length > 0) {
// 实现Tab跳转逻辑
this.setupTabNavigation(placeholders);
}
this.utils.showNotification(`已插入: ${snippet.name}`);
}
setupTabNavigation(placeholders) {
// 简化实现:移动到插入位置
// 完整实现需要跟踪占位符位置并支持Tab跳转
console.log('Placeholders:', placeholders);
}
}
module.exports = { plugin: SnippetManager };
6.3 实时预览增强插件
javascript
// plugin/custom/plugins/previewEnhance.js
const { BasePlugin } = require('../global/core/plugin');
class PreviewEnhancePlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.previewTimeout = null;
this.isPreviewMode = false;
}
process() {
this.utils.eventHub.on('content:changed', this.onContentChanged.bind(this));
this.utils.eventHub.on('typora:ready', this.setupPreviewEnhancements.bind(this));
}
setupPreviewEnhancements() {
// 添加预览控制按钮
this.addPreviewControls();
// 增强预览样式
this.enhancePreviewStyles();
}
addPreviewControls() {
const toolbar = document.querySelector('.typora-toolbar');
if (!toolbar) return;
const btn = document.createElement('button');
btn.className = 'preview-toggle-btn';
btn.textContent = '🔍 增强预览';
btn.style.cssText = `
margin-left: 10px;
padding: 4px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
`;
btn.addEventListener('click', this.togglePreviewMode.bind(this));
toolbar.appendChild(btn);
}
togglePreviewMode() {
this.isPreviewMode = !this.isPreviewMode;
const editor = document.querySelector('.typora-edit');
const preview = document.querySelector('.typora-preview');
if (this.isPreviewMode) {
// 进入增强预览模式
editor.style.display = 'none';
preview.style.width = '100%';
preview.style.maxWidth = '100%';
preview.style.padding = '40px';
this.applyEnhancedStyles();
this.utils.showNotification('已进入增强预览模式');
} else {
// 退出预览模式
editor.style.display = '';
preview.style.width = '';
preview.style.maxWidth = '';
preview.style.padding = '';
this.removeEnhancedStyles();
this.utils.showNotification('已退出增强预览模式');
}
}
enhancePreviewStyles() {
// 注入增强样式
this.utils.registerStyle('preview-enhance', `
.typora-preview {
font-size: 16px;
line-height: 1.8;
}
.typora-preview h1 {
border-bottom: 2px solid #007acc;
padding-bottom: 10px;
}
.typora-preview h2 {
border-bottom: 1px solid #ddd;
padding-bottom: 8px;
}
.typora-preview pre {
border-radius: 6px;
padding: 16px;
background: #f6f8fa;
border: 1px solid #e1e4e8;
}
.typora-preview blockquote {
border-left: 4px solid #007acc;
padding-left: 20px;
color: #555;
}
.typora-preview table {
border-collapse: collapse;
width: 100%;
}
.typora-preview th, .typora-preview td {
border: 1px solid #ddd;
padding: 8px 12px;
}
.typora-preview th {
background: #f6f8fa;
}
`);
}
applyEnhancedStyles() {
// 添加阅读模式样式
const style = document.createElement('style');
style.id = 'enhanced-preview-style';
style.textContent = `
.typora-preview {
max-width: 900px !important;
margin: 0 auto !important;
font-size: 17px !important;
line-height: 1.9 !important;
}
.typora-preview h1 { font-size: 2.2em !important; }
.typora-preview h2 { font-size: 1.8em !important; }
.typora-preview h3 { font-size: 1.5em !important; }
.typora-preview img { max-width: 100%; border-radius: 4px; }
.typora-preview .task-list-item { list-style: none; }
`;
document.head.appendChild(style);
}
removeEnhancedStyles() {
const style = document.getElementById('enhanced-preview-style');
if (style) style.remove();
}
onContentChanged() {
// 防抖处理
clearTimeout(this.previewTimeout);
this.previewTimeout = setTimeout(() => {
this.updatePreviewStats();
}, 1000);
}
updatePreviewStats() {
const content = this.utils.getEditorContent();
const stats = this.calculateStats(content);
// 显示在状态栏
this.displayStats(stats);
}
calculateStats(content) {
const words = content.split(/\s+/).filter(w => w.length > 0).length;
const chars = content.replace(/\s/g, '').length;
const lines = content.split('\n').length;
const headings = (content.match(/^#{1,6}\s/gm) || []).length;
const codeBlocks = (content.match(/```/g) || []).length / 2;
return { words, chars, lines, headings, codeBlocks };
}
displayStats(stats) {
// 在状态栏显示统计信息
const statusBar = document.querySelector('.status-bar');
if (!statusBar) return;
let statsEl = document.getElementById('preview-stats');
if (!statsEl) {
statsEl = document.createElement('span');
statsEl.id = 'preview-stats';
statsEl.style.cssText = 'margin: 0 15px; font-size: 12px; color: #888;';
statusBar.appendChild(statsEl);
}
statsEl.textContent = `📊 ${stats.words}词 · ${stats.chars}字 · ${stats.lines}行 · ${stats.headings}标题 · ${stats.codeBlocks}代码块`;
}
}
module.exports = { plugin: PreviewEnhancePlugin };
6.4 语法高亮扩展插件
javascript
// plugin/custom/plugins/syntaxHighlight.js
const { BasePlugin } = require('../global/core/plugin');
class SyntaxHighlightPlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.highlighters = {};
this.languageMap = {
'js': 'javascript',
'ts': 'typescript',
'py': 'python',
'rb': 'ruby',
'go': 'go',
'rs': 'rust',
'swift': 'swift',
'kt': 'kotlin',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'cs': 'csharp',
'php': 'php',
'html': 'html',
'css': 'css',
'scss': 'scss',
'sql': 'sql',
'json': 'json',
'xml': 'xml',
'yaml': 'yaml',
'toml': 'toml',
'dockerfile': 'dockerfile',
'sh': 'bash',
'bash': 'bash',
'zsh': 'bash'
};
}
process() {
this.setupSyntaxHighlighting();
this.registerLanguageSupport();
}
setupSyntaxHighlighting() {
// 注入自定义高亮样式
this.utils.registerStyle('syntax-highlight', `
.code-block pre code {
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 14px;
line-height: 1.6;
}
/* 自定义颜色主题 */
.hljs-keyword { color: #c792ea; font-weight: bold; }
.hljs-string { color: #c3e88d; }
.hljs-comment { color: #546e7a; font-style: italic; }
.hljs-function { color: #82aaff; }
.hljs-number { color: #f78c6c; }
.hljs-operator { color: #89ddff; }
.hljs-class { color: #ffcb6b; }
.hljs-variable { color: #eeffff; }
.hljs-constant { color: #f78c6c; }
/* 行号支持 */
.code-block .line-numbers {
display: inline-block;
width: 30px;
color: #546e7a;
text-align: right;
padding-right: 15px;
user-select: none;
}
/* 高亮特定行 */
.code-block .highlight-line {
background: rgba(130, 170, 255, 0.15);
display: block;
margin: 0 -15px;
padding: 0 15px;
}
`);
}
registerLanguageSupport() {
// 注册语言检测器
this.utils.registerLanguageDetector((code, defaultLang) => {
return this.detectLanguage(code, defaultLang);
});
}
detectLanguage(code, defaultLang) {
// 基于代码内容检测语言
const patterns = {
'python': /^import |^def |^class |if __name__/m,
'javascript': /^const |^let |^var |=>|function\s*\(/m,
'typescript': /^interface |^type |^enum |:\s*(string|number|boolean)/m,
'java': /^public class |^private |^protected |System\.out/m,
'go': /^func |^package |^import |:=/m,
'rust': /^fn |^pub |^let mut |-> /m,
'ruby': /^def |^class |^require |attr_/m,
'php': /^\$|^function |^class |echo /m,
'sql': /^SELECT |^INSERT |^UPDATE |^DELETE |^CREATE /mi,
'html': /<[a-z]+[^>]*>/mi,
'css': /^[.#][a-zA-Z-]+\s*{/m,
'json': /^\{[\s\n]*"[^"]+"/m,
'yaml': /^[a-zA-Z-]+:\s/m,
'bash': /^#!/bin|^echo |^export |^for .* in /m
};
for (const [lang, pattern] of Object.entries(patterns)) {
if (pattern.test(code)) {
return lang;
}
}
return defaultLang || 'plaintext';
}
// 扩展方法:高亮特定行
highlightLines(code, lines) {
const codeLines = code.split('\n');
return codeLines.map((line, index) => {
const lineNum = index + 1;
if (lines.includes(lineNum)) {
return `<span class="highlight-line">${line}</span>`;
}
return line;
}).join('\n');
}
// 扩展方法:添加行号
addLineNumbers(code) {
const lines = code.split('\n');
const maxWidth = String(lines.length).length;
return lines.map((line, index) => {
const num = String(index + 1).padStart(maxWidth, ' ');
return `<span class="line-numbers">${num}</span>${line}`;
}).join('\n');
}
}
module.exports = { plugin: SyntaxHighlightPlugin };
6.5 AI辅助写作插件
6.5.1 AI插件基础结构
javascript
// plugin/custom/plugins/aiAssistant.js
const { BaseCustomPlugin } = require('../global/core/plugin');
class AIAssistantPlugin extends BaseCustomPlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.apiKey = setting.API_KEY || '';
this.apiEndpoint = setting.API_ENDPOINT || 'https://api.openai.com/v1/chat/completions';
this.model = setting.MODEL || 'gpt-3.5-turbo';
this.history = [];
this.maxHistory = 20;
}
selector = () => 'body';
hint = () => '🤖 AI助手';
callback = (anchorNode) => {
this.showAIPanel();
}
showAIPanel() {
const panel = document.createElement('div');
panel.id = 'ai-panel';
panel.style.cssText = `
position: fixed;
bottom: 80px;
right: 30px;
width: 400px;
max-height: 500px;
background: white;
border: 1px solid #ddd;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 10000;
display: flex;
flex-direction: column;
overflow: hidden;
`;
panel.innerHTML = `
<div style="padding: 15px; background: #f8f9fa; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold;">🤖 AI助手</span>
<div>
<button id="ai-minimize" style="background: none; border: none; cursor: pointer; margin-right: 8px;">─</button>
<button id="ai-close" style="background: none; border: none; cursor: pointer;">✕</button>
</div>
</div>
<div id="ai-messages" style="flex: 1; padding: 15px; overflow-y: auto; min-height: 200px; max-height: 300px;">
<div style="color: #999; text-align: center; padding: 20px;">
输入问题开始与AI对话
</div>
</div>
<div style="padding: 10px; border-top: 1px solid #ddd; display: flex; gap: 8px;">
<select id="ai-action" style="padding: 6px; border: 1px solid #ddd; border-radius: 4px; flex-shrink: 0;">
<option value="chat">💬 对话</option>
<option value="polish">✨ 润色</option>
<option value="translate">🌐 翻译</option>
<option value="summarize">📝 总结</option>
<option value="continue">✍️ 续写</option>
</select>
<input id="ai-input" placeholder="输入消息..." style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button id="ai-send" style="padding: 8px 16px; background: #007acc; color: white; border: none; border-radius: 4px; cursor: pointer;">发送</button>
</div>
`;
document.body.appendChild(panel);
this.bindAIEvents(panel);
}
bindAIEvents(panel) {
const input = document.getElementById('ai-input');
const sendBtn = document.getElementById('ai-send');
const closeBtn = document.getElementById('ai-close');
const minBtn = document.getElementById('ai-minimize');
const actionSelect = document.getElementById('ai-action');
sendBtn.addEventListener('click', () => {
this.handleAIAction(input.value, actionSelect.value);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleAIAction(input.value, actionSelect.value);
}
});
closeBtn.addEventListener('click', () => {
panel.remove();
});
let isMinimized = false;
minBtn.addEventListener('click', () => {
const messages = document.getElementById('ai-messages');
const controls = document.querySelector('#ai-panel > div:last-child');
if (isMinimized) {
messages.style.display = '';
controls.style.display = '';
minBtn.textContent = '─';
panel.style.maxHeight = '500px';
} else {
messages.style.display = 'none';
controls.style.display = 'none';
minBtn.textContent = '□';
panel.style.maxHeight = '50px';
}
isMinimized = !isMinimized;
});
}
async handleAIAction(input, action) {
if (!input.trim()) return;
const messagesContainer = document.getElementById('ai-messages');
if (!messagesContainer) return;
// 显示用户消息
this.addMessage(messagesContainer, 'user', input);
// 清空输入
document.getElementById('ai-input').value = '';
// 显示加载状态
const loadingId = this.addLoadingMessage(messagesContainer);
try {
// 构建提示词
const prompt = this.buildPrompt(action, input);
// 调用AI API
const response = await this.callAI(prompt);
// 移除加载状态
this.removeLoadingMessage(loadingId);
// 显示AI回复
this.addMessage(messagesContainer, 'assistant', response);
// 保存历史
this.saveHistory(action, input, response);
} catch (error) {
this.removeLoadingMessage(loadingId);
this.addMessage(messagesContainer, 'error', `错误: ${error.message}`);
}
}
buildPrompt(action, input) {
const prompts = {
'polish': `请润色以下文本,使其更加流畅和专业:\n\n${input}`,
'translate': `请将以下文本翻译成中文:\n\n${input}`,
'summarize': `请总结以下文本的核心要点:\n\n${input}`,
'continue': `请继续以下文本的写作,保持风格一致:\n\n${input}`,
'chat': input
};
return prompts[action] || input;
}
async callAI(prompt) {
// 构建消息历史
const messages = [
{ role: 'system', content: '你是一个专业的写作助手,帮助用户处理Markdown文档。' }
];
// 添加历史消息
for (const h of this.history.slice(-this.maxHistory)) {
messages.push({ role: h.role, content: h.content });
}
messages.push({ role: 'user', content: prompt });
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: messages,
temperature: 0.7,
max_tokens: 2000
})
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content;
}
addMessage(container, role, content) {
const div = document.createElement('div');
const isUser = role === 'user';
div.style.cssText = `
margin: 8px 0;
padding: 10px 14px;
border-radius: 8px;
max-width: 85%;
${isUser ? 'align-self: flex-end; background: #007acc; color: white; margin-left: auto;' :
'align-self: flex-start; background: #f0f0f0; color: #333;'}
word-wrap: break-word;
`;
div.textContent = content;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
addLoadingMessage(container) {
const id = 'loading-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.style.cssText = 'margin: 8px 0; padding: 10px 14px; border-radius: 8px; background: #f0f0f0; color: #999; align-self: flex-start;';
div.textContent = '🤔 思考中...';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return id;
}
removeLoadingMessage(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
saveHistory(action, input, response) {
this.history.push(
{ role: 'user', content: input },
{ role: 'assistant', content: response }
);
if (this.history.length > this.maxHistory * 2) {
this.history = this.history.slice(-this.maxHistory * 2);
}
}
}
module.exports = { plugin: AIAssistantPlugin };
6.6 JSON-RPC外部API集成
JSON-RPC插件通过JSON-RPC 2.0协议将Typora的能力暴露给外部进程。这使得外部程序(Python、Node.js等)可以编程控制Typora,自动化文档操作,将Typora集成到更广阔的开发工作流中。
6.6.1 JSON-RPC API方法
插件暴露三个核心RPC方法:
1. ping - 验证连接和插件活跃性
javascript
// 返回: "pong from typora-plugin"
2. invokePlugin - 调用其他插件的方法
javascript
// 参数: [pluginName, functionName, ...args]
// 示例: invokePlugin('snippetManager', 'insertSnippet', 'snippet-1')
3. eval - 执行任意JavaScript代码
javascript
// 参数: [code]
// 示例: eval('utils.getFilePath()')
6.6.2 外部调用示例
Python调用示例:
python
import json
import requests
def call_typora_plugin(plugin_name, func_name, *args):
url = "http://localhost:12345/jsonrpc" # 默认端口
payload = {
"jsonrpc": "2.0",
"method": "invokePlugin",
"params": [plugin_name, func_name, *args],
"id": 1
}
response = requests.post(url, json=payload)
return response.json()
# 调用AI助手
result = call_typora_plugin('aiAssistant', 'handleAIAction', '请总结这段文字', 'summarize')
print(result)
Node.js调用示例:
javascript
const axios = require('axios');
async function callTypora(method, params) {
const response = await axios.post('http://localhost:12345/jsonrpc', {
jsonrpc: '2.0',
method: method,
params: params,
id: Date.now()
});
return response.data;
}
// 获取当前文件路径
const result = await callTypora('eval', ['utils.getFilePath()']);
console.log('Current file:', result.result);
6.6.3 安全警告
JSON-RPC插件仅面向开发者使用。一旦启用,外部客户端将获得对Typora进程的重要控制权。应通过配置将其绑定到127.0.0.1,以防止未授权的远程访问。
七、UI/UX深度定制
7.1 自定义主题与样式
7.1.1 通过custom.css定制
在Typora配置文件夹中创建custom.css文件:
css
/* %APPDATA%/Typora/custom.css (Windows) */
/* ~/Library/Application Support/Typora/custom.css (macOS) */
/* ~/.config/Typora/custom.css (Linux) */
/* 自定义编辑器字体 */
#write {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 16px;
line-height: 1.8;
}
/* 自定义标题样式 */
#write h1 {
font-size: 2.2em;
border-bottom: 3px solid #007acc;
padding-bottom: 12px;
margin-top: 30px;
}
#write h2 {
font-size: 1.8em;
border-bottom: 2px solid #e1e4e8;
padding-bottom: 8px;
margin-top: 25px;
}
/* 代码块样式增强 */
#write pre {
border-radius: 8px;
padding: 16px;
background: #1e1e1e;
border: 1px solid #333;
}
#write pre code {
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 14px;
}
/* 表格样式 */
#write table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
}
#write th, #write td {
border: 1px solid #ddd;
padding: 8px 12px;
}
#write th {
background: #f0f4f8;
font-weight: bold;
}
/* 引用块样式 */
#write blockquote {
border-left: 4px solid #007acc;
padding-left: 20px;
color: #555;
background: #f8f9fa;
border-radius: 0 4px 4px 0;
margin: 16px 0;
}
/* 任务列表样式 */
#write .task-list-item {
list-style: none;
}
#write .task-list-item input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.1);
}
/* 图片样式 */
#write img {
max-width: 100%;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin: 16px 0;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f0f0f0;
}
::-webkit-scrollbar-thumb {
background: #c1c7cd;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a0a6ac;
}
7.1.2 通过base.user.css定制
如果要修改CSS样式并应用到所有主题,应修改base.user.css文件:
css
/* 在所有主题中生效的全局样式 */
body {
--primary-color: #007acc;
--secondary-color: #c792ea;
--background-color: #ffffff;
--text-color: #333333;
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
body {
--background-color: #1e1e1e;
--text-color: #d4d4d4;
}
}
7.2 动态组件开发
7.2.1 创建可交互组件
javascript
// plugin/custom/plugins/dynamicComponent.js
const { BasePlugin } = require('../global/core/plugin');
class DynamicComponentPlugin extends BasePlugin {
html() {
return `
<div id="dynamic-component" style="
position: fixed;
bottom: 20px;
right: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 16px;
z-index: 9999;
min-width: 200px;
transition: all 0.3s ease;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="font-weight: bold; color: #333;">📊 文档统计</span>
<button id="component-close" style="background: none; border: none; cursor: pointer; color: #999;">✕</button>
</div>
<div id="component-content">
<div>📝 字符: <span id="stat-chars">0</span></div>
<div>📊 单词: <span id="stat-words">0</span></div>
<div>📄 行数: <span id="stat-lines">0</span></div>
<div>🔖 标题: <span id="stat-headings">0</span></div>
<div>💻 代码块: <span id="stat-codeblocks">0</span></div>
</div>
<div style="margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap;">
<button class="stat-action" data-action="refresh" style="padding: 4px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px;">🔄 刷新</button>
<button class="stat-action" data-action="export" style="padding: 4px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px;">📤 导出</button>
</div>
</div>
`;
}
process() {
this.utils.eventHub.on('typora:ready', () => {
this.setupComponent();
});
this.utils.eventHub.on('content:changed', () => {
this.updateStats();
});
}
setupComponent() {
// 关闭按钮
const closeBtn = document.getElementById('component-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
const component = document.getElementById('dynamic-component');
if (component) component.remove();
});
}
// 动作按钮
document.querySelectorAll('.stat-action').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (action === 'refresh') this.updateStats();
if (action === 'export') this.exportStats();
});
});
// 初始更新
this.updateStats();
// 使组件可拖拽
this.makeDraggable(document.getElementById('dynamic-component'));
}
updateStats() {
const content = this.utils.getEditorContent();
if (!content) return;
const chars = content.replace(/\s/g, '').length;
const words = content.split(/\s+/).filter(w => w.length > 0).length;
const lines = content.split('\n').length;
const headings = (content.match(/^#{1,6}\s/gm) || []).length;
const codeBlocks = (content.match(/```/g) || []).length / 2;
document.getElementById('stat-chars').textContent = chars;
document.getElementById('stat-words').textContent = words;
document.getElementById('stat-lines').textContent = lines;
document.getElementById('stat-headings').textContent = headings;
document.getElementById('stat-codeblocks').textContent = codeBlocks;
}
exportStats() {
const stats = {
chars: document.getElementById('stat-chars').textContent,
words: document.getElementById('stat-words').textContent,
lines: document.getElementById('stat-lines').textContent,
headings: document.getElementById('stat-headings').textContent,
codeBlocks: document.getElementById('stat-codeblocks').textContent,
timestamp: new Date().toISOString(),
file: this.utils.getFilePath()
};
const json = JSON.stringify(stats, null, 2);
this.utils.showNotification('统计已导出到控制台');
console.log('Document Stats:', json);
}
makeDraggable(element) {
if (!element) return;
let isDragging = false;
let offsetX, offsetY;
element.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
const rect = element.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
element.style.cursor = 'grabbing';
element.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
element.style.left = (e.clientX - offsetX) + 'px';
element.style.top = (e.clientY - offsetY) + 'px';
element.style.right = 'auto';
element.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
element.style.cursor = '';
element.style.userSelect = '';
});
}
}
module.exports = { plugin: DynamicComponentPlugin };
7.3 拖拽交互实现
javascript
// 拖拽工具函数
class DragDropManager {
constructor() {
this.dragData = null;
this.dragOverlay = null;
}
enableDrag(element, data) {
element.draggable = true;
element.addEventListener('dragstart', (e) => {
this.dragData = data;
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/plain', JSON.stringify(data));
element.style.opacity = '0.5';
});
element.addEventListener('dragend', () => {
element.style.opacity = '1';
});
}
enableDrop(element, onDrop) {
element.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
element.style.border = '2px dashed #007acc';
});
element.addEventListener('dragleave', () => {
element.style.border = '';
});
element.addEventListener('drop', (e) => {
e.preventDefault();
element.style.border = '';
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
onDrop(data, element);
});
}
}
7.4 响应式布局设计
javascript
// 响应式面板
class ResponsivePanel {
constructor() {
this.breakpoints = {
mobile: 480,
tablet: 768,
desktop: 1024
};
this.currentBreakpoint = this.getBreakpoint();
this.setupResizeListener();
}
getBreakpoint() {
const width = window.innerWidth;
if (width < this.breakpoints.mobile) return 'mobile';
if (width < this.breakpoints.tablet) return 'tablet';
if (width < this.breakpoints.desktop) return 'desktop';
return 'wide';
}
setupResizeListener() {
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const newBreakpoint = this.getBreakpoint();
if (newBreakpoint !== this.currentBreakpoint) {
this.currentBreakpoint = newBreakpoint;
this.onBreakpointChange(newBreakpoint);
}
}, 200);
});
}
onBreakpointChange(breakpoint) {
const panel = document.getElementById('responsive-panel');
if (!panel) return;
switch(breakpoint) {
case 'mobile':
panel.style.width = '100%';
panel.style.maxWidth = '100%';
panel.style.height = '50vh';
panel.style.bottom = '0';
panel.style.right = '0';
panel.style.borderRadius = '12px 12px 0 0';
break;
case 'tablet':
panel.style.width = '70%';
panel.style.maxWidth = '500px';
panel.style.height = '60vh';
panel.style.bottom = '20px';
panel.style.right = '20px';
panel.style.borderRadius = '12px';
break;
default:
panel.style.width = '400px';
panel.style.maxWidth = '400px';
panel.style.height = '70vh';
panel.style.bottom = '20px';
panel.style.right = '20px';
panel.style.borderRadius = '12px';
}
}
}
7.5 动画效果集成
javascript
// 动画工具
class AnimationUtils {
static fadeIn(element, duration = 300) {
element.style.opacity = '0';
element.style.display = 'block';
element.style.transition = `opacity ${duration}ms ease`;
requestAnimationFrame(() => {
element.style.opacity = '1';
});
setTimeout(() => {
element.style.transition = '';
}, duration);
}
static fadeOut(element, duration = 300) {
element.style.transition = `opacity ${duration}ms ease`;
element.style.opacity = '0';
setTimeout(() => {
element.style.display = 'none';
element.style.transition = '';
}, duration);
}
static slideIn(element, direction = 'right', duration = 300) {
const transforms = {
'right': 'translateX(100%)',
'left': 'translateX(-100%)',
'top': 'translateY(-100%)',
'bottom': 'translateY(100%)'
};
element.style.transform = transforms[direction] || transforms.right;
element.style.transition = `transform ${duration}ms ease`;
requestAnimationFrame(() => {
element.style.transform = 'translate(0)';
});
setTimeout(() => {
element.style.transition = '';
}, duration);
}
static pulse(element) {
element.style.animation = 'pulse 0.5s ease';
setTimeout(() => {
element.style.animation = '';
}, 500);
}
}
// 注入关键帧动画
const animationStyles = document.createElement('style');
animationStyles.textContent = `
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
`;
document.head.appendChild(animationStyles);
八、性能优化与调试
8.1 插件性能监控
javascript
// plugin/custom/plugins/performanceMonitor.js
const { BasePlugin } = require('../global/core/plugin');
class PerformanceMonitorPlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.metrics = {
renderTime: [],
fileLoadTime: [],
memoryUsage: [],
eventLatency: []
};
this.maxMetrics = 100;
}
process() {
this.startMonitoring();
this.registerPerformanceHooks();
}
startMonitoring() {
// 监控内存使用
setInterval(() => {
const memory = process.memoryUsage();
this.metrics.memoryUsage.push({
timestamp: Date.now(),
heapUsed: memory.heapUsed,
heapTotal: memory.heapTotal,
rss: memory.rss
});
if (this.metrics.memoryUsage.length > this.maxMetrics) {
this.metrics.memoryUsage.shift();
}
}, 30000);
}
registerPerformanceHooks() {
// 监控文件加载时间
this.utils.eventHub.on('file:opened', (path) => {
const start = performance.now();
this.utils.eventHub.once('file:rendered', () => {
const duration = performance.now() - start;
this.metrics.fileLoadTime.push({ path, duration });
if (this.metrics.fileLoadTime.length > this.maxMetrics) {
this.metrics.fileLoadTime.shift();
}
});
});
// 监控渲染性能
let renderStart = 0;
this.utils.eventHub.on('content:changed', () => {
renderStart = performance.now();
});
this.utils.eventHub.on('content:rendered', () => {
if (renderStart) {
const duration = performance.now() - renderStart;
this.metrics.renderTime.push(duration);
if (this.metrics.renderTime.length > this.maxMetrics) {
this.metrics.renderTime.shift();
}
}
});
}
getPerformanceReport() {
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
return {
avgRenderTime: avg(this.metrics.renderTime),
avgFileLoadTime: avg(this.metrics.fileLoadTime.map(m => m.duration)),
currentMemory: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1],
totalMetrics: {
renderTime: this.metrics.renderTime.length,
fileLoadTime: this.metrics.fileLoadTime.length,
memoryUsage: this.metrics.memoryUsage.length
}
};
}
}
module.exports = { plugin: PerformanceMonitorPlugin };
8.2 内存泄漏检测
javascript
// 内存泄漏检测工具
class MemoryLeakDetector {
constructor() {
this.registeredElements = new Map();
this.eventListeners = new Map();
this.intervals = new Set();
this.timeouts = new Set();
}
registerElement(element, source) {
const id = this.generateId();
this.registeredElements.set(id, {
element,
source,
timestamp: Date.now()
});
return id;
}
unregisterElement(id) {
this.registeredElements.delete(id);
}
registerEventListener(target, event, handler, source) {
const id = this.generateId();
this.eventListeners.set(id, {
target,
event,
handler,
source,
timestamp: Date.now()
});
target.addEventListener(event, handler);
return id;
}
unregisterEventListener(id) {
const listener = this.eventListeners.get(id);
if (listener) {
listener.target.removeEventListener(listener.event, listener.handler);
this.eventListeners.delete(id);
}
}
registerInterval(interval, source) {
this.intervals.add({ interval, source, timestamp: Date.now() });
return interval;
}
clearInterval(interval) {
clearInterval(interval);
this.intervals.delete(interval);
}
generateId() {
return 'leak-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
}
detectLeaks() {
const now = Date.now();
const threshold = 60000; // 1分钟
const leaks = [];
// 检查注册的元素
for (const [id, data] of this.registeredElements) {
if (!document.contains(data.element)) {
leaks.push({
type: 'element',
id,
source: data.source,
age: now - data.timestamp,
message: 'Element removed from DOM but not unregistered'
});
}
}
// 检查事件监听器
for (const [id, data] of this.eventListeners) {
if (!document.contains(data.target)) {
leaks.push({
type: 'eventListener',
id,
source: data.source,
age: now - data.timestamp,
message: `Event listener on removed element: ${data.event}`
});
}
}
return leaks;
}
cleanup() {
// 清理所有注册的资源
for (const [id, data] of this.eventListeners) {
data.target.removeEventListener(data.event, data.handler);
}
this.eventListeners.clear();
for (const interval of this.intervals) {
clearInterval(interval);
}
this.intervals.clear();
this.registeredElements.clear();
}
}
8.3 调试技巧与工具
8.3.1 使用Chrome DevTools调试
- 打开开发者工具 :
Ctrl+Shift+I(Windows/Linux) 或Cmd+Option+I(macOS) - 断点调试:在Sources面板中设置断点
- 控制台调试:在Console中执行JavaScript代码
- 性能分析:使用Performance面板分析性能瓶颈
8.3.2 日志系统
javascript
// 日志工具
class Logger {
constructor(pluginName) {
this.pluginName = pluginName;
this.logLevel = 'info';
this.logs = [];
this.maxLogs = 1000;
}
setLevel(level) {
const levels = ['debug', 'info', 'warn', 'error'];
if (levels.includes(level)) {
this.logLevel = level;
}
}
debug(...args) {
if (this.shouldLog('debug')) {
this.log('DEBUG', ...args);
}
}
info(...args) {
if (this.shouldLog('info')) {
this.log('INFO', ...args);
}
}
warn(...args) {
if (this.shouldLog('warn')) {
this.log('WARN', ...args);
}
}
error(...args) {
if (this.shouldLog('error')) {
this.log('ERROR', ...args);
}
}
shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error'];
return levels.indexOf(level) >= levels.indexOf(this.logLevel);
}
log(level, ...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] [${this.pluginName}] [${level}] ${args.join(' ')}`;
console.log(message);
this.logs.push({ timestamp, level, message });
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
}
getLogs(level) {
if (level) {
return this.logs.filter(log => log.level === level);
}
return this.logs;
}
exportLogs() {
return this.logs.map(log => log.message).join('\n');
}
}
8.4 错误处理与日志
8.4.1 全局错误捕获
javascript
// 错误处理工具
class ErrorHandler {
constructor(pluginName) {
this.pluginName = pluginName;
this.errorCallbacks = [];
this.setupGlobalHandler();
}
setupGlobalHandler() {
// 捕获未处理的Promise异常
window.addEventListener('unhandledrejection', (event) => {
this.handleError('Unhandled Promise Rejection', event.reason);
});
// 捕获全局异常
window.addEventListener('error', (event) => {
this.handleError('Uncaught Exception', event.error || event.message);
});
}
handleError(type, error) {
const errorInfo = {
type,
message: error?.message || String(error),
stack: error?.stack,
timestamp: new Date().toISOString(),
plugin: this.pluginName
};
console.error(`[${this.pluginName}] Error:`, errorInfo);
// 通知所有错误回调
this.errorCallbacks.forEach(cb => {
try {
cb(errorInfo);
} catch (e) {
console.error('Error in error callback:', e);
}
});
}
onError(callback) {
this.errorCallbacks.push(callback);
return () => {
const index = this.errorCallbacks.indexOf(callback);
if (index > -1) {
this.errorCallbacks.splice(index, 1);
}
};
}
wrap(fn, context) {
return async (...args) => {
try {
return await fn.apply(context, args);
} catch (error) {
this.handleError('Wrapped Function Error', error);
throw error;
}
};
}
}
8.4.2 插件中使用错误处理
javascript
// 在插件中使用错误处理
class SafePlugin extends BaseCustomPlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.errorHandler = new ErrorHandler(fixedName);
this.logger = new Logger(fixedName);
// 设置错误回调
this.errorHandler.onError((error) => {
this.logger.error('Plugin error:', error);
this.utils.showNotification(`插件错误: ${error.message}`, 'error');
});
}
// 使用错误包装执行敏感操作
async safeOperation() {
return this.errorHandler.wrap(async () => {
// 可能出错的操作
const data = await this.loadData();
this.processData(data);
}, this)();
}
}
8.5 性能优化策略
8.5.1 优化清单
- 减少DOM操作:批量更新DOM,使用DocumentFragment
- 防抖和节流:对高频事件使用防抖/节流
- 懒加载:按需加载插件模块
- 缓存计算结果:使用Memoization
- 异步处理:使用async/await避免阻塞
- 减少事件监听器:使用事件委托
- 优化CSS选择器:避免使用复杂选择器
- 限制日志输出:生产环境减少日志
8.5.2 防抖/节流工具
javascript
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流函数
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 使用示例
class OptimizedPlugin extends BasePlugin {
process() {
// 防抖处理内容变化
const debouncedUpdate = debounce(this.updateContent.bind(this), 500);
this.utils.eventHub.on('content:changed', debouncedUpdate);
// 节流处理滚动事件
const throttledScroll = throttle(this.handleScroll.bind(this), 100);
document.addEventListener('scroll', throttledScroll);
}
}
九、插件发布与分发
9.1 插件打包规范
9.1.1 插件文件结构
my-plugin/
├── plugin/
│ └── custom/
│ └── plugins/
│ └── myPlugin.js # 插件主文件
├── config/
│ └── settings.user.toml # 配置文件
├── README.md # 说明文档
├── LICENSE # 许可证
└── package.json # 包信息(可选)
9.1.2 package.json配置
json
{
"name": "typora-my-plugin",
"version": "1.0.0",
"description": "Typora插件描述",
"main": "plugin/custom/plugins/myPlugin.js",
"keywords": ["typora", "plugin", "markdown"],
"author": "Your Name",
"license": "MIT",
"typora": {
"minVersion": "0.9.98",
"maxVersion": "2.0.0",
"type": "custom"
}
}
9.2 版本管理策略
9.2.1 语义化版本
遵循SemVer规范:MAJOR.MINOR.PATCH
- MAJOR:不兼容的API变更
- MINOR:向后兼容的功能新增
- PATCH:向后兼容的问题修复
9.2.2 版本更新日志
markdown
# Changelog
## [1.2.0] - 2026-06-01
### Added
- 新增AI辅助写作功能
- 新增代码片段管理
### Fixed
- 修复快捷键冲突问题
- 修复大文件渲染卡顿
## [1.1.0] - 2026-05-15
### Added
- 新增状态栏扩展
- 新增自定义菜单
### Changed
- 优化插件加载性能
- 更新API文档
## [1.0.0] - 2026-05-01
### Added
- 初始版本发布
- 基础插件框架
- Hello World示例
9.3 插件市场发布
9.3.1 发布到GitHub
- 创建GitHub仓库
- 上传插件源码
- 编写README文档
- 创建Release版本
- 使用GitHub Issues收集反馈
9.3.2 发布到社区
- 在Typora社区论坛发布
- 在技术博客分享插件开发经验
- 在GitHub上提交Pull Request到主项目
9.4 用户反馈收集
javascript
// 反馈收集工具
class FeedbackCollector {
constructor() {
this.feedbackEndpoint = 'https://api.example.com/feedback';
this.sessionId = this.generateSessionId();
}
generateSessionId() {
return 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 8);
}
async collectFeedback(type, data) {
try {
const payload = {
sessionId: this.sessionId,
type: type, // 'error', 'feature', 'bug', 'general'
data: data,
timestamp: new Date().toISOString(),
plugin: this.pluginName,
version: this.version,
platform: navigator.platform,
typoraVersion: this.utils.typoraVersion
};
await fetch(this.feedbackEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (error) {
console.error('Feedback collection failed:', error);
}
}
trackFeatureUsage(featureName) {
this.collectFeedback('feature_usage', {
feature: featureName,
duration: 0
});
}
}
9.5 持续集成部署
9.5.1 GitHub Actions配置
yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Build plugin
run: npm run build
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
dist/*.js
README.md
LICENSE
十、安全与最佳实践
10.1 安全编码规范
10.1.1 输入验证
javascript
// 输入验证工具
class InputValidator {
static validatePath(path) {
// 防止路径遍历攻击
if (path.includes('..') || path.includes('~')) {
throw new Error('Invalid path');
}
return path;
}
static validateCommand(command) {
// 防止命令注入
const dangerous = [';', '&&', '||', '|', '`', '$('];
for (const char of dangerous) {
if (command.includes(char)) {
throw new Error(`Invalid character in command: ${char}`);
}
}
return command;
}
static sanitizeHTML(html) {
// 防止XSS攻击
const temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
}
}
10.1.2 安全配置
javascript
// 安全配置示例
const SECURE_CONFIG = {
// 禁用危险的eval
allowEval: false,
// 限制文件系统访问
fileSystemAccess: {
allowedPaths: ['/documents', '/projects'],
deniedPaths: ['/etc', '/system']
},
// 网络请求限制
networkAccess: {
allowedDomains: ['api.example.com'],
blockedDomains: ['*']
},
// 插件权限
permissions: {
'file:read': true,
'file:write': false,
'network:request': true,
'system:command': false
}
};
10.2 权限最小化原则
10.2.1 权限声明
javascript
// 插件权限声明
class SecurePlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
// 声明所需权限
this.permissions = {
'file:read': true, // 读取文件
'file:write': false, // 不写入文件
'network:request': true, // 网络请求
'clipboard:read': false, // 不读取剪贴板
'clipboard:write': true // 写入剪贴板
};
// 验证权限
this.validatePermissions();
}
validatePermissions() {
// 检查权限是否被授予
for (const [perm, required] of Object.entries(this.permissions)) {
if (required && !this.hasPermission(perm)) {
throw new Error(`Missing permission: ${perm}`);
}
}
}
hasPermission(perm) {
// 从配置中检查权限
return this.config.PERMISSIONS?.[perm] || false;
}
}
10.3 XSS防护策略
10.3.1 内容安全策略
javascript
// CSP配置
class CSPManager {
static setup() {
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = `
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https:;
`;
document.head.appendChild(meta);
}
static sanitizeInput(input) {
// 移除潜在的XSS向量
return input
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/on\w+\s*=/gi, '')
.replace(/javascript:/gi, '')
.replace(/data:text\/html/gi, '');
}
}
10.3.2 安全DOM操作
javascript
// 安全的DOM操作
class SafeDOM {
static createElement(tag, content, attributes = {}) {
const el = document.createElement(tag);
// 使用textContent而不是innerHTML
if (content) {
el.textContent = content;
}
// 安全设置属性
for (const [key, value] of Object.entries(attributes)) {
if (key.startsWith('on')) {
// 拒绝事件处理器属性
continue;
}
el.setAttribute(key, value);
}
return el;
}
static setHTML(element, html) {
// 使用DOMPurify或类似库清理HTML
const clean = this.sanitizeHTML(html);
element.innerHTML = clean;
}
}
10.4 数据隐私保护
10.4.1 敏感数据处理
javascript
// 数据隐私工具
class PrivacyManager {
constructor() {
this.encryptionKey = null;
this.storage = new Map();
}
// 加密存储敏感数据
async storeSecure(key, data) {
const encrypted = await this.encrypt(data);
this.storage.set(key, encrypted);
}
async retrieveSecure(key) {
const encrypted = this.storage.get(key);
if (!encrypted) return null;
return await this.decrypt(encrypted);
}
async encrypt(data) {
// 使用Web Crypto API
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
// 生成或获取密钥
const key = await this.getEncryptionKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
dataBuffer
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
};
}
async decrypt(encrypted) {
const key = await this.getEncryptionKey();
const iv = new Uint8Array(encrypted.iv);
const data = new Uint8Array(encrypted.data);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
data
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
async getEncryptionKey() {
if (this.encryptionKey) return this.encryptionKey;
// 从安全存储获取或生成密钥
this.encryptionKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
return this.encryptionKey;
}
}
10.5 代码审查要点
10.5.1 审查清单
- 安全性:是否存在XSS、注入、信息泄露风险?
- 性能:是否存在性能瓶颈、内存泄漏?
- 可靠性:错误处理是否完善?
- 兼容性:是否兼容不同Typora版本?
- 可维护性:代码是否清晰、有注释?
- 国际化:是否支持多语言?
- 文档:是否有完整的README和使用说明?
10.5.2 代码质量检查
javascript
// ESLint配置示例
module.exports = {
env: {
browser: true,
node: true,
es2021: true
},
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
},
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
'no-script-url': 'error',
'no-unsafe-finally': 'error',
'require-await': 'warn',
'max-lines': ['warn', 300],
'complexity': ['warn', 10]
}
};
十一、真实案例分析
11.1 代码片段管理器插件
11.1.1 功能需求
- 管理常用代码片段
- 支持分类和标签
- 快速插入到文档
- 支持自定义片段
11.1.2 技术实现
javascript
// 完整代码片段管理器(见6.2节)
// 核心功能:
// 1. 片段存储 - 使用JSON文件持久化
// 2. 片段搜索 - 支持名称、描述、语言搜索
// 3. 片段插入 - 支持占位符和Tab跳转
// 4. 片段管理 - 增删改查操作
11.2 实时协作编辑插件
11.2.1 架构设计
javascript
// 协作编辑基础框架
class CollaborationPlugin extends BasePlugin {
constructor(fixedName, setting, i18n) {
super(fixedName, setting, i18n);
this.roomId = null;
this.peers = new Map();
this.operations = [];
this.websocket = null;
}
process() {
this.setupWebSocket();
this.setupOTEngine();
this.setupPresence();
}
setupWebSocket() {
// WebSocket连接
const wsUrl = this.config.WS_URL || 'ws://localhost:8080';
this.websocket = new WebSocket(wsUrl);
this.websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleRemoteOperation(data);
};
}
setupOTEngine() {
// 操作变换引擎
// 实现OT算法处理并发编辑
}
setupPresence() {
// 用户在线状态
// 光标位置同步
// 选区和高亮
}
}
11.3 智能目录生成插件
11.3.1 功能实现
javascript
// 智能目录生成(见5.5节侧边栏面板)
// 核心功能:
// 1. 自动提取标题层级
// 2. 生成可点击的目录树
// 3. 支持滚动跟随
// 4. 支持折叠/展开
11.4 图片优化处理插件
11.4.1 功能实现
javascript
// 图片优化插件
class ImageOptimizerPlugin extends BaseCustomPlugin {
selector = () => 'img';
hint = () => '🖼️ 优化图片';
callback = (anchorNode) => {
this.optimizeImage(anchorNode);
}
async optimizeImage(imgElement) {
const src = imgElement.src;
// 检查是否为本地图片
if (src.startsWith('file://')) {
// 压缩图片
const compressed = await this.compressImage(src);
// 更新图片
imgElement.src = compressed;
// 显示优化结果
this.utils.showNotification('图片已优化');
}
}
async compressImage(filePath) {
// 使用sharp或类似库压缩图片
// 返回压缩后的base64或路径
}
}
11.5 多语言翻译插件
11.5.1 功能实现
javascript
// 翻译插件
class TranslationPlugin extends BaseCustomPlugin {
selector = () => 'p, h1, h2, h3, h4, h5, h6, blockquote';
hint = () => '🌐 翻译选中内容';
callback = (anchorNode) => {
const text = anchorNode.textContent;
this.translateText(text);
}
async translateText(text) {
// 调用翻译API
const targetLang = this.config.TARGET_LANG || 'zh-CN';
const result = await this.callTranslationAPI(text, targetLang);
// 显示翻译结果
this.showTranslationPopup(result);
}
showTranslationPopup(translation) {
// 创建浮动显示翻译结果
const popup = document.createElement('div');
popup.style.cssText = `
position: fixed;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 10000;
max-width: 400px;
`;
popup.textContent = translation;
document.body.appendChild(popup);
// 定位到鼠标位置
// 添加关闭按钮
// 支持替换原文
}
}
十二、未来展望与生态
12.1 Typora插件生态现状
当前Typora插件生态主要围绕obgnail/typora_plugin项目展开,该项目提供了:
- 60+内置插件:涵盖标签页管理、多关键词搜索、代码增强、图表支持等功能
- 活跃的社区:GitHub上有持续更新和贡献
- 丰富的扩展点:快捷键、菜单、按钮、JSON-RPC等多种扩展方式
12.2 新版本API展望
随着Typora版本更新,插件API可能的发展方向:
- 官方API支持:Typora官方可能提供正式插件API
- TypeScript支持:更好的类型定义和开发体验
- 更丰富的UI组件:标准化的UI组件库
- 性能优化:更高效的插件加载和运行机制
12.3 跨平台兼容性
Typora基于Electron,天然支持跨平台:
- Windows:支持Windows 7及以上
- macOS:支持macOS 10.12及以上
- Linux:支持主流Linux发行版
插件开发时需注意:
- 路径分隔符差异(
/vs\) - 快捷键差异(
CtrlvsCmd) - 文件系统权限差异
12.4 社区贡献指南
12.4.1 如何贡献
- 报告问题:在GitHub提交Issue
- 提交代码:Fork仓库,创建PR
- 编写文档:完善README和API文档
- 分享经验:撰写博客和教程
12.4.2 贡献规范
- 遵循代码风格
- 添加单元测试
- 更新文档
- 保持向后兼容
12.5 企业级应用前景
Typora插件在企业级场景中的应用:
- 技术文档平台:集成企业文档管理系统
- 知识库建设:团队协作写作工具
- 自动化出版:集成CI/CD流水线
- 数据报告生成:自动化报告生成工具
十三、总结
13.1 核心要点回顾
- 架构理解:Typora基于Electron,插件系统采用三层架构
- 开发基础:使用JavaScript/HTML/CSS扩展Typora功能
- 插件类型:BasePlugin(复杂核心插件)和BaseCustomPlugin(上下文感知插件)
- 生命周期:七阶段生命周期模型
- 核心API:IPlugin、Utils框架、事件系统
- 扩展方式:快捷键、菜单、按钮、JSON-RPC等多种方式
13.2 学习路径建议
第一阶段:基础入门(1-2周)
- 了解Typora插件系统架构
- 掌握IPlugin和BaseCustomPlugin基类
- 完成Hello World插件
第二阶段:实践进阶(2-4周)
- 开发自定义菜单和快捷键插件【5.2、5.3节】
- 实现UI扩展和状态栏插件【5.4、5.5节】
- 学习Utils框架和事件系统
第三阶段:高级开发(4-8周)
- 开发AI辅助写作插件【6.5节】
- 集成JSON-RPC外部API【6.6节】
- 性能优化和调试【第八章】
第四阶段:生态贡献(持续)
- 发布插件到社区【第九章】
- 参与开源项目贡献【12.4节】
- 分享经验和最佳实践
13.3 开发者成长路线
初学者 → 了解Electron和Typora架构
↓
插件开发者 → 掌握BaseCustomPlugin开发
↓
高级开发者 → 掌握BasePlugin和系统级扩展
↓
核心贡献者 → 参与框架开发和生态建设
↓
技术专家 → 架构设计和最佳实践推广
十四、附录
14.1 API参考手册
14.1.1 IPlugin生命周期方法
| 方法 | 返回值 | 说明 |
|---|---|---|
beforeProcess() |
Promise/void | 预处理,可返回stopLoadPluginError阻止加载 |
style() |
string | 返回CSS样式字符串 |
styleTemplate() |
object | 返回样式模板参数 |
html() |
string | 返回HTML字符串 |
hotkey() |
object | 返回快捷键映射 |
init() |
void | 初始化插件 |
process() |
void | 主处理逻辑 |
afterProcess() |
void | 清理资源 |
14.1.2 Utils框架核心方法
| 方法 | 说明 |
|---|---|
getFilePath() |
获取当前文件路径 |
getHomeDir() |
获取用户主目录 |
getBasePlugin(name) |
获取基础插件实例 |
getCustomPlugin(name) |
获取自定义插件实例 |
callPluginFunction(name, func, ...args) |
调用其他插件方法 |
registerStyle(name, css) |
注入样式 |
showNotification(msg) |
显示通知 |
14.1.3 事件列表
| 事件 | 说明 |
|---|---|
typora:ready |
Typora加载完成 |
file:opened |
文件打开 |
file:saved |
文件保存 |
content:changed |
内容变化 |
allPluginsHadInjected |
所有插件注入完成 |
14.2 快捷键大全
14.2.1 常用快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+Shift+I |
打开开发者工具 |
Ctrl+Shift+F |
格式化文档 |
Ctrl+Shift+S |
保存并导出 |
Ctrl+Alt+T |
插入表格 |
14.2.2 自定义快捷键配置
javascript
// 在hotkey()方法中注册
hotkey() {
return {
'Ctrl+Shift+M': this.myFunction.bind(this),
'Ctrl+Alt+R': this.runScript.bind(this)
};
}
14.3 常见问题解答
Q1: 插件安装后不生效?
解决方案:
- 确认插件目录位置正确
- 检查Typora版本是否支持插件系统(需要0.9.86以上版本)
- 彻底退出Typora,清缓存后重启
- 检查配置文件是否正确启用插件
Q2: 如何调试插件?
- 开启调试模式
- 使用
Ctrl+Shift+I打开开发者工具 - 在Console中查看日志输出
- 使用
console.log()输出调试信息
Q3: 插件更新后不生效?
- 彻底退出Typora
- 清缓存
- 重启Typora
Q4: 如何获取当前文档内容?
javascript
const content = this.utils.getEditorContent();
Q5: 如何插入文本到编辑器?
javascript
this.utils.insertText('要插入的文本');
14.4 学习资源推荐
14.4.1 官方资源
14.4.2 社区资源
- CSDN Typora专栏
- DeepWiki Typora插件文档
- V2EX Typora讨论区
14.4.3 推荐阅读
- 《Electron实战》
- 《JavaScript高级程序设计》
- 《Node.js设计模式》
14.5 术语表
| 术语 | 说明 |
|---|---|
| Electron | 跨平台桌面应用框架 |
| 渲染进程 | 负责渲染UI的进程 |
| 主进程 | 负责管理应用生命周期的进程 |
| IPlugin | 插件基类接口 |
| BasePlugin | 基础插件类 |
| BaseCustomPlugin | 自定义插件类 |
| Utils | 工具框架 |
| 生命周期 | 插件从加载到卸载的过程 |
| Hook | 钩子函数,在特定时机执行 |
| JSON-RPC | JSON远程过程调用协议 |
十五、详细资料
15.1 官方文档索引
- Typora官方文档:https://support.typora.io/
- 插件框架文档:https://github.com/obgnail/typora_plugin
- DeepWiki文档:https://deepwiki.com/obgnail/typora_plugin
- Custom Plugin Development:https://deepwiki.com/obgnail/typora_plugin/17-custom-plugin-development
15.2 第三方库推荐
| 库名 | 用途 | 说明 |
|---|---|---|
| lodash | 工具函数 | 数据处理和操作 |
| axios | HTTP请求 | API调用 |
| marked | Markdown解析 | 文档处理 |
| highlight.js | 语法高亮 | 代码高亮 |
| mermaid | 图表绘制 | 流程图、时序图 |
15.3 示例代码仓库
- typora_plugin主仓库:https://github.com/obgnail/typora_plugin
- 插件示例集合:plugin/custom/plugins/目录下的示例
- 社区插件:GitHub上搜索"typora plugin"
15.4 社区论坛资源
- GitHub Issues:https://github.com/obgnail/typora_plugin/issues
- CSDN Typora专栏:https://blog.csdn.net/
- V2EX:https://www.v2ex.com/
- Typora官方论坛:https://support.typora.io/Community/
15.5 进阶学习路径
15.5.1 前端技术栈
- JavaScript ES6+:现代JavaScript语法
- DOM操作:文档对象模型操作
- 事件系统:事件捕获和冒泡
- 异步编程:Promise、async/await
- CSS3:布局、动画、响应式
15.5.2 桌面应用开发
- Electron基础:主进程和渲染进程
- IPC通信:进程间通信
- Node.js集成:文件系统、网络
- 打包发布:electron-builder
15.5.3 插件工程化
- 模块化:ES Module、CommonJS
- 测试:单元测试、集成测试
- CI/CD:持续集成和部署
- 文档生成:API文档自动化
本文完,全文约40000字,包含100+代码示例和50+最佳实践建议。
最后更新:2026年7月