AICon 深圳站聚焦 Agent 技术、应用与生态,大咖分享实战干货 了解详情
写点什么

Rust 治好了我的精神内耗

  • 2022-08-30
    北京
  • 本文字数:5567 字

    阅读完需:约 18 分钟

Rust 治好了我的精神内耗

九年来,我一直用Hakyll作为静态站点的生成工具。再往前追溯,我主要用的是 Jekyll,动态页面大概是用 Perl 加 Mojolicious 和 PHP 加 Kohana 来实现。但我对这些只有模糊的印象,当时还没有 git,所以很多开发痕迹都找不到了。


如今,我终于下定决心,打算转向自己用Rust亲手编写的自定义站点生成器。通过此番重写,我主要是想解决以下三个问题:


第一,越来越慢的速度。在我的低配笔记本电脑上,完整站点的重建大概要 75 秒(不涉及编译,单纯只是站点生成)。我这博客上一共只有 240 篇帖子,所以应该不至于这么慢才对。虽然已经配备了不错的缓存系统,并且只在编辑期间对帖子变更执行更新的 watch 命令,但整个执行速度还是太慢了。


第二,外部依赖项。虽然站点生成器本身是用 Haskell 编写的,但除了众多 Haskell 库之外,其中还包含其他依赖项。我的博客助手脚本是用 Perl 编写的,我使用 sassc 进行 sass 转换,还使用 Python 的 pygments 实现语法高亮,并使用 s3cmd 将生成的站点上传至 S3。管理和更新这么多依赖项真的很烦人,我想摆脱麻烦,专心回归到博客内容上来。


第三,设置问题。跟大量依赖项相关,我的博客网站有时候会宕机,必须得花时间调试和修复。有时候我脑子里刚有点灵感,系统就崩溃了,必须赶紧把网站生成器换掉。


有些朋友可能会问,这么简单的网站还有什么可崩溃的?主要还是更新的锅,往往会以意想不到的方式引发问题。例如:


  • 在更新 GHC 之后,它无法找到 cabal 包。

  • 在运行 Haskell 二进制文件时,系统提示:


[ERROR] Prelude.read: no parse(只出现在台式机上,在我的低配笔记本上倒是运行良好。)


或者是以下 Perl 错误:


Magic.c: loadable library and perl binaries are mismatched (got handshake key 0xcd00080, needed 0xeb00080)(只出现在笔记本上,在台式机上运行良好。)


  • 当 Hakyll 的不同版本间发生 Pandoc 参数变更时,就会破坏 Atom 提要中的代码渲染效果。我知道这些并不是太大的问题,可我只希望轻轻松松写个博文,所以能正常运行才是头号目标。

Haskell 引发了我的精神内耗

其实我还挺喜欢 Haskell 的,特别是它纯函数的部分。另外,我也很喜欢 Hakyll 对站点配置使用的声明性方法。以生成静态(即独立页面)为例:


match "static/*.markdown" $ do    route   staticRoute    compile $ pandocCompiler streams        >>= loadAndApplyTemplate "templates/static.html" siteCtx        >>= loadAndApplyTemplate "templates/site.html" siteCtx        >>= deIndexUrls
复制代码


就算看不懂 $ 和 >>=分别代表什么,仍然能看出我们是在从 static/ 文件夹中查找文件,再把这些文件发送至 pandocCompiler (以转换原始的 markdown 格式)、再发送至模板,之后取消索引 urls(以避免链接以 index.html 结尾)。


多么简单,多么明了!


但我已经很多年没用过 Haskell 了,所以每当需要在网站上添加稍微复杂点的功能,都需要耗费巨大的精力。


例如,我想在帖子中添加下一篇/上一篇的链接,却难以轻松实现。最后,我不得不拿出时间重新学习了 Haskell 和 Hakyll。即使如此,我琢磨出的解决方案也非常慢,是依靠线性搜索来查找下一篇/上一篇帖子。直到现在,我也不知道要怎么以正确的设置方式通过 Hakyll 实现这个功能。


相信各位大牛肯定有好办法,但对我来说这么一项小功能耗费的精神也太多了,着实受不了。

为什么选择 Rust?


  1. 我喜欢用 Rust,偏好基本足以决定这类业余项目的实现方法。

  2. Rust 的性能很强,文本转换应该也正是它所擅长的任务。

  3. Cargo 让人非常省心。在安装了 Rust 之后,就可以执行 cargo build 并等待运行结果。为什么要重新发明轮子?因为我想发挥主观能动性,试试自己能编写出怎样的静态站点生成器。这事应该不是特别难,我能借助它完全控制自己的博客网站,享受远超现成生成器的功能灵活性。当然,我也知道 cobalt 这类工具能配合任意语言对页面进行灵活的类型转换。我只是想在灵活之余,享受一下解决问题的乐趣。


关于实施的细节,因受篇幅所限,我没办法在文章中完整回顾整个构建过程。感兴趣的朋友可以点击此处(https://github.com/treeman/jonashietala)查看项目源代码。

将“硬骨头”各个击破


起初,我很担心没法重现自己熟悉的各种 Hakyll 功能,例如模板引擎、多种语言的语法高亮显示,或者自动重新生成编辑的页面并充当文件服务器的 watch 命令,有了它我才能边写作边在浏览器中查看帖子。


但事实证明,每块“硬骨头”都有对应的理想的工具。下面来看我使用的几个效果拔群的库:


  • 使用 tera 作为模板引擎。它比 Hakyll 更强大,因为它能执行循环等复杂操作:


<div class="post-footer">  <nav class="tag-links">      Posted in {% for tag in tags %}{% if loop.index0 > 0 %}, {% endif %}<a href="{{ tag.href }}">{{ tag.name }}</a>{% endfor %}.  </nav></div>
复制代码


  • 使用 pulldown-cmark 来解析 Markdown。对于 Markdown 的标准语法规范 CommonMark,pulldown-cmark 的表现真的很棒。虽然速度更快,但它的支持范围不像 Pandoc 那么广泛,所以我还得配合其他功能进行支持性扩展。这个问题稍后再谈。

  • 用 syntect 实现语法高亮,能够支持 Sublime Text 语法。

  • 用 yaml-front-matter 解析帖子中的元数据。

  • 用 grass 作为纯 Rust 中的 Sass 编译器。

  • 用 axum 创建负责在本地托管站点的静态文件服务器。

  • 用 hotwatch 监控文件变更,这样就能在文件内容变化时更新页面。

  • 用 scraper 解析生成的 html。我的某些测试和特定转换中需要用到。

  • 用 rust-s3 将生成的站点上传至 S3 存储端。即使有了这些库,我的 Rust 源文件本身还是超过了 6000 行。必须承认,Rust 代码可能有点冗长,再加上我自己的水平也不高,但这个项目的编写工作量还是比预期要多不少。(但好像软件项目都是这样……)

Markdown 转换

虽然在帖子里只使用标准 markdown 能免去这一步,但多年以来我的帖子已经涉及大量 pulldown-cmark 无法支持的功能和扩展,所以只能亲手编码来解决。

预处理

我设置了一个预处理步骤,用以创建包含多个图像的图形。这是个通用的处理步骤,具体形式如下:


::: <type><content>:::
复制代码


我将它用于不同类型的图像集合,例如 Flex, Figure 以及 Gallery。下面来看示例:


::: Flex/images/img1.png/images/img2.png/images/img3.png Figcaption goes here:::
复制代码


它会被转换为:


<figure class="flex-33"><img src="/images/img1.png" /><img src="/images/img2.png" /><img src="/images/img3.png" /><figcaption>Figcaption goes here</figcaption></figure>
复制代码


这是怎么实现的?当然是用正则表达式啦!


use lazy_static::lazy_static;use regex::{Captures, Regex};use std::borrow::Cow; lazy_static! {    static ref BLOCK: Regex = Regex::new(        r#"(?xsm)        ^        # Opening :::        :{3}        \s+        # Parsing id type        (?P<id>\w+)        \s*        $         # Content inside        (?P<content>.+?)         # Ending :::        ^:::$        "#    )    .unwrap();} pub fn parse_fenced_blocks(s: &str) -> Cow<str> {    BLOCK.replace_all(s, |caps: &Captures| -> String {        parse_block(            caps.name("id").unwrap().as_str(),            caps.name("content").unwrap().as_str(),        )    })} fn parse_block(id: &str, content: &str) -> String {    ...}
复制代码


(图像和图形解析部分太长了,所以咱们直接跳过好了。)

扩展 pulldown-cmark

我还用自己的转换扩展了 pulldown-cmark:


// Issue a warning during the build process if any markdown link is broken.let transformed = Parser::new_with_broken_link_callback(s, Options::all(), Some(&mut cb));// Demote headers (eg h1 -> h2), give them an "id" and an "a" tag.let transformed = TransformHeaders::new(transformed);// Convert standalone images to figures.let transformed = AutoFigures::new(transformed);// Embed raw youtube links using iframes.let transformed = EmbedYoutube::new(transformed);// Syntax highlighting.let transformed = CodeBlockSyntaxHighlight::new(transformed);let transformed = InlineCodeSyntaxHighlight::new(transformed);// Parse `{ :attr }` attributes for blockquotes, to generate asides for instance.let transformed = QuoteAttrs::new(transformed);// parse `{ .class }` attributes for tables, to allow styling for tables.let transformed = TableAttrs::new(transformed);
复制代码


我以前也做过标题降级和嵌入裸 YouTube 链接之类的尝试,实现起来相当简单。不过现在想想,在预处理或后处理步骤中嵌入 YouTube 链接可能会更好。


Pandoc 能够支持向任意元素添加属性和类,这可太实用了。所以下面这部分:


![](/images/img1.png){ height=100 }
复制代码


可以转换成这个样子:


<figure>  <img src="/images/img1.png" height="100"></figure>
复制代码


这项功能随处都有用到,所以我决定在 Rust 中重新实现,只是这次要用一种不那么笼统和老套的方式。


我在 Pandoc 中用到的另一项冲突功能,就是评估 html 标签内的 markdown。现在的呈现效果有问题:


<aside>My [link][link_ref]</aside>
复制代码


我起初是打算在通用预处理步骤中实现这项功能的,但后来我总会忘记引用链接。因此在以下示例中:


::: AsideMy [link][link_ref]::: [link_ref]: /some/path
复制代码


link 就不再被转化为链接了,所有解析都只在:::内完成。


> Some text{ :notice }
复制代码


这样会调用一个通知解析器,它会在以上示例中创建一个 <aside>标签(而非 <blockquote> 标签),同时保留已解析的 markdown。


虽然现有 crate 会使用 syntect 添加代码高亮,但我还是自己编写了一个功能,把它打包在<code>标签中以支持内联代码高亮。例如,“Inside row: let x = 2;”会显示为:


Inside row: `let x = 2;`rust
复制代码

性能提升


我没花太多时间来优化性能,但还是发现了两个性能要点。


首先,如果使用 syntect 并包含自定义语法,那就应该把 SyntaxSet 压缩为二进制格式。


另一点就是使用 rayon 实现并行化渲染。所谓渲染,就是指 markdown 解析、应用模板和创建输出文件的过程。Rayon 的强大之处,在于它在执行这项任务时的效率只受限于 CPU 性能,而且易用性非常好(只要代码结构正确)。下面是渲染的简化示例:


fn render(&self) -> Result<()> {    let mut items = Vec::new();     // Add posts, archives, and all other files that should be generated here.    for post in &self.content.posts {        items.push(post.as_ref());    }     // Render all items.    items        .iter()        .try_for_each(|item| self.render_item(*item))}
复制代码


要实现并行化,我们只需要将 iter() 更改为 par_iter():


use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; items    .par_iter() // This line    .try_for_each(|item| self.render_item(*item))
复制代码


这就成了,非常简单!


我也承认,这里的性能提升非常有限,真正的性能改善主要还是来自我使用的库。例如,我的旧站点使用由 Python 编写的外部 pygments 进程来实现语法高亮,而现在的替代方案是 Rust 编写的高亮器。后者不仅速度快得多,而且并行化难度也更低。

健全性检查

维护自己的网站,我才发现原来开发项目这么容易出错。比如一不留神就链接到了不存在的页面或图像,或者没有使用[my link][todo]来定义链接引用,而且在发布前还总是忘记更新。


所以,除了测试 watch 命令等基本功能之外,我还解析了整个站点,并检查所有内部链接是否存在且正确(也会验证/blog/my-post#some-title 中的 some-title 部分)。外部链接也是要检查的,但我在这里使用的是手动命令。


在文章的开头,我列出了自己之前的一些设置问题。下面咱们就看看具体解决得怎么样。在生成过程中,我也采取了比较严苛的检查标准,尽可能避免遗漏各种稀奇古怪的错误。

效果如何?

在文章的开头,我列出了之前设置中的一些问题。下面咱们就一起来看具体解决得怎么样。


  • 性能方面现在,还是那台低配笔记本电脑,完整的站点重建(不包含编译时间)只需要 4 秒。性能一下子提升了 18 倍,这个成绩算是相当不错了。当然,这里面肯定还有进步空间——比如,我使用 rayon 处理文件 IO,如果采取异步机制肯定还能再优化一些;而且我也没有使用缓存系统,所以每次构建时都得重新生成所有文件(但通过观察,我发现构建过程还挺智能的)。


请注意,我不是说 Rust 就一定比 Haskell 更快,这里比较的只是两种具体实现。相信肯定有高手能在 Haskell 中实现同样的速度提升。


  • 单一依赖现在我的所有功能都在 Rust 中实现,不需要安装和维护任何外部脚本/工具。

  • Cargo 不添麻烦只要在系统里用上 Rust,cargo build 就永远服服帖帖、不添麻烦。我觉得这可能就是低调的 Rust 最突出的优势之一——build 系统不给人找事。


大家用不着手动查找丢失的依赖项,牺牲某些子功能来实现跨平台,或者在构建系统自动拉取更新时造成破坏。往椅子里一躺,等着代码编译完成就行。

Rust 治好了我的精神内耗

虽然我发现在 Rust 当中,创建系列文章或者上一篇/下一篇链接之类的功能确实更轻松,但我并不是想说 Rust 就比 Haskell 更简单易用。我的意思是,Rust 对我个人来说比 Haskell 更容易理解。


而其中最大的区别,很可能在于实践经验。我最近一直在用 Rust,而从小十年前使用 Haskell 完成网站创建以来,我就几乎没跟 Haskell 打过什么交道。所以如果我也十年不接触 Rust,那再次使用起来肯定也是痛苦万分。


总体来说,我对自己的这次尝试非常满意。这是个好玩又有益的项目,虽然工作量超出了我的预期,但也确实消除了长期困扰我的老大难问题。希望我的经历对各位有所帮助。


原文链接:


https://www.jonashietala.se/blog/2022/08/29/rewriting_my_blog_in_rust_for_fun_and_profit/

2022-08-30 14:206370

评论 1 条评论

发布
用户头像
看了另外一个版本的翻译,这个翻译比那个好:),不错不错。
2022-09-05 16:06 · 广东
回复
没有更多了
发现更多内容

什么是需求管理,产品如何进行需求管理

阿里云云效

云计算 阿里云 需求管理 持续交付 产品研发

Nebula Graph 在众安金融的图实践

NebulaGraph

图数据库 知识图谱 保险业

低代码实现探索(三十九)组件库的开发

零道云-混合式低代码平台

天翼云云主机上搭建FTP服务最佳实践

天翼云开发者社区

从2018到2022: 一个大数据工程师眼中的TiDB

TiDB 社区干货传送门

企业怎么制作帮助文档

小炮

企业 帮助文档

从一起Linux云主机无法远程ssh登录故障说起

天翼云开发者社区

Linux之fgrep命令

入门小站

Linux

java高级用法之:JNA类型映射应该注意的问题

程序那些事

Java Netty 程序那些事 3月月更

国产化浪潮下TiDB解决的痛点问题

TiDB 社区干货传送门

将 AWS S3 数据迁移至 TiDB Cloud 集群

TiDB 社区干货传送门

轻轻松松实现本地和云主机之间的文件上传下载

天翼云开发者社区

5 款阿里常用代码检测工具,免费用!

阿里云云效

云计算 阿里云 代码审查 研发 代码检测

[Day4]-[二分查找] 查找数组元素位置

方勇(gopher)

LeetCode 数据结构与算法

一文简述:云端架构的演变过程

穿过生命散发芬芳

3月月更

windowsXP用户无法远程桌面连接天翼云2008云主机

天翼云开发者社区

【技术干货分享】一文了解Nginx反向代理与conf原理

Linux服务器开发

nginx 负载均衡 反向代理 后端开发 Linux服务器开发

玩转天翼云安全组

天翼云开发者社区

Flutter 路由参数处理

岛上码农

flutter ios开发 Android开发 移动端开发 3月月更

【征文大赛】TiDB 社区专栏第一届征文大赛,快来一次性集齐所有周边吧!

TiDB 社区干货传送门

AI工具-标注工具labelme

AIWeker

人工智能 标注工具

在线Javascript压缩工具

入门小站

工具

Q1过去了,Gartner战略技术趋势在不动产领域落了几项?

大数据 技术 低代码 AIOT 分布式,

如何实现Spring Gateway 路由的动态加载和刷新?

领创集团Advance Intelligence Group

微服务 Spring Cloud API api 网关

AI目标检测概要

AIWeker

人工智能 目标检测

HAVE FUN|Layotto 源码解析

SOFAStack

GitHub 开发者 活动 源码解析 源码剖析

在线MarkDown转HTML工具

入门小站

工具

浅谈外挂常识和如何防御

行者AI

AI观点说-关于深度学习的一点思考

AIWeker

人工智能 深度学习

一张图看懂全球最新DDoS攻击趋势

科技热闻

数字化转型-基本认知

Geek_XOXO

数字化转型

Rust 治好了我的精神内耗_文化 & 方法_Jonas Hietala_InfoQ精选文章