Rust & WebAssembly 实践:构建一个简单实时的 Markdown 编辑器

Rust & WebAssembly 实践:构建一个简单实时的 Markdown 编辑器

一、引言:从零打造一个 WebAssembly 应用------实时 Markdown 编辑器

欢迎回到【Rust & WebAssembly】系列!

在前面的八篇文章里,我们已经完成了 Rust 与 WebAssembly 的基础知识和技术、工具的介绍。理解了 Rust 与 WebAssembly 的核心概念,还熟练掌握了编写 WebAssembly 应用的关键工具:用于工程打包的 wasm-pack、用于接口绑定的 wasm-bindgen、用于 DOM 交互的 web-sysjs-sys。同时介绍了如何进行 性能优化,确保我们的应用产物高效而强大。

那么接下来,我们将通过运用前面所介绍的知识和技术,编写一个可运行的 Rust Wasm 应用 ------ 轻量化 Markdown 编辑器。

二、项目初始化:从 0 到 1 搭建 Rust Wasm 工程

(一)环境准备与项目创建

在开启项目之前,先确保已安装好 Rust 工具链。若尚未安装,可前往Rust官方网站,按照指引完成安装,同时别忘了安装wasm-pack,它是我们将 Rust 代码打包成 Wasm 模块的得力助手,使用如下命令即可完成安装:

bash 复制代码
cargo install wasm-pack

安装成功后,使用wasm-pack创建新项目。wasm-pack提供了便捷的模板,让我们能快速搭建起标准的 Rust Wasm 项目框架。运行以下命令:

bash 复制代码
wasm-pack new markdown-editor
cd markdown-editor

(二)项目结构解析

进入markdown-editor项目目录,来看看这个 "框架" 里都有什么。

css 复制代码
markdown-editor/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   └── utils.rs
├── pkg/
└── tests/
  • Cargo.toml文件,负责管理项目的依赖、版本等重要信息。默认配置了wasm-bindgen依赖 ,这是 Rust 与 JavaScript 交互的关键桥梁,后续我们还将在此基础上添加 Markdown 解析库。

  • src目录是存放项目源代码的地方,其中lib.rs是核心文件,Rust 逻辑代码大部分都将放在这里。

  • tests目录用于存放测试代码.

  • pkg目录用于存放打包后的 Wasm 模块和相关文件,包括.wasm.js等文件。

三、引入依赖:配置 Rust 生态工具链

(一)添加核心依赖

编辑 Cargo.toml 文件:

toml 复制代码
[package]
name = "markdown-editor"
version = "0.1.0"
authors = ["xxx <xxx@xxx.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.84"
pulldown-cmark = { version = "0.9", default-features = false }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }

[dependencies.js-sys]
version = "0.3"

[dependencies.web-sys]
version = "0.3"
features = ["Document", "Element", "HtmlElement", "Window", "console"]
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

依赖如下:

  • wasm-bindgen: 用于 Rust 和 JavaScript 的交互
  • pulldown-cmark: 纯 Rust 实现的 Markdown 解析器
  • js-sysweb-sys: 提供 JavaScript 和 Web API 的绑定

(二)配置编译目标

为确保项目能正确编译为 Wasm 模块,需要在Cargo.toml中指定输出类型。在文件中添加如下配置:

toml 复制代码
[lib]
crate-type = ["cdylib"]

这行配置告诉 Rust 编译器,我们要生成一个动态链接库(cdylib),这是 Wasm 模块的常见输出类型。有了这个配置,编译器在编译时就会按照 Wasm 的要求生成相应的文件结构和代码,为后续在前端页面中使用 Wasm 模块做好准备 。

四、编写核心 Rust 逻辑:打造 Markdown 解析引擎

src/lib.rs文件中,编写核心的 Markdown 解析函数。首先,引入必要的库和模块, 然后再编写 render_markdown 函数来实现 Markdown 解析:

rust 复制代码
use js_sys::{Error, JsString};
use pulldown_cmark::{html, Options, Parser};
use wasm_bindgen::prelude::*;
use web_sys::console;

// 当编译 Wasm 时启用 console_error_panic_hook
#[cfg(feature = "console_error_panic_hook")]
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn error(s: &str);
}

/// 将 Markdown 文本转换为 HTML
#[wasm_bindgen]
pub fn render_markdown(input: &str) -> Result<JsString, JsValue> {
    // 设置 panic hook 以便更好的错误提示
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();

    console::log_1(&"开始渲染 Markdown...".into());

    // 配置 Markdown 解析选项
    let mut options = Options::empty();
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TABLES);
    options.insert(Options::ENABLE_FOOTNOTES);
    options.insert(Options::ENABLE_TASKLISTS);

    // 创建解析器
    let parser = Parser::new_ext(input, options);

    // 渲染为 HTML
    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    console::log_1(&"Markdown 渲染完成".into());

    Ok(JsString::from(html_output))
}

/// 初始化 Wasm 模块
#[wasm_bindgen(start)]
pub fn init() -> Result<(), JsValue> {
    // 设置 panic hook
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();

    console::log_1(&"Markdown 编辑器 Wasm 模块已初始化".into());

    Ok(())
}
  • Parser::new_ext(input, options) 创建了一个 Parser 实例,它就像是一个 "文本处理器",能够对输入的 Markdown 文本进行流式处理,将其转换为一个可迭代的事件流。
  • html::push_html(&mut html_output, parser) 则将解析后的 Markdown 内容以 HTML 格式推送到html_output字符串中,最终返回生成的 HTML 字符串,完成 Markdown 到 HTML 的华丽转变 。

五、构建 Wasm 模块:从 Rust 代码到二进制资产

(一)执行构建命令

完成 Rust 核心逻辑的编写后,接下来就需要使用 wasm-pack 将 Rust 代码编译成浏览器能够识别和运行的 WebAssembly 模块。进入项目根目录,运行以下命令:

bash 复制代码
wasm-pack build --target web

这里的--target web参数明确指定了输出格式为适用于浏览器环境的模块。在编译过程中,wasm-pack会根据这个参数生成符合 ES6 模块规范的 JavaScript 胶水代码。

当命令执行完毕后,项目目录中多了一个pkg/目录,这里就是存放构建产物的地方。

pkg/目录下,有两个关键文件:markdown_editor.jsmarkdown_editor_bg.wasm。它们是一对紧密合作的 "伙伴",共同为前端页面提供强大的 Markdown 解析功能 。

(二)构建产物解析

  • markdown_editor_bg.wasm 文件是二进制格式的 WebAssembly 模块,它包含了编译后的 Rust 代码,是整个项目的核心计算逻辑所在。它以紧凑的二进制形式存储,在浏览器中加载和执行时,能够以接近原生的速度运行,为 Markdown 解析提供了强大的性能支持 。

  • markdown_editor.js 则是自动生成的 JavaScript 接口层,它充当了 Rust 与 JavaScript 之间的 "翻译官"。这个文件封装了 Wasm 模块的加载和函数调用逻辑,使得 JavaScript 代码能够方便地加载 rust_markdown_editor_bg.wasm 模块,并调用其中由 #[wasm_bindgen] 导出的 render_markdown 函数。rust_markdown_editor.js会处理好数据类型的转换、函数参数的传递等复杂细节,让前端开发者可以像调用普通 JavaScript 函数一样调用 Rust 编写的 Markdown 解析逻辑 ,极大地降低了开发的难度和复杂性 。

  • markdown_editor.d.ts 是 TypeScript 类型定义文件,它为前端 JavaScript 代码提供了类型信息,使得在开发过程中能够获得更好的代码提示和类型检查 。

六、创建前端界面:搭建交互层与视图层

有了强大的 Markdown 解析能力,再加上前端界面,用户就能够与我们的应用进行交互了。

接下来我们将创建一个简单直观的 HTML 页面,并编写 JavaScript 代码来加载 Wasm 模块,实现 Markdown 文本的实时预览功能。

在项目根目录下(与 srcpkg 同级),创建一个 index.html 文件:

html 复制代码
<!doctype html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Rust Wasm Markdown 编辑器</title>
        <style>
            body {
                font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
                max-width: 1200px;
                margin: 0 auto;
                padding: 20px;
                background-color: #f5f5f5;
            }
            .container {
                display: flex;
                gap: 20px;
                height: 80vh;
            }
            .editor-panel,
            .preview-panel {
                flex: 1;
                background: white;
                border-radius: 8px;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                padding: 20px;
                overflow: auto;
            }
            textarea {
                width: 100%;
                height: 100%;
                border: none;
                resize: none;
                font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
                font-size: 14px;
                outline: none;
            }
            h1 {
                color: #333;
                text-align: center;
                margin-bottom: 30px;
            }
            .panel-title {
                font-weight: bold;
                margin-bottom: 10px;
                color: #555;
            }
        </style>
    </head>
    <body>
        <h1>Rust Wasm Markdown 编辑器</h1>

        <div class="container">
            <div class="editor-panel">
                <div class="panel-title">编辑区 (Markdown)</div>
                <textarea id="editor" placeholder="请输入 Markdown 文本...">
# Hello Rust Wasm!
            </textarea
                >
            </div>

            <div class="preview-panel">
                <div class="panel-title">预览区 (HTML)</div>
                <div id="preview"></div>
            </div>
        </div>

        <script type="module">
            import init, { render_markdown } from "./pkg/markdown_editor.js";

            async function run() {
                // 初始化 Wasm 模块
                await init();

                const editor = document.getElementById("editor");
                const preview = document.getElementById("preview");

                // 初始渲染
                updatePreview();

                // 监听输入事件
                editor.addEventListener("input", updatePreview);

                function updatePreview() {
                    try {
                        const markdownText = editor.value;
                        const html = render_markdown(markdownText);
                        preview.innerHTML = html;
                    } catch (error) {
                        preview.innerHTML = `<div style="color: red;">渲染错误: ${error}</div>`;
                    }
                }
            }

            run().catch(console.error);
        </script>
    </body>
</html>

updatePreview 实现了实时预览功能,当用户在编辑区输入 Markdown 文本时,会触发input事件,调用updatePreview函数更新预览区的内容。

这种设计模式使得用户在输入 Markdown 内容的同时,能够即时看到转换后的 HTML 效果,大大提升了用户体验。

七、运行与部署:从本地调试到生产环境

(一)本地运行

完成以上步骤,就可以运行我们的 Markdown 编辑器应用了。

运行应用的第一步是启动一个静态文件服务器,将我们编写的 HTML、JavaScript 和 Wasm 文件提供给浏览器访问。

bash 复制代码
# 使用 Python 简单服务器
python3 -m http.server 8080

# 或者使用 Node.js 的 http-server
npx http-server

然后在浏览器中打开 http://localhost:8080 就能看到你的 Markdown 编辑器了!

(二)部署优化(可选)

如果想要将应用部署到生产环境,还需要进行一些优化,以提升应用的性能和稳定性。

在构建 Wasm 模块时,添加--release标志,以启用优化并减少调试信息。

css 复制代码
wasm-pack build --release --target web

添加 --release 标志后,wasm-pack 会在构建过程中对代码进行一系列优化,如去除不必要的调试信息、优化函数调用、减少代码体积等。

这些优化措施可以显著提升 Wasm 模块的执行效率,让应用在生产环境中运行得更加流畅。

例如,在处理大段 Markdown 文本时,经过优化的 Wasm 模块能够更快地完成解析和渲染,减少用户等待的时间。同时,去除调试信息也可以减小 Wasm 文件的大小,加快文件的加载速度,进一步提升用户体验。

八、总结:Rust Wasm 实战中的技术价值

(一)工具链价值

工具/库 作用一句话
wasm-pack 一条龙生成可发布包
wasm-bindgen Rust ↔ JS 的翻译官
web-sys/js-sys DOM 操作和 JS 交互
pulldown-cmark 纯 Rust Markdown 解析器

(二)Rust Wasm 的核心优势

  1. 性能优势:Rust 编译生成的 Wasm 代码,在执行效率上表现卓越,近乎原生。Rust 编译器基于 LLVM,能够生成高度优化的机器码,使得 Wasm 模块在浏览器中能够快速执行,大大缩短了 Markdown 文本的解析时间,为用户带来了即时的预览反馈 。

  2. 内存安全:Rust 引以为傲的所有权系统,在编译期就对内存访问进行了严格的检查和管理,从根本上杜绝了空指针引用、数据竞争等常见的内存安全问题。这种内存安全特性,不仅提升了应用的可靠性,还减少了开发过程中的调试成本,让开发者可以更加专注于功能的实现和优化。

  3. 代码复用:借助 Rust Wasm,我们实现了代码的高效复用。在项目中,Markdown 解析的核心逻辑既可以在前端的 Wasm 模块中运行,为用户提供实时预览功能;也可以在后端的 Rust API 服务中复用,用于服务器端的 Markdown 内容处理。

结语

这个 Markdown 编辑器虽然简单,但完整展示了 Rust WebAssembly 的开发全流程。因此你可以在此基础上继续扩展功能,比如添加代码高亮、导出功能、历史版本等。

相关推荐
Pu_Nine_92 小时前
Axios 实例配置指南
前端·笔记·typescript·axios
hit56实验室3 小时前
腾讯云的运维笔记——从yum的安装与更新源开始
运维·笔记·腾讯云
再睡一夏就好4 小时前
浅谈new与::operator new
笔记
梅见十柒4 小时前
UNIX网络编程笔记:共享内存区和远程过程调用
linux·服务器·网络·笔记·tcp/ip·udp·unix
我命由我123456 小时前
Word - Word 查找文本中的特定内容
运维·经验分享·笔记·word·运维开发·文档·文本
崔高杰6 小时前
大模型训练中对SFT和DPO的魔改——PROXIMAL SUPERVISED FINE-TUNING和Semi-online DPO论文阅读笔记
论文阅读·笔记
~黄夫人~8 小时前
Nginx Ubuntu vs CentOS 常用命令对照表---详解笔记
运维·笔记·学习·nginx·ubuntu·centos
ZZHow10249 小时前
React前端开发_Day10
前端·笔记·react.js·前端框架·web
会思考的猴子10 小时前
UE5 PCG 笔记(三) Normal To Density 节点
笔记·ue5