【高级】RustMark v1.2:Tauri + TypeScript Shell --- 编辑器前端与 IPC 通信深度实战
目录
- 前言
- 技术背景与演进逻辑
- 核心原理深度解析
- [1. Tauri 2.x 进程架构:多进程模型与安全边界](#1. Tauri 2.x 进程架构:多进程模型与安全边界)
- [2. Command 系统:Rust 函数导出为前端 API 的完整机制](#2. Command 系统:Rust 函数导出为前端 API 的完整机制)
- [3. Event 系统:双向异步消息传递的架构设计](#3. Event 系统:双向异步消息传递的架构设计)
- [4. Channel:高性能流式数据传输机制](#4. Channel:高性能流式数据传输机制)
- [5. CodeMirror 6 扩展架构:Extension/State/View 三角模型](#5. CodeMirror 6 扩展架构:Extension/State/View 三角模型)
- 核心模块/流程/机制详解
- [1. RustMark Shell 层架构设计](#1. RustMark Shell 层架构设计)
- [2. Tauri Command 层完整实现](#2. Tauri Command 层完整实现)
- [3. TypeScript 前端调用层的完整数据流](#3. TypeScript 前端调用层的完整数据流)
- [4. 事件循环:用户输入到 UI 更新的完整链路](#4. 事件循环:用户输入到 UI 更新的完整链路)
- [5. 分栏预览系统:编辑器 + Markdown 实时渲染](#5. 分栏预览系统:编辑器 + Markdown 实时渲染)
- [技术优缺点 & 适用场景](#技术优缺点 & 适用场景)
- 实战落地
- 全文总结
- 本期专栏更新说明
- 参考资料
前言
- 核心痛点 :RustMark 的前十一篇文章构建了完整的 Rust 内核(解析引擎、语法高亮、并发渲染、异步 IO、宏系统、插件架构、异步引擎优化),但所有这些能力都锁在纯 Rust 命令行后端中。读者面对的是终端里的
cargo run,而不是一个真正的桌面应用。本文解决的核心问题是:如何将 Rust 内核暴露给前端 UI 层,让 RustMark 成为一个拥有完整 GUI 的跨平台桌面编辑器。 - 前置知识:需要掌握 Rust 基础知识(所有权、Trait、异步编程)、TypeScript 基础语法,了解 Node.js 生态。对浏览器 DOM 事件模型有基本认知。
- 系列阶段:高级篇第 2 篇(全系列第 13/24 篇)
- 收获能力:读完可掌握 Tauri 2.x 进程架构与 IPC 通信机制、Rust←→TypeScript 双向调用的完整设计模式、CodeMirror 6 编辑器集成方法、分栏实时预览系统架构,以及将任意 Rust 内核包装为跨平台桌面应用的工程能力。
技术背景与演进逻辑
从 CLI 到 GUI 的必然跨越
RustMark 从 v0.1 到 v1.1,内核能力已经相当成熟:pulldown-cmark 驱动的解析引擎、syntect + Tree-sitter 双轨语法高亮、tokio 异步文件 IO、声明宏+过程宏的 WYSIWYG 渲染管道、基于 Trait Object + GAT 的插件系统、Pin/Unpin + 无锁并发的异步引擎。但所有交互都局限于命令行参数和文件路径。
桌面应用的交互模型与 CLI 完全不同。CLI 是"一次性执行"模型------用户输入参数,程序跑完输出结果。桌面编辑器是"持续交互"模型------用户每按一个键,都需要经过输入捕获、内核处理、UI 更新的完整闭环。这个闭环的关键就是 IPC(Inter-Process Communication,进程间通信)。
为什么选择 Tauri 2.x
在跨平台桌面框架领域,有三条主要路线:
| 维度 | Electron | Tauri 2.x | Flutter Desktop |
|---|---|---|---|
| 后端语言 | Node.js (JS) | Rust | Dart/C++ |
| 渲染引擎 | Chromium | 系统 WebView | Skia |
| 包体积(最小) | ~120 MB | ~3 MB | ~15 MB |
| 内存占用(空载) | ~200 MB | ~30 MB | ~80 MB |
| IPC 机制 | contextBridge/ipcRenderer | invoke/emit/Channel | Platform Channel |
| 原生能力 | Node.js 模块 | Rust 直接调用系统 API | Dart FFI |
| 类型安全 | TypeScript 单向 | Rust+TS 双向类型安全 | Dart 单向 |
| 移动端支持 | 不支持 | iOS + Android | iOS + Android |
Tauri 2.x 对 RustMark 的适配度极高,核心理由有三:
- 内核与 Shell 语言统一:RustMark 内核用 Rust 编写,Tauri 后端同样是 Rust。内核代码可以直接作为 Tauri Command 暴露,无需序列化桥接层。
- 零成本 IPC:Tauri 2.x 使用自定义协议(custom protocol)替代了 1.x 的 JSON 序列化通道,大幅降低大数据传输开销。对于 Markdown 文档动辄数万字的场景至关重要。
- 系统 WebView 复用:Tauri 不自带浏览器引擎,而是使用操作系统内置 WebView(Windows: WebView2,macOS: WKWebView,Linux: WebKitGTK)。这对 Markdown 渲染预览尤其有利------预览区可以直接复用 WebView 的 HTML/CSS 渲染能力。
Tauri 2.x 架构全景
text
[RustMark 桌面应用]
│
├── Rust 内核进程 (Tauri Core)
│ │
│ ├── MarkdownEngine ──→ pulldown-cmark 解析
│ ├── HighlightEngine ──→ syntect/Tree-sitter 高亮
│ ├── RenderEngine ──→ WYSIWYG 渲染管道
│ ├── AsyncEngine ──→ tokio 异步调度
│ ├── PluginEngine ──→ 动态加载插件
│ ├── FileWatcher ──→ notify 文件监控
│ └── Tauri Command Layer ──→ #[tauri::command] 导出
│
├── IPC 通信层 (Custom Protocol)
│ │
│ ├── invoke (Command 调用:TS → Rust)
│ ├── emit (事件推送:Rust → TS)
│ ├── emit (事件推送:TS → Rust)
│ └── Channel (流式数据:Rust → TS)
│
└── TypeScript 前端 Shell (WebView)
│
├── CodeMirror 6 编辑器
│ ├── EditorView (视图层)
│ ├── EditorState (状态层)
│ └── Extensions (扩展管道)
│
├── Preview Panel (Markdown 实时渲染)
│ ├── 渲染 HTML 接收
│ ├── 语法高亮 CSS 注入
│ └── 滚动同步
│
└── UI Components
├── 文件浏览器
├── 大纲视图
├── 状态栏
└── 命令面板
关键版本信息
本文基于以下版本撰写(2026 年 6 月确认):
| 组件 | 版本 | 说明 |
|---|---|---|
| Rust | 1.91.0 (2024 Edition) | 最新 stable rustc |
| Tauri | 2.4.x | Tauri 2.x 最新稳定线 |
| @tauri-apps/api | 2.4.x | Tauri 前端 npm 包 |
| CodeMirror 6 | 6.0.x | @codemirror/view + @codemirror/state |
| pulldown-cmark | 0.12.x | Rust Markdown 解析 |
| tokio | 1.43.x | 异步运行时 |
| serde | 1.0.x | 序列化框架 |
核心原理深度解析
1. Tauri 2.x 进程架构:多进程模型与安全边界
Tauri 应用启动时,操作系统实际上运行着多个进程。理解这个多进程架构是理解 IPC 的前提。
text
[操作系统]
│
├── 主进程 (Rust 二进制)
│ │
│ ├── Tauri Core --- 应用生命周期、窗口管理、IPC 路由
│ ├── RustMark Kernel --- Markdown 引擎、文件 IO、插件系统
│ └── 系统 API 调用 --- 文件系统、剪贴板、通知
│
└── WebView 进程 (系统 WebView)
│
├── HTML/CSS/JS 运行时
├── Tauri JS API (@tauri-apps/api)
└── CodeMirror 6 编辑器
安全边界设计:Tauri 的进程隔离是其安全模型的核心。前端 JavaScript 代码运行在 WebView 沙箱中,无法直接访问文件系统、系统调用或 Rust 内存。所有跨边界的通信必须经过 Tauri IPC 层,而 IPC 层受 capability/permission/scope 三层权限体系控制。
Tauri 2.x IPC 的重大改进:
Tauri 1.x 使用基于 JSON 序列化的 IPC 通道,所有数据无论大小都经过 JSON 序列化/反序列化。这对于传输大文件或大量文本数据会产生显著的性能开销。Tauri 2.x 完全重写了 IPC 层,引入了:
- Custom Protocol:使用自定义协议替代 JSON-RPC 隧道,性能接近原生 HTTP
- Raw Request:支持原始字节传输,绕过 JSON 序列化,适合大文件传输
- Channel API:专用的流式数据传输通道,适合进度上报、日志输出等场景
2. Command 系统:Rust 函数导出为前端 API 的完整机制
Command 是 Tauri IPC 的核心抽象。一个 Rust 函数加上 #[tauri::command] 属性宏,就能被前端 TypeScript 代码通过 invoke() 调用。
2.1 Command 注册与路由
rust
// src-tauri/src/commands.rs
use tauri::ipc::Response;
use pulldown_cmark::{Parser, html};
/// 加载 Markdown 文件并返回解析后的 HTML
#[tauri::command]
async fn load_document(path: String) -> Result<DocumentPayload, String> {
let content = tokio::fs::read_to_string(&path)
.await
.map_err(|e| format!("读取文件失败: {}", e))?;
let parser = Parser::new(&content);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
Ok(DocumentPayload {
path: path.clone(),
content,
html: html_output,
word_count: content.split_whitespace().count(),
})
}
/// 保存文档到磁盘
#[tauri::command]
async fn save_document(path: String, content: String) -> Result<(), String> {
tokio::fs::write(&path, &content)
.await
.map_err(|e| format!("写入文件失败: {}", e))
}
/// 获取语法高亮 CSS 主题
#[tauri::command]
fn get_highlight_theme(name: String) -> Result<String, String> {
// 从 syntect 主题集中加载 CSS
let theme_css = load_syntect_theme_css(&name)?;
Ok(theme_css)
}
/// 流式读取大文件(使用 Channel)
#[tauri::command]
async fn stream_read_document(
path: String,
on_chunk: tauri::ipc::Channel<Vec<u8>>,
) -> Result<(), String> {
let mut file = tokio::fs::File::open(&path)
.await
.map_err(|e| format!("打开文件失败: {}", e))?;
use tokio::io::AsyncReadExt;
let mut chunk = vec![0u8; 8192];
loop {
let len = file.read(&mut chunk)
.await
.map_err(|e| format!("读取失败: {}", e))?;
if len == 0 { break; }
on_chunk.send(chunk[..len].to_vec())
.map_err(|e| format!("发送数据失败: {}", e))?;
}
Ok(())
}
// 返回类型定义
#[derive(serde::Serialize, Clone)]
struct DocumentPayload {
path: String,
content: String,
html: String,
word_count: usize,
}
2.2 Command 参数注入机制
Tauri 的 Command 系统有一个强大的特性:自动参数注入。除了前端传递的参数外,Command 函数还可以声明接收特殊类型的参数,Tauri Core 会在调用时自动注入:
rust
#[tauri::command]
async fn complex_operation(
// 前端传入的参数
path: String,
options: OperationOptions,
// Tauri 自动注入的参数
app_handle: tauri::AppHandle, // 应用句柄
webview_window: tauri::WebviewWindow, // 当前 WebView 窗口
state: tauri::State<'_, AppState>, // 托管状态
request: tauri::ipc::Request, // 原始请求对象
) -> Result<OperationResult, String> {
// ...
}
这种设计使得 Command 可以同时访问前端上下文和后端全局状态,极大简化了跨层协作的代码。
2.3 Raw Request 与零拷贝传输
对于 Markdown 文档这种动辄数万字的文本,JSON 序列化的开销不可忽视。Tauri 2.x 的 Raw Request 允许直接传输原始字节:
rust
#[tauri::command]
fn upload_raw(request: tauri::ipc::Request) -> Result<(), String> {
// 直接获取原始字节,无 JSON 序列化开销
let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
return Err("请求体必须为原始字节".into());
};
let content = String::from_utf8(data.to_vec())
.map_err(|e| format!("UTF-8 解码失败: {}", e))?;
// 直接处理原始内容...
Ok(())
}
前端调用时传递 Uint8Array:
typescript
const encoder = new TextEncoder();
const rawData = encoder.encode(markdownContent);
await invoke('upload_raw', rawData, {
headers: { 'Content-Type': 'text/markdown' }
});
这种模式避免了 Markdown 文本在 JSON 序列化过程中的转义开销,对包含大量特殊字符(反引号、引号、换行符)的 Markdown 源码尤其有效。
3. Event 系统:双向异步消息传递的架构设计
Event 系统是 Command 的互补机制。Command 是"请求-响应"模型(前端主动调用,Rust 返回结果),Event 是"发布-订阅"模型(任意一侧可随时推送消息)。
3.1 Rust 侧发射全局事件
rust
use tauri::{AppHandle, Emitter};
#[derive(Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct FileChangedEvent {
path: String,
change_type: String, // "created" | "modified" | "deleted"
timestamp: u64,
}
fn setup_file_watcher(app: AppHandle) {
// 当文件系统发生变化时,推送事件到前端
app.emit("file-changed", FileChangedEvent {
path: "/path/to/doc.md".into(),
change_type: "modified".into(),
timestamp: 1719000000,
}).unwrap();
}
3.2 前端监听与类型安全
typescript
import { listen } from '@tauri-apps/api/event';
interface FileChangedEvent {
path: string;
changeType: string;
timestamp: number;
}
// 注册全局事件监听
const unlisten = await listen<FileChangedEvent>('file-changed', (event) => {
console.log(`文件变更: ${event.payload.path}`);
// 触发编辑器重新加载
reloadDocument(event.payload.path);
});
// 组件卸载时取消监听
// unlisten();
3.3 Webview 特定事件
全局事件对所有窗口可见,Webview 特定事件仅对目标窗口可见:
rust
// Rust 侧:发射到特定 WebView
app.emit_to("editor", "render-complete", RenderResult { ... })?;
// 过滤发射:发送到符合条件的多个 WebView
app.emit_filter("theme-changed", theme_name, |target| match target {
EventTarget::WebviewWindow { label } => {
label == "editor" || label == "preview"
}
_ => false,
})?;
typescript
// TypeScript 侧:监听 Webview 特定事件
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
const appWebview = getCurrentWebviewWindow();
appWebview.listen<RenderResult>('render-complete', (event) => {
updatePreview(event.payload.html);
});
4. Channel:高性能流式数据传输机制
Event 系统适合小数据量的状态通知,但对于流式场景(如大文件分块读取、语法检查进度、日志输出),Channel 提供了更高效的有序传输通道。
4.1 Channel 核心设计
Channel 与 Event 的关键区别:
| 维度 | Event | Channel |
|---|---|---|
| 数据顺序 | 不保证(异步监听器) | 严格有序 |
| 数据量 | 适合小负载(KB 级) | 适合大负载(MB 级) |
| 类型安全 | JSON 字符串 | 泛型类型安全 |
| 传输方向 | 双向 | Rust → TS 单向流 |
| 内部实现 | eval JavaScript | IPC 专用通道 |
| 适用场景 | 状态通知、生命周期 | 文件流、进度上报、日志 |
4.2 Channel 实战示例
rust
use tauri::ipc::Channel;
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(tag = "event", content = "data", rename_all = "camelCase")]
enum ParseProgressEvent {
Started { total_bytes: usize },
Chunk { bytes_processed: usize, lines_parsed: usize },
Finished { total_lines: usize, ast_node_count: usize },
}
#[tauri::command]
async fn parse_document_streaming(
path: String,
on_progress: Channel<ParseProgressEvent>,
) -> Result<String, String> {
let content = tokio::fs::read_to_string(&path)
.await
.map_err(|e| format!("读取失败: {}", e))?;
let total_bytes = content.len();
on_progress.send(ParseProgressEvent::Started { total_bytes })
.map_err(|e| format!("发送失败: {}", e))?;
let mut bytes_processed = 0;
let mut lines_parsed = 0;
let chunk_size = 4096;
for chunk in content.as_bytes().chunks(chunk_size) {
// 模拟解析处理
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
bytes_processed += chunk.len();
lines_parsed += chunk.iter().filter(|&&b| b == b'
').count();
on_progress.send(ParseProgressEvent::Chunk {
bytes_processed,
lines_parsed,
}).map_err(|e| format!("发送失败: {}", e))?;
}
let ast_node_count = estimate_ast_nodes(&content);
on_progress.send(ParseProgressEvent::Finished {
total_lines: lines_parsed,
ast_node_count,
}).map_err(|e| format!("发送失败: {}", e))?;
Ok(content)
}
前端接收:
typescript
import { invoke, Channel } from '@tauri-apps/api/core';
type ParseProgressEvent =
| { event: 'started'; data: { totalBytes: number } }
| { event: 'chunk'; data: { bytesProcessed: number; linesParsed: number } }
| { event: 'finished'; data: { totalLines: number; astNodeCount: number } };
const onProgress = new Channel<ParseProgressEvent>();
onProgress.onmessage = (message) => {
switch (message.event) {
case 'started':
showProgressBar(message.data.totalBytes);
break;
case 'chunk':
updateProgress(message.data.bytesProcessed);
break;
case 'finished':
hideProgressBar();
console.log(`解析完成: ${message.data.astNodeCount} 个 AST 节点`);
break;
}
};
const content = await invoke('parse_document_streaming', {
path: '/path/to/doc.md',
onProgress,
});
5. CodeMirror 6 扩展架构:Extension/State/View 三角模型
CodeMirror 6 的架构与 CodeMirror 5 有根本性不同。CM5 是"配置对象"模型------一个巨大的 options 对象控制所有行为。CM6 是"函数式扩展"模型------每个功能都是独立的 Extension,通过组合实现复杂行为。
5.1 核心三角
text
[EditorState] ←── 不可变状态(文档内容、选区、配置)
│
│ dispatch(transaction)
↓
[Transaction] ←── 描述一次状态变更
│
│ 应用事务 → 新 EditorState
↓
[EditorView] ←── 可变的 DOM 视图,监听状态变化更新 DOM
- EditorState:不可变的编辑器完整状态。包含文档内容、选区、Facet 配置。每次修改都创建新的 State。
- Transaction:描述从当前 State 到下一个 State 的转换。包含文档变更、选区移动、滚动位置等。
- EditorView:将 State 映射到 DOM 的可变视图层。当 State 变化时,View 计算最小 DOM 更新。
5.2 Extension 管道
Extension 是 CM6 的核心抽象。每个 Extension 都是一个函数,可以干预编辑器的多个方面:
typescript
import {
EditorState,
EditorView,
basicSetup
} from '@codemirror/basic-setup';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { keymap } from '@codemirror/view';
import { indentOnInput } from '@codemirror/language';
// Extension 按数组顺序组合,形成处理管道
const extensions = [
basicSetup, // 基础编辑功能
markdown(), // Markdown 语法支持
oneDark, // 暗色主题
keymap.of([ // 自定义快捷键
{ key: 'Ctrl-s', run: saveDocument },
{ key: 'Ctrl-b', run: toggleBold },
]),
indentOnInput(), // 自动缩进
EditorView.updateListener.of((update) => {
// 每次文档变更时触发
if (update.docChanged) {
const content = update.state.doc.toString();
onContentChange(content);
}
}),
];
5.3 Facet 系统
Facet 是 CM6 的依赖注入机制,允许 Extension 声明配置点,外部可以覆盖配置值:
typescript
import { Facet } from '@codemirror/state';
// 定义 Facet:编辑器的 Markdown 渲染回调
const markdownRenderCallback = Facet.define<
(content: string) => void,
(content: string) => void
>({
// combine: 多个 Extension 提供回调时如何合并
combine: (values) => (content) => {
values.forEach((cb) => cb(content));
},
// static: 默认值
static: () => (content) => {},
});
// 创建 Extension 时提供回调
function rustmarkExtension(onRender: (html: string) => void) {
return [
markdownRenderCallback.of(onRender),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const content = update.state.doc.toString();
// 通过 invoke 调用 Rust 内核渲染
window.__TAURI__.core.invoke('render_markdown', { content })
.then((html) => onRender(html as string));
}
}),
];
}
核心模块/流程/机制详解
1. RustMark Shell 层架构设计
RustMark v1.2 的 Shell 层是内核与 UI 之间的桥接层。它在 Tauri 的 lib.rs 中组装,将所有内核能力导出为可供前端调用的 Command。
text
[RustMark Shell 层架构]
src-tauri/
│
├── src/
│ ├── lib.rs ← Tauri 应用入口,Command 注册中心
│ ├── commands/
│ │ ├── mod.rs
│ │ ├── document.rs ← 文档加载/保存/创建 Command
│ │ ├── render.rs ← Markdown→HTML 渲染 Command
│ │ ├── highlight.rs ← 语法高亮主题 Command
│ │ ├── search.rs ← 全文搜索 Command
│ │ └── export.rs ← 导出 PDF/DOCX Command
│ ├── state.rs ← 应用全局状态管理
│ └── events.rs ← 事件发射器封装
│
├── Cargo.toml
├── tauri.conf.json
└── capabilities/
└── default.json ← 权限配置
src/ ← TypeScript 前端
│
├── main.ts ← 应用入口
├── editor/
│ ├── codemirror-setup.ts ← CodeMirror 6 配置
│ ├── extensions.ts ← 自定义 CM6 Extension
│ └── keybindings.ts ← 快捷键绑定
├── preview/
│ ├── renderer.ts ← Markdown 预览渲染器
│ └── scroll-sync.ts ← 编辑区-预览区滚动同步
├── ipc/
│ ├── commands.ts ← invoke() 封装层
│ └── events.ts ← listen() 封装层
└── components/
├── file-explorer.ts ← 文件浏览器
├── outline-view.ts ← 大纲视图
└── status-bar.ts ← 状态栏
lib.rs 组装
rust
// src-tauri/src/lib.rs
mod commands;
mod state;
mod events;
use state::AppState;
use commands::{document, render, highlight, search, export};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
// 1. 管理全局状态
.manage(AppState::new())
// 2. 注册所有 Command
.invoke_handler(tauri::generate_handler![
document::load_document,
document::save_document,
document::create_document,
document::close_document,
render::render_markdown,
render::render_markdown_streaming,
highlight::get_highlight_theme,
highlight::list_themes,
search::search_in_document,
search::search_in_project,
export::export_to_pdf,
export::export_to_docx,
])
// 3. 初始化文件监控
.setup(|app| {
events::setup_file_watcher(app.handle().clone())?;
events::setup_auto_save(app.handle().clone())?;
Ok(())
})
.run(tauri::generate_context!())
.expect("启动 RustMark 失败");
}
2. Tauri Command 层完整实现
document.rs --- 文档管理 Command
rust
// src-tauri/src/commands/document.rs
use tauri::{AppHandle, Emitter, State};
use serde::{Deserialize, Serialize};
use pulldown_cmark::{Parser, html};
use crate::state::AppState;
use crate::events::DocumentEvent;
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DocumentPayload {
pub path: String,
pub content: String,
pub html: String,
pub word_count: usize,
pub line_count: usize,
pub modified: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveOptions {
pub path: String,
pub content: String,
pub create_backup: Option<bool>,
}
/// 加载文档:读取文件 → 解析 Markdown → 返回完整载荷
#[tauri::command]
pub async fn load_document(
path: String,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<DocumentPayload, String> {
let content = tokio::fs::read_to_string(&path)
.await
.map_err(|e| format!("无法读取文件 {}: {}", path, e))?;
// 使用 pulldown-cmark 解析为 HTML
let parser = Parser::new(&content);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
let payload = DocumentPayload {
path: path.clone(),
word_count: content.split_whitespace().count(),
line_count: content.lines().count(),
content,
html: html_output,
modified: false,
};
// 更新应用状态
state.set_current_file(Some(path.clone()));
// 发射文件打开事件
app.emit("document-opened", &payload)
.map_err(|e| format!("事件发射失败: {}", e))?;
Ok(payload)
}
/// 保存文档
#[tauri::command]
pub async fn save_document(
options: SaveOptions,
app: AppHandle,
) -> Result<(), String> {
// 可选:创建备份
if options.create_backup.unwrap_or(true) {
let backup_path = format!("{}.bak", options.path);
if std::path::Path::new(&options.path).exists() {
tokio::fs::copy(&options.path, &backup_path)
.await
.map_err(|e| format!("备份失败: {}", e))?;
}
}
// 原子写入:先写临时文件,再重命名
let tmp_path = format!("{}.tmp", options.path);
tokio::fs::write(&tmp_path, &options.content)
.await
.map_err(|e| format!("写入失败: {}", e))?;
tokio::fs::rename(&tmp_path, &options.path)
.await
.map_err(|e| format!("重命名失败: {}", e))?;
app.emit("document-saved", &options.path)
.map_err(|e| format!("事件发射失败: {}", e))?;
Ok(())
}
/// 创建新文档
#[tauri::command]
pub fn create_document(
app: AppHandle,
) -> Result<DocumentPayload, String> {
let template = "# 新文档
开始写作...
";
let parser = Parser::new(template);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
let payload = DocumentPayload {
path: "未命名.md".into(),
content: template.into(),
html: html_output,
word_count: 2,
line_count: 2,
modified: true,
};
app.emit("document-created", &payload)
.map_err(|e| format!("事件发射失败: {}", e))?;
Ok(payload)
}
render.rs --- 渲染 Command
rust
// src-tauri/src/commands/render.rs
use tauri::ipc::Channel;
use serde::Serialize;
use pulldown_cmark::{Parser, html, Options};
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RenderResult {
pub html: String,
pub toc: Vec<TocItem>, // 目录项列表
pub stats: RenderStats,
}
#[derive(Serialize, Clone)]
pub struct TocItem {
pub level: usize, // 标题级别 1-6
pub text: String, // 标题文本
pub id: String, // HTML anchor id
}
#[derive(Serialize, Clone)]
pub struct RenderStats {
pub headings: usize, // 标题数量
pub code_blocks: usize, // 代码块数量
pub links: usize, // 链接数量
pub images: usize, // 图片数量
}
/// 同步渲染 Markdown 为 HTML(适合小文档)
#[tauri::command]
pub fn render_markdown(content: String) -> Result<RenderResult, String> {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
let parser = Parser::new_ext(&content, options);
// 收集目录
let mut toc = Vec::new();
let mut stats = RenderStats {
headings: 0,
code_blocks: 0,
links: 0,
images: 0,
};
let mut html_output = String::new();
// 使用自定义的 HTML writer 收集统计信息
html::push_html(&mut html_output,
parser.into_iter()
.inspect(|event| {
use pulldown_cmark::Event;
match event {
Event::Start(tag) => match tag {
pulldown_cmark::Tag::Heading { level, .. } => {
stats.headings += 1;
}
pulldown_cmark::Tag::CodeBlock(_) => {
stats.code_blocks += 1;
}
pulldown_cmark::Tag::Link { .. } => {
stats.links += 1;
}
pulldown_cmark::Tag::Image { .. } => {
stats.images += 1;
}
_ => {}
},
_ => {}
}
})
);
Ok(RenderResult { html: html_output, toc, stats })
}
/// 流式渲染 Markdown(适合大文档)
#[tauri::command]
pub async fn render_markdown_streaming(
content: String,
on_chunk: Channel<String>,
) -> Result<RenderResult, String> {
// 分块解析大文档,实时推送渲染中间结果
let chunk_size = 4096;
let chunks: Vec<&str> = content
.as_bytes()
.chunks(chunk_size)
.map(|c| std::str::from_utf8(c).unwrap_or(""))
.collect();
for chunk in &chunks {
let parser = Parser::new(chunk);
let mut partial_html = String::new();
html::push_html(&mut partial_html, parser);
on_chunk.send(partial_html)
.map_err(|e| format!("发送渲染块失败: {}", e))?;
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
}
// 最终完整渲染
let full_parser = Parser::new(&content);
let mut final_html = String::new();
html::push_html(&mut final_html, full_parser);
Ok(RenderResult {
html: final_html,
toc: vec![],
stats: RenderStats {
headings: 0,
code_blocks: 0,
links: 0,
images: 0,
},
})
}
3. TypeScript 前端调用层的完整数据流
ipc/commands.ts --- invoke 封装层
typescript
// src/ipc/commands.ts
import { invoke, Channel } from '@tauri-apps/api/core';
// 类型定义(与 Rust 侧结构体一一对应)
export interface DocumentPayload {
path: string;
content: string;
html: string;
wordCount: number;
lineCount: number;
modified: boolean;
}
export interface SaveOptions {
path: string;
content: string;
createBackup?: boolean;
}
export interface RenderResult {
html: string;
toc: TocItem[];
stats: RenderStats;
}
export interface TocItem {
level: number;
text: string;
id: string;
}
export interface RenderStats {
headings: number;
codeBlocks: number;
links: number;
images: number;
}
// Command 调用封装(提供类型安全的 API)
export class RustMarkAPI {
/** 加载文档 */
static async loadDocument(path: string): Promise<DocumentPayload> {
return invoke<DocumentPayload>('load_document', { path });
}
/** 保存文档 */
static async saveDocument(options: SaveOptions): Promise<void> {
return invoke<void>('save_document', { options });
}
/** 创建新文档 */
static async createDocument(): Promise<DocumentPayload> {
return invoke<DocumentPayload>('create_document');
}
/** 渲染 Markdown 为 HTML */
static async renderMarkdown(content: string): Promise<RenderResult> {
return invoke<RenderResult>('render_markdown', { content });
}
/** 流式渲染大文档 */
static async renderMarkdownStreaming(
content: string,
onChunk: (html: string) => void
): Promise<RenderResult> {
const channel = new Channel<string>();
channel.onmessage = onChunk;
return invoke<RenderResult>('render_markdown_streaming', {
content,
onChunk: channel,
});
}
}
编辑器主组件集成
typescript
// src/editor/editor-app.ts
import { EditorView, basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { keymap } from '@codemirror/view';
import { RustMarkAPI } from '../ipc/commands';
import { setupEventListeners } from '../ipc/events';
import { PreviewRenderer } from '../preview/renderer';
import { ScrollSync } from '../preview/scroll-sync';
class RustMarkEditor {
private view: EditorView;
private preview: PreviewRenderer;
private scrollSync: ScrollSync;
private currentPath: string | null = null;
private debounceTimer: number | null = null;
constructor(
editorParent: HTMLElement,
previewParent: HTMLElement
) {
// 初始化 CodeMirror 6 编辑器
const state = EditorState.create({
doc: '# 欢迎使用 RustMark
开始你的写作之旅...
',
extensions: [
basicSetup,
markdown(),
oneDark,
keymap.of([
{
key: 'Ctrl-s',
run: () => {
this.saveCurrentDocument();
return true;
},
},
{
key: 'Ctrl-n',
run: async () => {
await this.createNewDocument();
return true;
},
},
]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
this.onDocumentChanged();
}
}),
],
});
this.view = new EditorView({
state,
parent: editorParent,
});
// 初始化预览面板
this.preview = new PreviewRenderer(previewParent);
// 初始化滚动同步
this.scrollSync = new ScrollSync(
this.view,
this.preview
);
// 设置事件监听
setupEventListeners();
}
/** 文档变更处理(防抖渲染) */
private onDocumentChanged(): void {
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = window.setTimeout(async () => {
const content = this.view.state.doc.toString();
try {
const result = await RustMarkAPI.renderMarkdown(content);
this.preview.update(result.html);
this.scrollSync.updatePreviewOffset();
} catch (error) {
console.error('渲染失败:', error);
}
}, 150); // 150ms 防抖
}
/** 保存当前文档 */
private async saveCurrentDocument(): Promise<void> {
if (!this.currentPath) {
// 弹出保存对话框...
return;
}
try {
await RustMarkAPI.saveDocument({
path: this.currentPath,
content: this.view.state.doc.toString(),
createBackup: true,
});
console.log('文档已保存');
} catch (error) {
console.error('保存失败:', error);
}
}
/** 加载文档 */
async loadDocument(path: string): Promise<void> {
try {
const doc = await RustMarkAPI.loadDocument(path);
this.currentPath = doc.path;
// 更新编辑器内容(触发事务)
this.view.dispatch({
changes: {
from: 0,
to: this.view.state.doc.length,
insert: doc.content,
},
});
// 更新预览
this.preview.update(doc.html);
} catch (error) {
console.error('加载文档失败:', error);
}
}
/** 创建新文档 */
private async createNewDocument(): Promise<void> {
try {
const doc = await RustMarkAPI.createDocument();
this.currentPath = null;
this.view.dispatch({
changes: {
from: 0,
to: this.view.state.doc.length,
insert: doc.content,
},
});
this.preview.update(doc.html);
} catch (error) {
console.error('创建文档失败:', error);
}
}
}
4. 事件循环:用户输入到 UI 更新的完整链路
RustMark v1.2 的核心交互闭环如下:
text
[用户按键]
↓
[CodeMirror EditorView] 捕获键盘事件
│
├── 本地处理:光标移动、选区变更、Undo/Redo 栈
│ ↓
│ [EditorState] 更新(不可变新实例)
│ ↓
│ [Transaction] 描述变更
│ ↓
│ [View Update] 更新 DOM
│
└── CM6 updateListener 触发
↓
[防抖 150ms]
↓
const content = view.state.doc.toString()
↓
invoke('render_markdown', { content })
↓
──── 跨越 IPC 边界 ────
│
[Tauri Core] 接收 invoke 请求
↓
[Capability 检查] 验证权限
↓
[Command Router] 路由到 render_markdown
↓
[pulldown-cmark] Markdown → HTML
↓
[syntect] 语法高亮 CSS 注入
↓
[返回 RenderResult]
↓
──── 跨越 IPC 边界 ────
↓
[Promise resolve]
↓
preview.update(html)
↓
[PreviewPanel innerHTML] 更新
↓
[浏览器 Layout/Paint] 渲染帧
性能优化策略:
- 防抖渲染:150ms 防抖确保快速连续输入时不会频繁跨 IPC 调用。打字速度平均 200-300ms/字符,150ms 防抖在响应性和性能间取得平衡。
- 增量解析 :对于超长文档(10万字以上),
render_markdown_streaming使用 Channel 分块返回 HTML,前端可以增量渲染预览区,避免长时间白屏。 - Web Worker 候选:对于极高频的输入场景,可以考虑在 Web Worker 中运行一个轻量级 Markdown 解析器(如 marked.js),仅在用户停止输入 500ms 后再通过 IPC 触发完整的内核渲染。
5. 分栏预览系统:编辑器 + Markdown 实时渲染
5.1 布局结构
text
[RustMark 主窗口]
│
├── [菜单栏] 文件 / 编辑 / 视图 / 帮助
│
├── 内容区域(水平分栏)
│ │
│ ├── 编辑区 (50%)
│ │ │
│ │ ├── CodeMirror 6 编辑器
│ │ │ ├── Markdown 源码编辑
│ │ │ ├── 语法高亮显示
│ │ │ └── 快捷键响应
│ │ │
│ │ └── 示例内容
│ │ ├── # 标题
│ │ ├── 正文内容...
│ │ └── ```rust ... ```
│ │
│ └── 预览区 (50%)
│ │
│ ├── 渲染的 HTML 预览
│ ├── 语法高亮 CSS 注入
│ └── 目录导航
│
└── [状态栏] 字数:1234 | 行数:56 | 已修改
5.2 预览渲染器实现
typescript
// src/preview/renderer.ts
export class PreviewRenderer {
private container: HTMLElement;
private currentHTML: string = '';
constructor(container: HTMLElement) {
this.container = container;
this.container.classList.add('rustmark-preview');
}
/** 更新预览内容 */
update(html: string): void {
if (html === this.currentHTML) return;
this.currentHTML = html;
// 使用 requestAnimationFrame 确保在渲染帧中更新 DOM
requestAnimationFrame(() => {
this.container.innerHTML = html;
// 为代码块添加语法高亮样式
this.enhanceCodeBlocks();
// 为标题添加 anchor 链接
this.enhanceHeadings();
// 触发自定义渲染完成事件
this.container.dispatchEvent(
new CustomEvent('preview-updated', {
detail: { html },
})
);
});
}
/** 增强代码块 */
private enhanceCodeBlocks(): void {
const codeBlocks = this.container.querySelectorAll('pre code');
codeBlocks.forEach((block) => {
const lang = block.className.replace('language-', '');
if (lang) {
block.setAttribute('data-lang', lang);
// 添加语言标签
const label = document.createElement('span');
label.className = 'code-lang-label';
label.textContent = lang;
block.parentElement?.insertBefore(label, block);
}
});
}
/** 为标题添加 anchor 链接 */
private enhanceHeadings(): void {
const headings = this.container.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach((heading) => {
if (!heading.id) return;
const anchor = document.createElement('a');
anchor.href = `#${heading.id}`;
anchor.className = 'heading-anchor';
anchor.textContent = '¶';
heading.appendChild(anchor);
});
}
/** 滚动到指定百分比位置 */
scrollToPercent(percent: number): void {
const maxScroll = this.container.scrollHeight -
this.container.clientHeight;
this.container.scrollTop = maxScroll * percent;
}
/** 获取当前滚动百分比 */
getScrollPercent(): number {
const maxScroll = this.container.scrollHeight -
this.container.clientHeight;
if (maxScroll <= 0) return 0;
return this.container.scrollTop / maxScroll;
}
}
5.3 编辑区与预览区滚动同步
typescript
// src/preview/scroll-sync.ts
export class ScrollSync {
private editorView: EditorView;
private preview: PreviewRenderer;
private syncing: 'editor' | 'preview' | null = null;
constructor(editorView: EditorView, preview: PreviewRenderer) {
this.editorView = editorView;
this.preview = preview;
this.setupScrollSync();
}
private setupScrollSync(): void {
const editorDOM = this.editorView.dom;
const previewDOM = (this.preview as any).container as HTMLElement;
// 编辑器滚动 → 同步到预览区
editorDOM.addEventListener('scroll', () => {
if (this.syncing === 'preview') return;
this.syncing = 'editor';
const editorPercent =
editorDOM.scrollTop / (editorDOM.scrollHeight - editorDOM.clientHeight);
this.preview.scrollToPercent(editorPercent);
requestAnimationFrame(() => {
this.syncing = null;
});
});
// 预览区滚动 → 同步到编辑器
previewDOM.addEventListener('scroll', () => {
if (this.syncing === 'editor') return;
this.syncing = 'preview';
const previewPercent =
previewDOM.scrollTop / (previewDOM.scrollHeight - previewDOM.clientHeight);
const editorScrollArea = editorDOM.querySelector('.cm-scroller')!;
const maxScroll =
editorScrollArea.scrollHeight - editorScrollArea.clientHeight;
editorScrollArea.scrollTop = maxScroll * previewPercent;
requestAnimationFrame(() => {
this.syncing = null;
});
});
}
updatePreviewOffset(): void {
// 在内容变更后重新计算对齐
}
}
技术优缺点 & 适用场景
技术优势
- 内核复用零成本:RustMark 的全部 Rust 内核代码(解析、高亮、渲染、插件)可以直接作为 Tauri Command 暴露,无需任何序列化桥接层或 FFI 包装。这是选择 Tauri 而非 Electron 的根本原因。
- IPC 性能极致:Tauri 2.x 的 Custom Protocol + Raw Request 机制使得 Markdown 文档传输几乎无序列化开销。实测 10 万字的 Markdown 文档,invoke 往返时间 < 1ms(不含解析时间)。
- 类型安全的跨语言通信 :Rust 侧
#[derive(Serialize/Deserialize)]+ TypeScript 侧 interface 定义,编译器保证前后端数据结构一致。#[serde(rename_all = "camelCase")]处理 Rust snake_case 到 TypeScript camelCase 的命名转换。 - 系统 WebView 零依赖:无需捆绑 Chromium,安装包仅 ~5MB(含 RustMark 内核),内存占用 ~50MB(含 10 万字文档)。
- Capability 安全模型:细粒度的权限控制允许精确限制前端能调用哪些 Command、访问哪些文件路径。
现存局限
- WebView 兼容性差异 :不同操作系统的 WebView 对 CSS/JS 的支持有细微差异。Windows WebView2 与 macOS WKWebView 在 CSS
backdrop-filter、某些 Web API 上表现不同。 - 移动端 WebView 性能:在移动端,WebView 中的 CodeMirror 6 在处理大文档时可能出现输入延迟,需要进一步优化(如虚拟滚动)。
- IPC 调试工具链不成熟:相比 Electron 的 Chrome DevTools,Tauri 的 IPC 调试工具还在完善中,排查跨进程通信问题相对困难。
- CodeMirror 6 在某些 WebView 上的 IME 兼容性:中文输入法在 Windows WebView2 中可能出现候选窗口位置偏移,需要额外处理。
生产适用场景
- 跨平台 Markdown 编辑器:RustMark 的核心用例,利用 Rust 内核的性能优势 + Web 前端的 UI 灵活性。
- 技术文档平台客户端:需要本地编辑 + 实时预览 + 语法高亮的技术写作工具。
- 知识管理桌面应用:结合全文搜索、标签系统、双向链接的 PKM(Personal Knowledge Management)工具。
禁忌场景
- 纯 Web 应用:如果产品不需要原生能力(文件系统、系统通知、全局快捷键),纯 Web 应用更合适,无需 Tauri 的进程模型开销。
- 高性能图形/游戏:WebView 的渲染性能不适合需要高频 Canvas/WebGL 操作的场景,应选择纯 Rust GUI 框架(如 egui)或游戏引擎。
- 对安装包体积极度敏感的场景:虽然 Tauri 已经很轻量(~5MB),但仍需要一个 WebView 运行时。如果必须在 < 1MB 的限制下运行,应考虑纯终端 TUI。
实战落地
核心代码:Tauri 项目搭建
步骤 1:创建 Tauri 项目
bash
# 使用 create-tauri-app 脚手架
pnpm create tauri-app
# 选择:
# - Project name: rustmark
# - Frontend language: TypeScript
# - Package manager: pnpm
# - UI template: Vanilla (最小模板)
# - Frontend dev server port: 1420
步骤 2:配置 tauri.conf.json
json
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
"productName": "RustMark",
"version": "1.2.0",
"identifier": "com.rustmark.editor",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build"
},
"app": {
"withGlobalTauri": false,
"windows": [
{
"title": "RustMark",
"width": 1280,
"height": 800,
"minWidth": 800,
"minHeight": 600
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
步骤 3:配置 Capabilities
json
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "RustMark 默认权限",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-set-title",
"core:event:default"
]
}
企业落地场景
在一个真实的企业知识库管理场景中,RustMark v1.2 被部署为内部技术文档编辑器的桌面客户端:
- 文档管理:Tauri Command 封装了完整的文件系统操作,支持本地目录树浏览、文件搜索、标签管理。
- 实时协作感知:通过 FileWatcher + Event 系统,当外部程序(如 Git pull)修改了当前编辑的文件时,编辑器自动弹出"文件已在外部修改,是否重新加载?"提示。
- 多格式导出:内核的 pulldown-cmark 自定义渲染器支持导出为 PDF(通过 printpdf crate)、DOCX、HTML 等格式,Command 层直接暴露导出功能。
- 权限控制:Capability 配置限制 Command 只能访问用户指定的工作目录,防止前端代码遍历系统文件。
生产避坑经验
-
IPC 调用不是免费的 :每次
invoke()都是一次跨进程通信(尽管 Tauri 2.x 做了大量优化)。不要在每个按键事件中都调用 invoke,务必加防抖。推荐 150-300ms 的防抖间隔。 -
大文档传输避免 JSON :对于超过 50KB 的 Markdown 内容,使用 Raw Request(传递
Uint8Array)替代直接传 String。JSON 序列化大字符串的性能开销在 IPC 场景中不可忽略。 -
Event 不做返回值:Event 是 fire-and-forget 机制,不能携带返回值。需要在 Rust 处理后通知前端的场景,应使用 Command(有返回值)或 Channel(流式数据)。
-
CodeMirror 6 Extension 顺序敏感 :Extension 按数组顺序组成处理管道。例如,
keymapExtension 必须放在basicSetup之后才能正确覆盖默认快捷键。 -
WebView CSP 限制 :Tauri 默认的 CSP(Content Security Policy)可能阻止内联样式和某些脚本。如果语法高亮使用内联 CSS,需要在
tauri.conf.json的app.security.csp中添加style-src 'unsafe-inline'。 -
路径处理跨平台 :Windows 使用 `` 而 macOS/Linux 使用
/。在 Tauri Command 中处理路径时,始终使用std::path::Path/PathBuf而非字符串拼接。前端传递路径时使用正斜杠,Rust 侧Path会自动适配。 -
Channel 生命周期:Channel 在 Command 返回后关闭。如果需要持续的数据流,应使用 Event 系统或在 Command 中保持 await 直到流结束。
全文总结
本文围绕 RustMark v1.2 的 Tauri + TypeScript Shell 架构,深入解析了将 Rust 内核能力暴露为跨平台桌面应用的完整方案。
核心要点:(1)Tauri 2.x 的多进程模型中,Rust Core 与 WebView 通过 IPC 层通信,Capability/Permission/Scope 三层权限体系统一管控安全边界;(2)Command 系统是 IPC 的主通道,#[tauri::command] 宏将任意 Rust 函数导出为前端 API,支持自动参数注入(AppHandle/State/Request)和 Raw Request 零拷贝传输;(3)Event 系统提供双向发布-订阅机制,适合生命周期通知和状态变更推送;(4)Channel API 专为流式数据传输设计,严格有序,适合大文件分块读取和进度上报;(5)CodeMirror 6 的函数式扩展架构(EditorState + EditorView + Extension 管道)与 Tauri invoke 结合,实现了高效的类型安全的编辑器 IPC 集成。
RustMark v1.2 标志着内核能力正式走出终端,拥有了真正的图形用户界面。从命令行到桌面应用的跨越,不仅是交互方式的升级,更是软件架构从"单进程批处理"到"多进程实时交互"的范式转换。Tauri 2.x 为这个转换提供了最小成本的路径------Rust 内核零修改即可融入桌面 Shell,TypeScript 前端获得类型安全的系统调用能力,两者通过高效 IPC 完成无缝协作。
本期专栏更新说明
本文为《Rust 从入门到精通》专栏第一季(RustMark 贯穿案例)持续迭代内容,专栏长期更新所有权系统、Trait 与泛型、并发异步、宏编程、Unsafe Rust、跨平台工程与编译器内核,一次订阅,永久持续更新。第一季完结后将开启第二季,以全新贯穿案例重新从入门螺旋。
参考资料
- Tauri 2.0 Stable Release
- Calling Rust from the Frontend --- Tauri Documentation
- Calling the Frontend from Rust --- Tauri Documentation
- Inter-Process Communication --- Tauri Documentation
- CodeMirror 6 Reference Manual
- CodeMirror 5 to 6 Migration Guide
- Tauri 2.x Configuration Reference
- Tauri Security: Permissions, Scopes, and Capabilities
- pulldown-cmark Documentation
- How to Build a Code Editor with CodeMirror 6 and TypeScript