我用 Rust 重写网站,性能居然提升了 18 倍!

14cde25c19ac443393d01752631d50eb.gif

摘要:对于构建中小型网站和个人博客来说,Hakyll 是一个不错的静态网站生成器库,9 年前的 Jonas Hietala 正是选择了 Hakyll 编写博客网站。但随着时间的推移,网站出现各种问题,考虑多种因素之后,Jonas Hietala 决定用 Rust 重写,看看他是怎么实现吧!

原文链接:https://www.jonashietala.se/blog/2022/08/29/rewriting_my_blog_in_rust_for_fun_and_profit/

声明:本文为 CSDN 翻译,未经授权,禁止转载。


作者 | Jonas Hietala

译者 | 弯月

出品 | CSDN(ID:CSDNnews)

我使用Hakyll编写我的静态网站已经有9年了。在此之前,我使用过Jekyll,还使用过Perl的Mojolicious和PHP的Kohana编写更动态的页面。

但这些已成为过去,最近我决定使用Rust重新编写我的网站。

我用 Rust 重写网站,性能居然提升了 18 倍!_第1张图片

c92f69f6210bebfd5f116cb95bcb170a.png

需要解决的难题

我想通过这次重写网站,重点解决以下难题:

1.速度越来越慢

在我的笔记本电脑上,重构一次网站需要75秒(不是编译,只是生成网站)。我的网站只有240个帖子,所以我认为不至于如此之慢。虽然我可以使用缓存系统,并在编辑期间通过watch命令查看更新后的帖子,但这个速度仍然是我无法接受的。

2.许多外部依赖项

虽然站点生成器本身是用Haskell编写的,但除了许多Haskell库之外,还有一些依赖项。我的博客助手脚本是用Perl编写的,我使用sassc转换sass,然后利用Python的pygments高亮显示语法,并使用s3cmd将生成的站点上传到S3。

安装所有这些工具并保持最新状态是一件很麻烦的事情。我希望能有一个统一的工具解决所有问题。

3.设置问题

有时,网站会出现一些问题,我必须花时间调试和修复。每当我有一些新的写作思路,却发现网站生成器有问题,就会觉得很沮丧。

你可能会想,什么地方会出问题?

有时,一些软件包的更新可能会破坏网站。例如,

  • 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 feed中渲染的代码。

我知道上述问题都可以得到解决,但我只想要一些可以正常工作的东西。

4.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代码)和模板,并删除URL中的index(避免链接以index.html结尾)。

简单明了!

但是我已经很多年没有使用过Haskell了,而且我不想在网站上添加复杂的东西。

例如,我曾想在帖子中添加链接“下一个/上一个”,但后来我不得不花时间重新学习Haskell和Hakyll。即便如此,最后我想到的解决方案还是超级慢,因为我采用了一个线性搜索来查找下一个/上一个帖子,但我未能搞清楚如何正确地利用Hakyll实现这个链接。

我相信你有更好的方法,但对我来说,这样的小功能不值得付出这么大的努力。

94984fef32ca0babb93ff0645dbfe6aa.png

为什么选择Rust?

1.我很喜欢Rust,而且它很适合业余项目

2.Rust非常高效,非常擅长转换文本。

3.Cargo很流行,只要安装了Rust,运行cargo build,就可以构建网站

为什么要重新发明轮子?

我想编写一个静态站点生成器,这是一个非常有趣的项目,难度应该不大,但可以让我全权控制网站,而且灵活性完胜我使用过的所有网站生成器。

实现细节

我不打算在此介绍所有的细节,如果你感兴趣,请查看源代码(地址:https://github.com/treeman/jonashietala)。

利用现有的库处理难度较大的工作

起初我很担心重新实现我喜欢的Hakyll功能的难度会太大,例如模板引擎、多种语言的语法高亮显示,以及通过watch命令自动重新生成编辑过的页面并充当文件服务器,这样我就可以在写作的过程中随时在浏览器中查看帖子。

事实证明,我可以利用现有的库处理一些难度较大的工作。以下是我使用的一些效果很好的库:

  • tera:模板引擎。

它比Hakyll更强大,甚至可以执行循环之类的操作:

  • pulldown-cmark:解析Markdown。

这个库非常适合CommonMark(Markdown标准的语法规范)。

虽然速度很快,但支持的功能不如Pandoc多,所以我不得不做一些扩展。

  • syntect:语法高亮显示,支持Sublime Text的语法。

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

  • grass:Rust中的Sass编译器。

  • axum:创建本地托管站点的静态文件服务器。

  • hotwatch:监视文件变更,在文件更新时更新页面。

  • scraper:解析生成的HTML。我的一些测试和特定的转换中用到了这个库。

  • rust-s3:将生成的站点上传到 S3 存储。

即便使用了这些库,Rust源代码本身也超过了6000行。在有些情况下,Rust代码可能会很冗长,我的代码确实不够漂亮,但是最后的代码量仍然超过了预期。

Markdown转换

如果我的帖子都采用标准的markdown,这个转换过程会更加容易,但多年来我添加了很多pulldown-cmark不支持的功能和扩展。所以,我不得不自己编写代码。

预处理

我有一个预处理步骤,利用多个图像来生成插图。这是一个通用的处理步骤,形式如下:

::: 

:::

我使用这个预处理来生成各种图像集合,例如Flex、Figure和Gallery。下面是一个例子:

::: Flex
/images/img1.png
/images/img2.png
/images/img3.png
Figcaption goes here
:::

需要转换成:

Figcaption goes here

那么,我该如何实现呢?当然是使用正则表达式。

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\w+)
\s*
$
# Content inside
(?P.+?)
# Ending :::
^:::$
"#
)
.unwrap();
}
pub fn parse_fenced_blocks(s: &str) -> Cow {
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 }

可以转换成:

这种用法在我的代码中比比皆是,所以我决定重新实现。

我在Pandoc中使用的功能还有一个不受支持:在HTML标签内计算markdown。比如下面这段代码就无法正确呈现:

起初我的计划是,在通用预处理中实现,但后来发现这样做会丢失链接引用。比如下面这个例子:

::: Aside
My [link][link_ref]
:::
[link_ref]: /some/path

link不会变成链接,因为我们只会在 ::: 内解析。

> Some text
{ :notice }

这段代码会调用notice解析器,在这个示例中,它将创建一个

你可能感兴趣的:(资讯,python,java,javascript,linux,编程语言)