项目仓库: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-bridgeFlutter 通过生成接口调用内嵌引擎,运行时无需另装 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 基础环境

基础安装直接参考下面两篇文章:

  1. Flutter-OH 环境《2026 年如何上车 Flutter-OH:环境搭建与上手流程》,作者:程序媛夏天。重点看 Flutter-OH SDK、DevEco Studio、SDK 路径配置和设备连接。
  2. Rust 开发环境《Rust | VS Code搭建Rust开发环境的超详细图文教程总结(含Rust开发常用插件)》,作者:CHENG-JustDoIt。可参考 Rust 工具链安装、Cargo 命令及 VS Code 配置。该文以 Windows 为例,macOS 开发者需要使用对应平台的工具链;鸿蒙目标和链接环境仍需按本项目配置。

教程用于理解安装流程,复现 Draftmark 时,还要核对仓库要求的工具版本、鸿蒙目标和签名配置。

2. 对齐当前仓库的版本与运行条件

Draftmark 仓库 README 记录的开发环境如下:

工具仓库记录的版本或要求
Flutter-OH3.35.8-ohos-0.0.3
Dart3.9.2
FRB2.13.0-beta.6
Rust1.92 或更高
DevEco Studio / SDKDevEco 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 真机效果

Draftmark 在 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 关闭后才开始贡献。若维护者已经合入修复,验证结果并补充必要反馈即可,避免重复提交。

一般流程如下,具体以目标仓库的贡献说明为准:

  1. Fork 对应仓库,从维护者要求的目标分支创建修复分支。
  2. 提交范围明确的修复,并增加能复现原问题的回归用例。若修改公开 API,同时按仓库要求更新绑定或文档。
  3. 运行仓库规定的格式、静态检查和测试。涉及 OHOS 原生构建或动态库加载时,补充设备型号、系统版本及真机结果。
  4. 推送到个人 Fork,在 AtomGit 的 Pull Requests 中选择自己的修复分支和上游目标分支。
  5. 在 PR 描述中关联 Issue,说明触发条件、修复前后的行为和验证结果;根据评审意见继续更新。
  6. 合并后记录修复提交或版本。如果修改了 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 中的实际用法
structenumRenderedPageDiagnostic、文档模型等结构体承载跨语言数据;编译结果和诊断级别使用枚举表达有限状态。
derive 派生为跨 FRB 类型派生 CloneDebugPartialEq 等 trait,便于绑定转换、日志和测试比较。
Option<T>字体、父目录、可选导出路径和诊断字段允许缺省,代码用 if letunwrap_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 与文件 APIFile 离开作用域自动释放,临时文件写入、sync_all 后再 rename,保证元数据替换过程可恢复。
FRB 导出标记#[flutter_rust_bridge::frb] 和可转换的数据类型生成 Dart 绑定,Flutter 不直接接触 Typst 内部对象。
七、当前范围与项目入口

Draftmark 已将文档存储、Typst 内嵌编译、分页 PNG、结构化诊断和 PDF 导出接入 Flutter 工作区。当前预览会逐页渲染整份文档,外部 Typst 包下载尚未接入;后续遇到长文档或复杂模板时,需要分别检查渲染开销和资源依赖。

现有 HarmonyOS PC 资料记录了临时签名联调包中的一页实际预览。正式 com.draftmark.editor 包名的签名和 PDF 真机导出仍需补充验证,完成后再更新对应结果。

Draftmark 的这部分代码集中在 StudioControllerapi/mod.rscompiler.rs。读完后可以从修改示例文档开始:让一个新的标题出现在预览中,再制造并修复一次语法错误,就能顺着调用关系把 FRB 两端的代码串起来。

源码、环境资料和问题反馈可以从以下入口查阅:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐