为什么这个网站是用 C 构建的?
Created at: 2024-08-26
我从 [2017](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<http:/web.archive.org/web/20171124021420/http:/marcelonet.com/snippets/>) 年开始在个人网站上写一些 **东西**。
大部分内容都是自我备忘录,主要记录如何做 A 或者 B。
直到最近,我才开始整理这些笔记,形成特定主题的文章。
我意识到,阻碍我更频繁写作的不是缺乏想法(或动力),而是不得不处理当时使用的网站构建器和平台所带来的麻烦。
当时还没有 GitHub pages,通常的做法是在某个 web 提供商上运行一个 Apache 服务器来托管网站。我对 Apache 一无所知,而且看到的一点也不感兴趣,所以我寻找了替代方案。
我使用 Django (由 Nginx 提供服务) 在 Digital Ocean 上的服务器上构建了我的第一个网站。那时还没有 Droplets,所以我不得不租用一台 Ubuntu 机器,每月花费 5.00 美元。考虑到我还必须支付其他服务(注册商、电子邮件等),对于一个拿着巴西工资的开发者来说,这有点贵。
我当时非常有动力发布内容,因为我仍然是 web 开发领域的新手,想了解一切是如何运作的。我也不知道自己在做什么,想把自己的网站作为一个沙箱,可以在里面尝试新事物。
这是我的第一个错误。使用 Django 构建 "静态" 网站太麻烦了。你必须设置 views、templates、运行服务器、获取 GitHub hooks,以便在 Digital Ocean 中推送新提交时重置远程服务器等等。
一旦对新手博客发布者的浪漫幻想消失,处理整个设备来发布一条笔记所花费的时间比编写笔记本身还要长。
在某个时候,我必须在网站变得太大之前进行切换。
我的第二个尝试是放弃整个网站,并使用静态网站生成器从头开始。我决定使用 Nuxt,因为我在工作中使用 Vue,而且整个设置看起来很容易上手。
一开始感觉不错。我用 GitHub Pages 设置了它。我只需要通过 cli 命令将 Nuxt 创建的静态站点推送到我的 git 仓库,GitHub 会为我处理剩下的事情。这比之前的架构有了重大改进。最重要的是,我可以嵌入 JavaScript 并使用框架与之交互,从而实现很酷的动态效果。
但我只有一个博客文章需要花哨的 JavaScript 工具。很快,维护网站再次变得痛苦。发布文章涉及到用 Vue 编写内容,而这并不是一种符合人体工程学的常规散文写作方式。
此外,该框架是一项新技术,维护者不断推出不向后兼容的更新。处理 Vue 和 Nuxt 的版本以及它们的所有 JavaScript 依赖关系是一个很大的痛点,我不得不在某个时候放弃。
## 现在
从过去的这两个错误中吸取教训,我为我的下一个(也是希望是最终的)网站制定了一系列要求:
1. 开始一篇文章必须像在一个空白文件中输入内容一样容易。
2. 网站必须是静态生成的。而且要快。
3. 生成网站的依赖项应该很少或没有。
4. 它需要至少持续未来 10 年。
第一个要求可以通过使用 markdown 文件编写来满足。在 Neovim 中编写这篇博客文章看起来像这样:

第二个要求有点棘手,但它与第三个要求直接相关。
用 markdown 编写文章意味着需要一个解析器将文件转换为 html。我可以自己编写这个解析器,并拥有一个零依赖的漂亮的静态站点生成器,或者我可以允许自己有一个依赖。
问题是从零依赖到一依赖的转变是巨大的。它感觉比从 10 个依赖到 100 个依赖要大得多。
问题是编写 markdown 解析器并不是最简单的事情。与此同时,解析器是我唯一需要的依赖。我设法说服自己接受一个依赖是没问题的,然后我就继续了。
我的第一反应是求助于 [Pandoc](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<https:/pandoc.org/>)。我这样做了,并实现了一个小型 shell 脚本,可以读取我的 markdown 文件目录树并将它们转换为 html。
对于大约 20 到 30 个 markdown 文件,这工作得很好。在那之后,将文件转换为 html 的过程开始在速度上恶化。Pandoc 是用 Haskell 编写的,它并不以快速解析大量文件而闻名。
为了节省重新编译时间,另一种方法是更新我的脚本,以便只有新的 markdown 文件或更改的文件才会被标记为重新编译。如果我想让脚本变得漂亮而健壮,那将涉及到太多的魔法。我不想这样做。我不希望我的脚本增长到需要开始添加测试用例和覆盖率的地步。
我也知道,在个位数秒内解析数百甚至数千个小文件应该是可行的。问题是 Pandoc 拖慢了一切,所以我的第二个要求没有得到满足。
更重要的是,整个 Pandoc 生态系统需要 **大量** 的依赖。确切地说,是 227 个依赖和超过 400MB 的安装大小:
Packages (227) ghc-libs-9.2.8-1 haskell-aeson-2.1.2.1-47 haskell-aeson-pretty-0.8.10-7 haskell-ansi-terminal-0.11.4-66 haskell-ansi-wl-pprint-0.6.9-418 haskell-appar-0.1.8-14 haskell-asn1-encoding-0.9.6-230 haskell-asn1-parse-0.9.5-230 haskell-asn1-types-0.3.4-209 haskell-assoc-1.0.2-266 haskell-async-2.2.5-27 haskell-attoparsec-0.14.4-74 haskell-attoparsec-aeson-2.1.0.0-31 haskell-attoparsec-iso8601-1.1.0.0-50 haskell-auto-update-0.1.6-339 haskell-base-compat-0.12.2-2 haskell-base-compat-batteries-0.12.2-83 haskell-base-orphans-0.8.8.2-13 haskell-base-unicode-symbols-0.2.4.2-14 haskell-base16-bytestring-1.0.2.0-80 haskell-base64-0.4.2.4-69 haskell-base64-bytestring-1.2.1.0-104 haskell-basement-0.0.16-2 haskell-bifunctors-5.6-77 haskell-bitvec-1.1.3.0-94 haskell-blaze-builder-0.4.2.3-2 haskell-blaze-html-0.9.1.2-226 haskell-blaze-markup-0.8.3.0-10 haskell-boring-0.2.1-3 haskell-bsb-http-chunked-0.0.0.4-383 haskell-byteorder-1.0.4-25 haskell-call-stack-0.4.0-184 haskell-case-insensitive-1.2.1.0-203 haskell-cassava-0.5.3.1-4 haskell-cereal-0.5.8.3-2 haskell-citeproc-0.8.1-105 haskell-cmdargs-0.10.22-2 haskell-colour-2.3.6-210 haskell-commonmark-0.2.4.1-1 haskell-commonmark-extensions-0.2.4-2 haskell-commonmark-pandoc-0.2.1.3-82 haskell-comonad-5.0.8-261 haskell-conduit-1.3.5-53 haskell-conduit-extra-1.3.6-134 haskell-constraints-0.13.4-50 haskell-contravariant-1.5.5-4 haskell-cookie-0.4.6-2 haskell-crypton-0.34-11 haskell-crypton-connection-0.3.2-8 haskell-crypton-x509-1.7.6-28 haskell-crypton-x509-store-1.6.9-28 haskell-crypton-x509-system-1.6.7-28 haskell-crypton-x509-validation-1.6.12-28 haskell-data-array-byte-0.1.0.1-55 haskell-data-default-0.7.1.1-306 haskell-data-default-class-0.1.2.0-25 haskell-data-default-instances-containers-0.0.1-37 haskell-data-default-instances-dlist-0.0.1-319 haskell-data-default-instances-old-locale-0.0.1-37 haskell-data-fix-0.3.2-102 haskell-dec-0.0.5-5 haskell-digest-0.0.1.7-2 haskell-digits-0.3.1-21 haskell-distributive-0.6.2.1-209 haskell-dlist-1.0-241 haskell-doclayout-0.4.0.1-29 haskell-doctemplates-0.11-71 haskell-easy-file-0.2.5-21 haskell-emojis-0.1.3-10 haskell-erf-2.0.0.0-25 haskell-fast-logger-3.1.2-74 haskell-file-embed-0.0.15.0-2 haskell-foldable1-classes-compat-0.1-77 haskell-generically-0.1.1-2 haskell-ghc-bignum-orphans-0.1.1-2 haskell-glob-0.10.2-90 haskell-gridtables-0.1.0.0-48 haskell-haddock-library-1.11.0-17 haskell-hashable-1.4.3.0-46 haskell-hourglass-0.2.12-246 haskell-hslua-2.3.0-52 haskell-hslua-aeson-2.3.0.1-34 haskell-hslua-classes-2.3.0-53 haskell-hslua-core-2.3.1-45 haskell-hslua-list-1.1.1-60 haskell-hslua-marshalling-2.3.1-5 haskell-hslua-module-doclayout-1.1.0-58 haskell-hslua-module-path-1.1.0-53 haskell-hslua-module-system-1.1.0.1-27 haskell-hslua-module-text-1.1.0.1-27 haskell-hslua-module-version-1.1.0-53 haskell-hslua-module-zip-1.1.1-22 haskell-hslua-objectorientation-2.3.0-49 haskell-hslua-packaging-2.3.1-14 haskell-hslua-repl-0.1.2-11 haskell-hslua-typing-0.1.1-7 haskell-http-api-data-0.5.1-54 haskell-http-client-0.7.15-23 haskell-http-client-tls-0.3.6.3-58 haskell-http-date-0.0.11-136 haskell-http-media-0.8.1.1-14 haskell-http-types-0.12.4-6 haskell-http2-4.1.0-22 haskell-hunit-1.6.2.0-227 haskell-indexed-traversable-0.1.3-69 haskell-indexed-traversable-instances-0.1.1.2-44 haskell-integer-logarithms-1.0.3.1-7 haskell-iproute-1.7.12-82 haskell-ipynb-0.2-139 haskell-isocline-1.0.9-2 haskell-jira-wiki-markup-1.5.1-22 haskell-juicypixels-3.3.8-31 haskell-lexer-1.1.1-2 haskell-libyaml-0.1.4-5 haskell-lpeg-1.0.4-26 haskell-lua-2.3.2-6 haskell-memory-0.18.0-8 haskell-mime-types-0.1.2.0-2 haskell-mmorph-1.2.0-6 haskell-monad-control-1.0.3.1-102 haskell-mono-traversable-1.0.17.0-8 haskell-network-3.1.4.0-20 haskell-network-byte-order-0.1.7-2 haskell-network-uri-2.6.4.2-31 haskell-old-locale-1.0.0.7-31 haskell-old-time-1.1.0.4-2 haskell-onetuple-0.3.1-75 haskell-only-0.1-23 haskell-optparse-applicative-0.17.1.0-29 haskell-ordered-containers-0.2.3-2 haskell-pandoc-3.1.8-34 haskell-pandoc-lua-engine-0.2.1.2-23 haskell-pandoc-lua-marshal-0.2.4-2 haskell-pandoc-server-0.1.0.5-39 haskell-pandoc-types-1.23.1-21 haskell-pem-0.2.4-286 haskell-pretty-show-1.10-15 haskell-prettyprinter-1.7.1-165 haskell-primitive-0.7.4.0-111 haskell-psqueues-0.2.8.0-10 haskell-quickcheck-2.14.3-64 haskell-random-1.2.1.2-8 haskell-recv-0.1.0-30 haskell-regex-base-0.94.0.2-3 haskell-regex-tdfa-1.3.2.2-44 haskell-resourcet-1.2.6-51 haskell-safe-0.3.21-5 haskell-safe-exceptions-0.1.7.4-21 haskell-scientific-0.3.7.0-113 haskell-semialign-1.2.0.1-160 haskell-semigroupoids-5.3.7-142 haskell-servant-0.20.1-12 haskell-servant-server-0.20-23 haskell-sha-1.6.4.4-20 haskell-simple-sendfile-0.2.32-36 haskell-singleton-bool-0.1.7-3 haskell-skylighting-0.14-15 haskell-skylighting-core-0.14-14 haskell-skylighting-format-ansi-0.1-121 haskell-skylighting-format-blaze-html-0.1.1.2-8 haskell-skylighting-format-context-0.1.0.2-86 haskell-skylighting-format-latex-0.1-121 haskell-socks-0.6.1-237 haskell-some-1.0.5-2 haskell-sop-core-0.5.0.2-2 haskell-split-0.2.5-6 haskell-splitmix-0.1.0.5-22 haskell-statevar-1.2.2-3 haskell-streaming-commons-0.2.2.6-26 haskell-strict-0.4.0.1-240 haskell-string-conversions-0.4.0.1-171 haskell-syb-0.7.2.4-8 haskell-tagged-0.8.8-2 haskell-tagsoup-0.14.8-226 haskell-temporary-1.3-585 haskell-texmath-0.12.8.4-15 haskell-text-conversions-0.3.1.1-63 haskell-text-icu-0.8.0.5-2 haskell-text-short-0.1.5-79 haskell-th-abstraction-0.4.5.0-2 haskell-th-compat-0.1.5-2 haskell-th-lift-0.8.4-2 haskell-th-lift-instances-0.1.20-47 haskell-these-1.1.1.1-267 haskell-time-compat-1.9.6.1-97 haskell-time-manager-0.0.1-35 haskell-tls-1.8.0-29 haskell-toml-parser-1.3.1.3-18 haskell-transformers-base-0.4.6-102 haskell-transformers-compat-0.7.2-2 haskell-type-equality-1.0.1-1 haskell-typed-process-0.2.11.1-15 haskell-typst-0.3.2.1-32 haskell-typst-symbols-0.1.4-2 haskell-unicode-collation-0.1.3.6-12 haskell-unicode-data-0.4.0.1-33 haskell-unicode-transforms-0.4.0.1-74 haskell-uniplate-1.6.13-223 haskell-unix-compat-0.7.1-16 haskell-unix-time-0.4.13-1 haskell-unliftio-0.2.25.0-10 haskell-unliftio-core-0.2.1.0-2 haskell-unordered-containers-0.2.20-18 haskell-utf8-string-1.0.2-150 haskell-uuid-types-1.0.5.1-16 haskell-vault-0.3.1.5-185 haskell-vector-0.13.1.0-31 haskell-vector-algorithms-0.9.0.2-3 haskell-vector-stream-0.1.0.1-2 haskell-wai-3.2.4-19 haskell-wai-app-static-3.1.9-14 haskell-wai-cors-0.2.7-355 haskell-wai-extra-3.1.15-2 haskell-wai-logger-2.4.0-443 haskell-warp-3.3.30-59 haskell-witherable-0.4.2-101 haskell-word8-0.1.3-23 haskell-xml-1.3.14-31 haskell-xml-conduit-1.9.1.3-53 haskell-xml-types-0.3.8-9 haskell-yaml-0.11.11.2-49 haskell-zip-archive-0.4.3.2-2 haskell-zlib-0.6.3.0-60 hslua-cli-1.4.1-49 lua-lpeg-1.1.0-2 numactl-2.0.18-1 pandoc-cli-0.1.1.1-113 Total Download Size: 65.52 MiB Total Installed Size: 473.35 MiB
有太多的依赖项,我无法相信环境会在很长一段时间内保持稳定。我最不想做的事情是处理我的小博客上不向后兼容的更改。
我寻找了一个更好的替代方案,并找到了 [md4c](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<http:/github.com/mity/md4c>),它是一个用 C 编写的解析器,除了标准 C 库之外没有任何依赖项。它也只有一个头文件和一个源文件,可以很容易地将其直接嵌入到任何 C 项目中。
我唯一需要做的工作就是编写一个 C 脚本(结果大约是 250 LOC)来调用 md4c 函数并解析我的 `md` 文件,然后将这些转换后的文件放入 GitHub Pages 仓库中。
我的网站转换器脚本,全部在这个 [250 LOC](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<https:/gist.github.com/marcelofern/896574e055a05d011449b00217600fe6>) 源文件中(不包括 md4c),功能齐全,并且可以在任何支持 1999 年以后的 C 标准的编译器上运行。没有平台相关的代码,并且可以移植到 Windows、Linux 和 MacOS。
它运行得非常好。我现在有 87 个 markdown 文件,从头开始同时解析所有这些文件几乎可以瞬间完成:
[~] time ./scripts/website/converter.bin real 0m0.115s user 0m0.087s sys 0m0.091s
这让我可以完全清除整个仓库,并在几乎没有时间的情况下从头开始创建它。我不必担心创建特定的逻辑来仅重新解析已更改的文件或任何花哨的东西,这减少了维护的负担,并使我的脚本更小且更容易推理。
这个结果比 Pandoc 花费大量时间(超过两位数的秒数)来解析仅仅 87 个 markdown 文件要合理得多。
## 结尾
当前解决这个问题的一个流行的替代方案是 [Hugo](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<https:/gohugo.io/>)。Hugo 本身并没有什么不好的地方。它速度相当快(用 Go 编写),并且对于一个简单的网站来说很容易上手。它似乎比一些替代方案更好,比如 [pelican](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<https:/github.com/getpelican/pelican>),它是用 Python 编写的,因此解析 md 文件会更慢。
然而,Hugo 并没有特别吸引我,因为对于我需要的东西来说,这个框架似乎太大而且太主观了:
> Hugo 采用数据文件、i18n bundles、配置、布局模板、静态文件、assets 和用 Markdown、HTML、AsciiDoctor 或 Org-mode 编写的内容,并呈现一个静态网站。一些值得注意的特性是多语言支持、图像处理、资产管理、自定义输出格式、markdown render hooks 和 shortcodes。嵌套部分允许分离不同类型的内容,例如,对于包含博客和 podcast 的网站。
[source](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<http:/web.archive.org/web/20240808001552/https:/en.wikipedia.org/wiki/Hugo_\(software\)>)
里面有很多东西,以至于需要大量特性的大型网站(例如 Smashing Magazine)能够大量依赖 Hugo。
而且,尽管 Hugo 今天看起来令人满意,但我不希望它不会不断增长和变化,以至于我不得不时不时地跟上它的步伐。
我只需要一个解析器来执行解析给定 markdown 文件的一次性工作。将基于 GC 的语言引入到这种类型的问题中没有任何好处。
我还希望我的网站使用我确信在未来几十年内会继续工作的技术(我的最后一个要求)。截至今天,在这一点上,几乎没有任何东西可以击败 C 编译器。对于任何新的平台,首先需要发生的是构建一个 C 编译器以及标准库(这可能是流行编程语言中唯一适合 [500 页评论书](https://marcelofern.com/posts/c/why-is-this-site-built-with-c/<https:/www.google.com/search?q=C+standard+library+P.J+Plauger>) 的标准库……)。否则,任何东西都无法在该平台上运行。所以我希望这个赌注会有回报。