不调用外部 typst 命令:Draftmark 用 FRB 内嵌排版引擎的实现
项目仓库:https://atomgit.com/nutpi/Draftmark
FRB 项目:https://atomgit.com/oh-flutter/flutter_rust_bridge
Rust 社区:https://xuanwu.openatom.cn/
做一个 Typst 编辑器,最容易想到的办法是调用命令行:把源码写到临时文件,执行 typst compile,再读取生成的 PDF。用来验证排版效果很方便,但做实时预览时,还有几件事要处理:编译器随应用怎么分发,连续输入时怎样管理编译任务,报错后怎样把位置标回编辑器。
Draftmark 是一个面向 HarmonyOS PC 和二合一设备的本地文档编辑器。这次采用的做法是,把 Typst 编译器作为 Rust 库编进应用,Flutter 通过 flutter_rust_bridge(下文简称 FRB)调用它。用户修改源码后,Rust 返回分页图片、诊断信息和耗时,Flutter 据此更新预览。整个排版过程在设备本地完成,运行时不需要另外安装 Typst CLI。
下面从环境准备开始,沿着“输入源码、生成预览、保存文档、导出 PDF”这条流程看实现。
技术选型与复现入口
1. Flutter、Rust、Typst 与 FRB 的职责
| 组成 | 在 Draftmark 中的职责 | 选择它的价值 |
|---|---|---|
| Flutter | 编辑器、文档列表、分页预览、诊断展示和导出操作 | 管理交互与展示,保持输入流畅 |
| Rust | 文档存储、字体与 World、Typst 编译/渲染、诊断和 PDF 导出 | 将排版与文件一致性集中在可测试的原生层 |
| Typst | 提供内嵌排版编译器、分页文档和 PDF 生成能力 | 应用内完成排版,运行时不依赖外部 CLI |
| FRB | 生成 Dart/Rust 绑定,传递源码、分页 PNG、诊断和错误 | 减少手写 FFI 转换,保持接口类型一致 |
2. 工具链检查
在 Draftmark/apps/draftmark 目录复现前,先确认 Flutter-OH、Rust、OHOS target 和 FRB 生成器:
flutter --version
flutter doctor -v
rustc --version
cargo --version
rustup target list --installed
flutter pub get
flutter_rust_bridge_codegen generate
生成绑定后,再按仓库说明执行 Rust 测试、Flutter 测试和 HarmonyOS 构建。若问题能在最小 FRB 工程中复现,提交 FRB 仓库;若涉及 Typst、字体、文档保存或预览状态,则在 Draftmark 仓库反馈。
3. 旋武社区与 FRB
开放原子旋武开源社区提供 Rust 学习与开源协作入口;flutter_rust_bridge负责生成 Dart/Rust 跨语言绑定。Draftmark 的排版和存储问题进入 Draftmark 仓库,通用绑定或类型映射问题再进入 FRB 仓库。
一、认识旋武社区与 FRB
1. 旋武社区:Rust 学习与开源协作入口
旋武社区是开放原子开源基金会旗下的 Rust 中国社区,提供 Rust 发行版、学习资料、在线编码体验,也孵化和展示 Rust 开源项目。准备在实际应用中使用 Rust,可以从这里查找入门资料、社区项目和技术活动。
社区地址:https://xuanwu.openatom.cn/
2. FRB:让 Flutter 调用 Rust 的排版接口
flutter_rust_bridge 是连接 Flutter/Dart 与 Rust 的跨语言绑定工具。开发者定义 Rust 接口后,由代码生成器生成两端的绑定代码,Dart 就能调用这些接口,并接收转换后的数据。项目地址:https://atomgit.com/oh-flutter/flutter_rust_bridge。鸿蒙相关的接入说明、示例和问题反馈,可以从这个仓库查起。
在 Draftmark 中,FRB 连接的是 Flutter 工作区和 draftmark-bridge 原生库。Flutter 管理输入框、文档列表和预览状态;Rust 管理文档文件,调用 Typst 编译、渲染和导出。Typst 的 World、字体集合、分页文档等内部对象留在 Rust 侧,Flutter 只接收界面需要的数据。
二、为什么 Draftmark 选择 FRB
Draftmark 需要在同一个工作区里完成源码编辑、排版、诊断和导出。Typst 已经提供了 Rust 排版能力,接下来要解决的是怎样把这些能力接到 Flutter 界面上。
| 需求 | 项目中的做法 | FRB 带来的价值 |
|---|---|---|
| 应用安装后就能本地排版 | 将 Typst 作为 Rust 依赖编入 draftmark-bridge | Flutter 通过生成接口调用内嵌引擎,运行时无需另装 Typst CLI |
| 预览要返回多页图片和尺寸 | Rust 返回 Vec<RenderedPage>,每页包含 PNG 字节与宽高 | 减少列表、结构体和字节数据的手写跨语言转换 |
| 报错后要定位到源码 | Rust 将 Span 转换为诊断说明、提示和行列号 | Dart 接收结构化诊断,界面不必解析 stderr |
| 编辑、保存和编译各自更新状态 | 控制器等待异步结果,用 revision 判断是否过时 | 生成的调用接口便于接入 Dart 的异步流程,结果有效性由应用管理 |
| 排版与存储需要独立测试 | Rust 测试编译、渲染和持久化,Flutter 测试界面状态 | 核心逻辑留在 Rust,跨语言接口集中在桥接层 |
这套设计的好处在修改接口时比较直观。例如预览需要新增一个字段,可以先改 Rust 返回类型,再重新生成绑定,Flutter 就能通过对应字段读取,不必同时维护一套手写的数据转换代码。
内嵌也有成本:Rust 库和字体会增加包体,原生构建环境需要配齐,FRB 两端依赖与代码生成器需要匹配。Draftmark 的主要能力都在 Rust 排版引擎里,因此接受这部分成本。预览是否流畅,还要继续看防抖、渲染分辨率和文档复杂度。
三、环境搭建:引用已有教程,再补齐项目依赖
1. 先完成 Flutter-OH 与 Rust 基础环境
基础安装直接参考下面两篇文章:
- Flutter-OH 环境:《2026 年如何上车 Flutter-OH:环境搭建与上手流程》,作者:程序媛夏天。重点看 Flutter-OH SDK、DevEco Studio、SDK 路径配置和设备连接。
- Rust 开发环境:《Rust | VS Code搭建Rust开发环境的超详细图文教程总结(含Rust开发常用插件)》,作者:CHENG-JustDoIt。可参考 Rust 工具链安装、Cargo 命令及 VS Code 配置。该文以 Windows 为例,macOS 开发者需要使用对应平台的工具链;鸿蒙目标和链接环境仍需按本项目配置。
教程用于理解安装流程,复现 Draftmark 时,还要核对仓库要求的工具版本、鸿蒙目标和签名配置。
2. 对齐当前仓库的版本与运行条件
Draftmark 仓库 README 记录的开发环境如下:
| 工具 | 仓库记录的版本或要求 |
|---|---|
| Flutter-OH | 3.35.8-ohos-0.0.3 |
| Dart | 3.9.2 |
| FRB | 2.13.0-beta.6 |
| Rust | 1.92 或更高 |
| DevEco Studio / SDK | DevEco Studio 6.0、OpenHarmony API 22 |
| Rust 目标 | aarch64-unknown-linux-ohos |
| 真机签名 | 匹配应用包名,并包含目标设备 UDID |
安装完成后,先确认当前终端用的是 Flutter-OH,并且能找到设备:
flutter --version
flutter doctor -v
flutter devices
rustc --version
cargo --version
rustup target add aarch64-unknown-linux-ohos
rustup target add 安装的是目标平台的 Rust 标准库,OHOS 链接器和 SDK 仍由鸿蒙开发环境提供。项目的 rust_builder/ohos 已接入 Cargokit,后续随 Flutter-OH 工程一起构建原生库。
3. 获取项目并生成桥接代码
下面命令以 macOS 的终端为例。克隆后的目录名统一使用 Draftmark:
git clone https://atomgit.com/nutpi/Draftmark.git
cd Draftmark/apps/draftmark
flutter pub get
当前仓库中,Dart 和 Rust 两侧的 FRB 依赖都固定为 2.13.0-beta.6。需要重新生成绑定时,先准备相同版本的生成器:
cargo install flutter_rust_bridge_codegen --version 2.13.0-beta.6 --locked
flutter_rust_bridge_codegen --version
flutter_rust_bridge_codegen generate
以上生成命令在 Draftmark/apps/draftmark 下执行,对应配置是:
rust_input: crate::api
rust_root: ../../crates/draftmark-bridge/
dart_output: lib/src/rust
修改 crates/draftmark-bridge/src/api 下的接口后,需要重新生成绑定。只改 Flutter 布局时,可以跳过这一步。生成器版本也应跟随项目依赖调整,避免一端更新、另一端仍使用旧生成物。
四、功能闭环与核心代码分析
1. 先看编辑、预览、保存和导出的调用关系
Flutter 工作区通过控制器调用 FRB,Rust 桥接层负责组织排版与存储。各层处理的内容如下:
| 层 | 负责的工作 | 交给下一层的内容 |
|---|---|---|
| Flutter 工作区 | 源码输入、文档切换、分页展示和状态提示 | 当前源码、标题及操作请求 |
StudioController | 防抖、异步状态、保存与编译版本判断 | FRB 调用参数,或可展示的最新结果 |
| Rust Bridge API | 提供编译、导出和文档操作入口 | 交给编译器与文档存储的参数 |
| Typst 与渲染组件 | 编译源码、转换诊断、生成 PNG 和 PDF | 页面字节、诊断及 PDF 数据 |
| 文档存储 | 保存源码、元数据和导出文件 | 文档列表、文档内容及文件路径 |
一次普通编辑会触发两项工作:停止输入约 420 ms 后请求编译,停止输入约 700 ms 后请求保存。这两个计时器分别重置,因此连续输入时,不会每按一次键就执行一轮完整排版。
读代码时,可以先看这几个位置:
Draftmark/
├── apps/draftmark/
│ ├── lib/main.dart
│ ├── lib/src/app/studio_controller.dart 编辑、保存与编译状态
│ ├── lib/src/rust/ FRB 生成的 Dart 绑定
│ ├── rust_builder/ohos/ 鸿蒙原生构建接入
│ └── flutter_rust_bridge.yaml
└── crates/draftmark-bridge/src/
├── api/mod.rs 对外接口与数据结构
├── api/typeset.rs 编译与导出入口
├── api/documents.rs 文档操作入口
├── compiler.rs World、字体、PNG 与 PDF
└── store.rs 文档源码与元数据存储
2. 数据结构:一次编译返回哪些内容
api/mod.rs 定义了一轮编译返回的内容:
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileDiagnostic {
pub severity: String,
pub message: String,
pub hints: Vec<String>,
pub line: Option<u32>,
pub column: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedPage {
pub number: u32,
pub width: u32,
pub height: u32,
pub png_data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileOutput {
pub pages: Vec<RenderedPage>,
pub diagnostics: Vec<CompileDiagnostic>,
pub elapsed_ms: u64,
}
RenderedPage 带有页码、像素宽高和 PNG 字节。Flutter 可以用 Image.memory(page.pngData) 显示页面,不必等一张临时图片写完再去读。诊断单独放进列表,成功时的 warning 和语法错误都能送到界面上。
这里要区分两类失败:Typst 源码有语法错误时,编译函数仍返回 Ok(CompileOutput),其中页面为空、诊断包含错误;如果 PNG 编码或存储初始化等环节失败,则通过 Result 的错误返回,由 Dart 侧捕获异常。前者需要用户改文档,后者需要检查运行环境或程序实现。
3. 字体初始化:在第一次编译前传入 Rust
Flutter 能显示中文,不代表 Rust 里的 Typst 也能找到同一套字体。项目启动时会从 assets 读取 HarmonyOS Sans,再把字体字节交给编译器。下面节选初始化顺序,省略了界面状态和异常处理:
await RustLib.init();
final font = await rootBundle.load('assets/fonts/HarmonyOS_Sans_SC.ttf');
final bytes = Uint8List.sublistView(font);
await bridge.configureCompiler(fontData: [bytes]);
final support = await getApplicationSupportDirectory();
final snapshot = await bridge.initialize(
dataDir: '${support.path}/draftmark',
);
// 随后恢复文档列表,加载文档并进行首次编译。
Rust 将传入字体与 Typst 内置字体合并到 FontStore,并用 OnceLock 保存资源。这样每次输入后不用重新解析字体文件,但也意味着初始化顺序不能颠倒:如果先编译,资源会按默认字体初始化,之后再调用 configureCompiler 不会替换已经建立的集合。
4. 内嵌引擎:调用 Typst 编译并逐页渲染
AppWorld 实现 Typst 的 World trait,提供当前源码、项目文件、字体和标准库。应用 API 把文档目录传给它,供相对路径文件读取使用。当前实现没有接入 Typst 包下载,带有外部包依赖的模板不能直接按在线环境的行为使用。
compiler.rs 中的编译函数如下:
pub(crate) fn compile(
source: &str,
project_root: &Path,
pixels_per_point: f64,
) -> Result<CompileOutput, String> {
let started = Instant::now();
let world = AppWorld::new(source, project_root);
let Warned { output, warnings } = typst::compile::<PagedDocument>(&world);
let mut diagnostics = warnings
.iter()
.map(|diagnostic| convert_diagnostic(&world, diagnostic))
.collect::<Vec<_>>();
let document = match output {
Ok(document) => document,
Err(errors) => {
diagnostics.extend(
errors.iter().map(|diagnostic| convert_diagnostic(&world, diagnostic)),
);
return Ok(CompileOutput {
pages: vec![],
diagnostics,
elapsed_ms: elapsed_ms(started),
});
}
};
let scale = pixels_per_point.clamp(0.6, 3.0);
let options = RenderOptions {
render_bleed: false,
format: PngFormatOptions { pixel_per_pt: Some(Scalar::new(scale)) },
}
.resolve(document.options().get::<PngFormat>());
let mut pages = Vec::with_capacity(document.pages().len());
for (index, page) in document.pages().iter().enumerate() {
let pixmap = typst_render::render(page, &options);
let png_data = pixmap
.encode_png()
.map_err(|error| format!("PNG 编码失败:{error}"))?;
pages.push(RenderedPage {
number: index as u32 + 1,
width: pixmap.width(),
height: pixmap.height(),
png_data,
});
}
Ok(CompileOutput {
pages,
diagnostics,
elapsed_ms: elapsed_ms(started),
})
}
typst::compile::<PagedDocument>(&world) 得到分页文档,typst_render::render 把每一页栅格化,最后编码为 PNG。整个函数直接调用 Rust 库,没有创建 typst 子进程。
缩放比例在 Rust 侧限制为 0.6~3.0,用来约束正常数值输入下的渲染分辨率。当前实现每轮都会渲染全部页面,适合先把编辑到预览的流程跑通。页数增加后,可以测量编译、PNG 编码和 Flutter 解码各自的耗时,再决定是否增加按可见页渲染等优化。
elapsed_ms 从函数开始计时,到页面编码结束为止,包含这段 Rust 编译和渲染过程,不包含完整的跨语言传输、Flutter 图片解码和屏幕绘制时间。
5. 诊断定位:把 Span 转成行列号
Typst 诊断中的位置是源码 Span。转换时,先找到对应源码和字节范围,再映射为行列号:
let (line, column) = diagnostic
.span
.id()
.and_then(|id| world.source(id).ok())
.and_then(|source| {
let range = world.range(diagnostic.span)?;
source.lines().byte_to_line_column(range.start)
})
.map(|(line, column)| {
(Some(line as u32 + 1), Some(column as u32 + 1))
})
.unwrap_or((None, None));
底层索引从 0 开始,界面展示从 1 开始。如果诊断没有可映射的位置,就保留 None,只显示说明和提示。这样不会凭空出现“第 0 行第 0 列”。
6. 异步状态:分别管理保存和编译结果
标题变化只需要保存,源码变化才需要同时保存和编译。控制器通过一个参数区分这两种情况:
void _markEdited({required bool compile}) {
if (_current == null) return;
_dirty = true;
_editRevision++;
_runtimeMessage = null;
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 700), () {
unawaited(save());
});
if (compile) {
_compileTimer?.cancel();
_compileTimer = Timer(const Duration(milliseconds: 420), () {
unawaited(compileNow());
});
}
notifyListeners();
}
保存时会记录 _editRevision。如果保存期间用户又输入了内容,旧任务完成后不能把最新草稿标成“已保存”,控制器会继续安排保存。
编译则用 _compileRevision 判断结果是否已经过时:
Future<void> compileNow() async {
if (_current == null) return;
_compileTimer?.cancel();
final revision = ++_compileRevision;
final source = sourceController.text;
_compiling = true;
notifyListeners();
try {
final output = await bridge.compileSource(
source: source,
pixelsPerPoint: _pixelsPerPoint,
);
if (revision != _compileRevision) return;
_pages = output.pages;
_diagnostics = output.diagnostics;
_lastCompileMs = output.elapsedMs.toInt();
_runtimeMessage = null;
} catch (error) {
if (revision == _compileRevision) {
_runtimeMessage = _message(error);
}
} finally {
if (revision == _compileRevision) {
_compiling = false;
notifyListeners();
}
}
}
每次发起编译时递增版本号,返回时与当前版本比较。后续编译已经发出后,前一次结果就不能再覆盖预览。这个判断只负责丢弃结果,不会取消已经进入 Rust 的任务,也不等于每次按键都立即让在途结果失效。
当前控制器直接用 output.pages 替换预览,因此语法编译失败时,预览会变为空页,同时显示诊断。如果以后改成保留上次成功页面,还需要明确标识旧预览,避免用户把它当成当前源码的排版结果。
两套计时器表示应用状态分别管理,也不代表底层操作一定并行。当前 Rust API 经由 with_store 持有存储写锁,编译、保存等操作可能互相等待。大文档下若保存延迟明显,这也是需要检查的位置。
7. 文档闭环:保存、重新打开与 PDF 导出
文档存储由 store.rs 处理,目录位于系统提供的应用支持目录下:
draftmark/
├── metadata.json
├── documents/
│ └── <文档 ID>.draft
└── exports/
└── <文档标题>.pdf
源码和元数据分别通过“写临时文件,再重命名”的方式更新。文档列表从元数据恢复,打开文档时再读取对应源码。两个文件各自替换,不应把它理解为一次覆盖两者的事务。
导出时,Flutter 先等待待保存任务,再把编辑器当前源码和标题传给 exportPdf。Rust 重新编译得到 PagedDocument,调用 typst_pdf::pdf 生成 PDF 字节,清理文件名后写入 exports 目录,并返回路径、页数和诊断。PDF 来自分页文档,预览 PNG 不参与 PDF 生成。
当前版本默认写入应用支持目录,没有在这条流程中让用户任选系统目录。导出完成后,应根据返回路径查找文件;保存、预览和 PDF 写入各自都有失败可能,需要分别看结果。
五、运行方式、效果图与验证
1. 先运行主机检查
在仓库根目录执行 Rust 测试,然后进入应用目录检查 Dart 代码:
# 当前目录:Draftmark
cargo test --package draftmark-bridge
cd apps/draftmark
dart analyze lib test integration_test
flutter test test
仓库中有文档持久化、编译渲染、源码诊断和 PDF 元数据等测试。命令是否通过,以当前检出版本和本机执行结果为准。
2. 构建并安装 HarmonyOS PC 应用
首次配置签名时,在 Draftmark/apps/draftmark 下复制模板;如果已经配置过,直接使用已有本地文件:
cp ohos/build-profile.example.json5 ohos/build-profile.json5
用 DevEco Studio 打开 ohos 目录,配置 default 签名项,并让 default 产品选择它。应用包名为 com.draftmark.editor,Profile 要匹配这个包名并包含目标设备。证书、密码和本机签名路径留在本地。
连接设备后,在应用目录运行,<device-id> 替换为 flutter devices 显示的设备 ID:
flutter run -d <device-id> --release
也可以单独构建 HAP,再安装和启动:
flutter build hap --release
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.draftmark.editor
以上假定 hdc 已加入 PATH,且只连接了一台设备。多设备环境需要用 hdc -t <设备标识> 指定目标。
3. 输入源码并完成一次编辑与导出
打开应用后,新建文档,输入下面的内容:
#set page(paper: "a4")
#set text(font: "HarmonyOS Sans SC", size: 11pt)
= FRB 工程记录
Rust 负责排版,Flutter 负责编辑与预览。
$ 1 + 1 = 2 $
#table(
columns: 3,
[模块], [职责], [输出],
[Flutter], [编辑与展示], [工作区],
[Rust], [编译与渲染], [页面与诊断],
)
停止输入后切换到预览,检查标题、公式和表格。再把标题改成一段容易辨认的文字,确认页面跟着变化。等待自动保存后切换文档再返回,检查修改是否还在。
接着删掉 #table(...) 最后的右括号,查看是否出现带位置的错误;补回括号,确认预览恢复。这一来一回可以检查源码、编译器和预览之间的对应关系。
最后点击导出,检查返回路径下的 PDF,用阅读器核对内容和页数。应用支持目录可能位于沙箱内,真机取出文件需要结合调试工具或后续提供的分享能力处理。
4. HarmonyOS PC 真机效果

这是项目联调时记录的 HarmonyOS PC 真机画面:应用显示“1 页”,预览中包含中文标题、公式和表格,底部状态为“就绪 / 47 ms”。这里的 47 ms 是该次文档的 Rust 编译与渲染耗时,换文档、字体或设备后会变化。
这张图对应临时签名的功能联调包。正式包名 com.draftmark.editor 对应的签名 Profile 仍需补齐,PDF 真机导出也尚需独立验证;现有截图记录的是内嵌引擎生成分页预览的效果。
5. 各类验证分别检查什么
仓库还提供了设备集成测试,可以单独验证原生库初始化、FRB 编译调用和 PNG 返回数据:
# 当前目录:Draftmark/apps/draftmark
flutter test integration_test/simple_test.dart -d <device-id>
该用例检查一页预览、PNG 签名字节和无 error 诊断,使用的是内置字体下的英文与公式样例,中文字体和 PDF 导出需要另外验证。
| 验证层级 | 可以检查的内容 | 需要另外验证的内容 |
|---|---|---|
| Rust 单元测试 | 文档持久化、编译渲染、诊断位置和 PDF 元数据 | 鸿蒙原生库加载与设备文件访问 |
| Dart 静态分析与 Flutter 测试 | 类型使用、控制器状态和界面交互 | 目标设备上的实际排版与字体 |
| 设备集成测试 | FRB 初始化、编译调用、一页 PNG 返回和无 error 诊断 | 中文字体、复杂文档与 PDF 真机导出 |
| 当前真机截图 | 该次联调包中的中文、公式、表格与一页预览 | 正式包签名、导出文件和长文档表现 |
| PDF 导出检查 | 返回路径、文件是否可读、内容和页数 | 应结合目标设备单独记录结果 |
这些检查对应不同环节。主机测试通过后,还要在目标设备核对字体和原生库;预览出现后,再核对保存与导出。当前真机资料的范围见上面的图片说明。
六、FAQ:如何反馈问题并贡献修复
1. 遇到框架问题,如何提 Issue?
先判断问题发生在哪一层。如果不经过 Flutter,Rust 单测就能复现,优先检查排版或存储实现;如果 Rust 调用正常,而经过 FRB 后才出现类型转换、绑定生成或动态库加载问题,再进一步缩小到桥接层。
确认与框架有关后,打开 FRB AtomGit 仓库,进入 Issues,先搜索相同报错和版本,再按仓库模板新建问题。没有模板时,可以按下面的格式整理:
标题:[OHOS][版本] 触发操作 + 实际错误
环境:
- 主机系统与 CPU 架构:
- Flutter-OH / Dart / Rust / Cargo 版本:
- FRB Dart、Rust 依赖与 codegen 版本:
- DevEco / SDK API / 目标设备系统版本:
- Draftmark commit:
复现:
1. 获取哪个版本或最小示例
2. 执行哪些命令,修改哪些配置或 API
3. 输入哪段源码,执行什么操作后出现问题
预期结果:
实际结果:
完整日志与堆栈:
最小复现仓库或补丁:
已排查步骤及结果:
FRB 问题尽量缩减为一个 Rust 函数、一个数据结构和一次 Dart 调用。如果涉及排版,附上最小 Typst 源码;如果涉及中文字体,写清字体名称、加载方式以及首次编译前是否完成配置。日志保留首个错误及上下文,提交前去掉签名密码、令牌等与复现无关的信息。
如果问题只涉及 Draftmark 的文档保存、预览状态或界面,直接到 Draftmark 仓库 的 Issues 反馈,附上最小文档和操作步骤。
2. 问题修复后,如何提 PR?
如果修复在自己手中,可以先在 Issue 中说明原因和修改方向,再把代码提交为 Pull Request。通常先提交修复 PR、完成评审合并,再关闭 Issue,不必等 Issue 关闭后才开始贡献。若维护者已经合入修复,验证结果并补充必要反馈即可,避免重复提交。
一般流程如下,具体以目标仓库的贡献说明为准:
- Fork 对应仓库,从维护者要求的目标分支创建修复分支。
- 提交范围明确的修复,并增加能复现原问题的回归用例。若修改公开 API,同时按仓库要求更新绑定或文档。
- 运行仓库规定的格式、静态检查和测试。涉及 OHOS 原生构建或动态库加载时,补充设备型号、系统版本及真机结果。
- 推送到个人 Fork,在 AtomGit 的 Pull Requests 中选择自己的修复分支和上游目标分支。
- 在 PR 描述中关联 Issue,说明触发条件、修复前后的行为和验证结果;根据评审意见继续更新。
- 合并后记录修复提交或版本。如果修改了 FRB,在 Draftmark 中更新对应依赖、重新生成绑定并构建,再复测原场景。
提交前检查差异,去掉本机签名配置、构建产物和无关格式化。若修的是 Draftmark,就向 Draftmark 提交;若修的是 FRB 通用能力,就向 FRB 对应仓库提交。目标分支不确定时,在原 Issue 中与维护者对齐即可。
PR 描述可以按下面几项填写:
问题:哪段源码、哪个操作或调用顺序会触发错误。
原因:问题位于接口转换、字体初始化、编译、状态管理还是文件操作。
修改:修复后页面、诊断、保存或导出行为有什么变化。
验证:回归用例、检查命令、设备与实际结果。
关联:Issue #<实际编号或链接>。
如果只是 SDK 或签名配置错误,修好环境后反馈排查结果即可;维护者已经合入的修复,更新版本后复测。新增代码、测试或文档改动时,再提交对应 PR。
3. 其他常见问题及解决方案
| 现象 | 优先检查 | 解决方向 |
|---|---|---|
找不到 flutter build hap,或没有 HarmonyOS 工具链 | PATH 和 Flutter SDK 来源 | 使用 Flutter-OH,按环境教程核对 SDK 配置,再运行 flutter doctor -v |
| Rust 找不到 OHOS 目标,或链接失败 | target、DevEco SDK、架构和链接器 | 安装 aarch64-unknown-linux-ohos,确认 SDK 完整并使用匹配的原生构建配置 |
| 修改 Rust API 后,Dart 方法或参数对不上 | FRB 两端依赖、codegen 版本及生成目录 | 在应用目录重新生成绑定,再重建原生库,避免单独修改生成文件 |
| Flutter 中文正常,排版页却缺字 | 字体 asset、传入字节和初始化顺序 | 在首次编译前完成 configureCompiler,资源已初始化时需重启后按正确顺序加载 |
| 复制的 Typst 模板提示找不到包 | 模板中的外部包依赖 | 当前 AppWorld 未实现包解析与下载,先使用无外部包依赖的样例验证 |
| 连续输入时预览短暂落后 | 防抖等待、编译耗时和 revision | 确认旧请求结果被丢弃;当前实现不会取消已进入 Rust 的任务 |
报 bundleName ... does not match ... SigningConfigs | 包名、Profile、设备和产品签名项 | 使用匹配 com.draftmark.editor 且包含目标设备的签名配置 |
arm64 构建仍提示缺少 flutter_native_x86_64 | 架构切换后残留的 oh_modules 生成链接 | 按仓库 README 核对架构,清理相关旧生成链接后重建 |
| 无法连接 Dart VM Service | 调试连接及 module.json5 权限声明 | 按仓库说明检查 ohos.permission.INTERNET 并确认设备连接正常 |
| 预览正常,却找不到导出的 PDF | 导出返回值、错误信息和实际路径 | 检查应用支持目录中的 draftmark/exports,按返回路径核对文件 |
本项目使用的 Rust 特性
Draftmark 的 Rust 代码围绕“内嵌 Typst 编译、分页渲染、诊断定位和可靠存储”组织,使用的语言特性都能在桥接层与编译器代码中找到对应场景:
| Rust 特性 | 在 Draftmark 中的实际用法 |
|---|---|
struct 与 enum | RenderedPage、Diagnostic、文档模型等结构体承载跨语言数据;编译结果和诊断级别使用枚举表达有限状态。 |
derive 派生 | 为跨 FRB 类型派生 Clone、Debug、PartialEq 等 trait,便于绑定转换、日志和测试比较。 |
Option<T> | 字体、父目录、可选导出路径和诊断字段允许缺省,代码用 if let 或 unwrap_or 明确处理缺失情况。 |
Result<T, E> 与 ? | Typst 编译、字体加载、文件写入、重命名和 PDF 导出错误沿调用链传播,在 API 边界转换为 FRB 可返回的错误。 |
trait 与 World 实现 | AppWorld 为 Typst 提供源码、字体、文件和时间等能力,通过 trait 接口把引擎依赖与应用存储隔离。 |
| 所有权与借用 | 编译期间以 &str、&Path 和借用的源码访问输入,返回的 PNG/PDF 字节由结果结构体拥有,避免悬垂引用。 |
| 泛型与集合 | Vec<RenderedPage>、Vec<u8> 保存分页结果和二进制数据;泛型辅助函数复用 JSON/文件写入逻辑。 |
| 闭包与迭代器 | 遍历字体、页面和诊断时使用迭代器与闭包,集中完成映射、过滤和错误转换。 |
| 异步任务与状态隔离 | FRB API 以异步函数执行编译、保存和导出;Flutter 侧用 revision 丢弃过时结果,Rust 侧不阻塞 UI 线程。 |
| RAII 与文件 API | File 离开作用域自动释放,临时文件写入、sync_all 后再 rename,保证元数据替换过程可恢复。 |
| FRB 导出标记 | #[flutter_rust_bridge::frb] 和可转换的数据类型生成 Dart 绑定,Flutter 不直接接触 Typst 内部对象。 |
七、当前范围与项目入口
Draftmark 已将文档存储、Typst 内嵌编译、分页 PNG、结构化诊断和 PDF 导出接入 Flutter 工作区。当前预览会逐页渲染整份文档,外部 Typst 包下载尚未接入;后续遇到长文档或复杂模板时,需要分别检查渲染开销和资源依赖。
现有 HarmonyOS PC 资料记录了临时签名联调包中的一页实际预览。正式 com.draftmark.editor 包名的签名和 PDF 真机导出仍需补充验证,完成后再更新对应结果。
Draftmark 的这部分代码集中在 StudioController、api/mod.rs 和 compiler.rs。读完后可以从修改示例文档开始:让一个新的标题出现在预览中,再制造并修复一次语法错误,就能顺着调用关系把 FRB 两端的代码串起来。
源码、环境资料和问题反馈可以从以下入口查阅:
更多推荐
所有评论(0)