summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-02-13 22:30:34 +0800
committereltrac <hi@eltr.ac>2026-02-13 22:30:34 +0800
commitc583b189a27a82e69d738c31678cf459cc90b418 (patch)
tree39c7a0de18dc77ddcd5c59a48387faf7dbbc23d8 /content
parentf2dafd2c7c4ecb1e3dddb354240af90dc7e98b71 (diff)
docs: new post
Diffstat (limited to 'content')
-rw-r--r--content/posts/webpage-as-lists.md (renamed from content/posts/static-site-with-clojure.md)74
1 files changed, 47 insertions, 27 deletions
diff --git a/content/posts/static-site-with-clojure.md b/content/posts/webpage-as-lists.md
index 4a3a64c9..9143ea3a 100644
--- a/content/posts/static-site-with-clojure.md
+++ b/content/posts/webpage-as-lists.md
@@ -1,31 +1,27 @@
+++
-title = '如何优雅地从零构建一个静态网站'
-subtitle = ''
-slug = ''
-tags = []
+title = '网页即列表!'
+subtitle = '用 Clojure 构建静态网站'
+slug = 'webpage-as-lists'
+tags = ['编程','Web','Lisp']
categories = 'Coding'
-date = 2026-02-12T16:12:59+08:00
-draft = true
+date = 2026-02-13 22:30:17
+draft = false
toc = true
+++
-本文不是《[如何优雅地部署一个静态网站?](/essays/deploying-static-sites/)》的姊妹篇。<!--more-->
-
----
-
购入 `eltr.ac` 域名之后,我打算把「个人身份」和「作品」分开,也就是用 `eltr.ac` 域名代表我在互联网上的身份标识,用 `geedea.pro`(也就是这个博客的域名)代表「极客死亡计划」这个作品。是的,我并不打算把我的博客当作我网络身份的载体,更多地是视作独立的作品。目前我的联邦宇宙实例用的是 `eltr.ac` 域名,我的 Matrix(一个去中心化的即时通信协议)服务器也是用这个域名,电子邮箱也是这个域名,所以就有了:
- `@eltrac@eltr.ac`(我的联邦宇宙账号)
- `@eltrac:eltr.ac`(我的 Matrix 账号)
- `hi@eltr.ac`(我的电子邮箱地址)
-不过,要是有有心人尝试访问 [www.eltr.ac](https://www.eltr.ac/),只能看到一个很简陋的页面。既然要把这个域名用作身份标识,那在万维网层也要有一个足够有个性的页面吧,就算仅仅是当作导航也好。
+不过,要是有有心人尝试访问 `www.eltr.ac`,只能看到一个很简陋的页面。既然要把这个域名用作身份标识,那在万维网层也要有一个足够有个性的页面吧,就算仅仅是当作导航也好。<!--more-->
## 编程语言的选择
一开始我用 [Hugo](https://gohugo.io/) 做了一个简陋的网站,还参考 [slashpages.net](https://slashpages.net/) 添加了一些足够「个人化」的页面,但总觉得不满意。仔细想想,这竟然已经是我的第三个 Hugo 网站了!第一个是这个博客,第二个是这个博客的[后花园](https://jar.geedea.pro/)。尽管我很喜欢 Hugo,这个用 Go 语言开发的静态网站生成器也是全世界最快的生成器,但也不能用 Hugo 解决所有的网站构建需求吧?况且,我只需要几个页面,用非常擅长处理大量 Markdown 文件的工具来构建网站,属实有点像「用大炮打蚊子」。再者,我也对 Go HTML Template 感到厌烦了。
-这种简单的、不常修改的个人主页,要是用我熟悉的 Vue.js(或者…… React)来做,也有过度工程之嫌。貌似手写 HTML 就能胜任,不过要是真这么做,那我还不如直接加入 [Neocities](https://neocities.org/) 呢。之前被迫写了太多 Java 代码,也不可避免地接触到了很多 XML(Java 的项目管理工具 Maven,配置文件就是用 XML 写的;还有老旧的 MyBatis,编写数据表和 Java Bean 的隐射也要写 `mapper.xml`),继而对 HTML 也感到厌烦了。*自动补全固然好用,但难道就没有更优雅的解决方案了吗!*
+简单的、不常修改的静态页面,要是用我熟悉的 Vue.js(或者…… React)来做,也有过度工程之嫌。貌似手写 HTML 就能胜任,不过要是真这么做,那我还不如直接加入 [Neocities](https://neocities.org/) 呢。之前被迫写了太多 Java 代码,也不可避免地接触到了很多 XML(Java 的项目管理工具 Maven,配置文件就是用 XML 写的;还有老旧的 MyBatis,编写数据表和 Java Bean 的隐射也要写 `mapper.xml`),继而对 HTML 也感到厌烦了。*自动补全固然好用,但难道就没有更优雅的解决方案了吗!*
前端开发者应该都熟悉「DOM 树」的概念,HTML 所代表的就是一种树形结构,根节点是 `<html>` 标签,`<head>` 和 `<body>` 组成了第一级的子节点,然后继续往下生长。这样想就很简单了,现代编程语言里能够代表树的数据类型多了去了,只要用某种语言先编写好 DOM 树的结构,再转换成 HTML 字符串就好了。那么,能够表示 DOM 树的最简单的结构是什么呢?
@@ -50,10 +46,8 @@ const html = ["html",
["head", [ ... ]],
["body", [
["p",
- {
- "class": "paragraph font-sans",
- "id": "paragraph"
- },
+ [ "class", "paragraph font-sans",
+ "id", "paragraph" ],
"Hello World!"
],
// ...
@@ -61,11 +55,11 @@ const html = ["html",
]
```
-你等一下,你再这么写就是在抄袭别人的 Lisp 传教文了!
+*你等一下,你再继续写下去就是在抄袭别人的 Lisp 传教文了。*
是的,既然我们已经知道嵌套的列表实际上就是树的结构,那为什么不可以直接用列表来写 DOM 树呢?既然我们已经把编写 HTML 页面变成了对列表的处理,那四舍五入就得到了「列表处理」,List Processing,所以,答案就是 Lisp!
-*老实说,除了我本来就对这门编程界的拉丁语感兴趣之外,促使我学习一门 Lisp 方言的最大推动力就是我发现它竟然可以用来写 HTML,这简直是太天才了!*
+老实说,除了我本来就对这门编程界的拉丁语感兴趣之外,促使我学习一门 Lisp 方言的最大推动力就是:*用它来写网页的方式简直是太优雅,太天才了!* 这下谁还需要依赖 Tree-sitter 和编辑器插件自动闭合 HTML 标签?还有那可恶的 JSX,也可以说再见了。~~现在只需要闭合一大堆没完没了的括号就好了。~~
## 技术栈的选择
@@ -73,7 +67,7 @@ const html = ["html",
*安装 Java?!*
-没错,Clojure 是运行在 JVM(Java 虚拟机)上的,这下再讨厌 Java 都逃不掉了,不过也还好,不需要亲手碰肮脏的 Java 代码。Minecraft 玩家们的电脑里应该都有 Java,所以可以跳过这一步了。
+没错,Clojure 是运行在 JVM(Java 虚拟机)上的,这下再讨厌 Java 都逃不掉了,不过也还好,不需要亲手碰肮脏的 Java 代码,还能享受偷窃 Java 库来用的快感。Minecraft 玩家们的电脑里应该都有 Java,所以可以跳过这一步了。
最常用的 Clojure 开发工具是 [Leiningen](https://leiningen.org/),用来创建项目、管理依赖、运行和编译项目等等。要将 Clojure 的数据结构转换为 HTML,可以使用 [Hiccup](https://github.com/weavejester/hiccup) 这个库。既然我们都用 Lisp 写 HTML 了,那为什么不用 Lisp 写 CSS 呢?于是我很快发现了 [Garden](https://github.com/noprompt/garden) 这个库,用于把 Clojure 转译为 CSS。
@@ -102,7 +96,7 @@ nvim project.clj
## 手搓静态网站生成器
-静态网站生成器(Static Site Generator,SSG)这个东西可谓是遍地开花,[Cryogen](https://cryogenweb.org/) 就是用 Clojure 写的一个 SSG。不过,由于我的需求不是额外制作一个博客,不需要将 Markdown 文件构建为 HTML 的功能,我只需要能把用 Clojure 写的 HTML 结构编译成 HTML 字符串,然后写入一个文件即可。听起来不难,所以动手自己写吧!
+静态网站生成器(Static Site Generator,SSG)这个东西可谓是遍地开花,[Cryogen](https://cryogenweb.org/) 就是用 Clojure 写的一个 SSG。不过,由于我的需求不是额外制作一个博客,不需要将 Markdown 文件或 Org-Mode 文件构建为 HTML 的功能,我只需要把 Clojure 的数据结构编译成 HTML 字符串,然后写入一个文件即可。听起来不难,所以动手自己写吧!
用 Hiccup 编写 HTML 结构非常简单,以下是一个例子:
@@ -127,7 +121,7 @@ nvim project.clj
*等等,就没了?!*
-看起来是这样的,在 Clojure 里,你不需要创建 `Writer`,也不需要创建什么 `File` 对象,也不用额外引入一个 I/O 库,你只需要使用 `spit` 函数,把内容「吐」出来。这个函数接收两个参数,第一个参数是文件路径名,第二个参数是要写入的内容。所以,我们可以编写这样的 Clojure 程序:
+看起来是这样的,在 Clojure 里,你不需要创建 `Writer`,也不需要创建什么 `File` 对象,也不用额外引入一个 I/O 库[^2],你只需要使用 `spit` 函数,把内容「吐」出来。这个函数接收两个参数,第一个参数是文件路径名,第二个参数是要写入的内容。所以,我们可以编写这样的 Clojure 程序:
```clojure
(defn -main
@@ -137,7 +131,7 @@ nvim project.clj
[:p "Hello World"]))))
```
-接下来,运行 `lein run`,然后你就会得到一个内容为 `<p>Hello World</p>` 的 `index.html` 文件。*Wow!*[^1]
+接下来,运行 `lein run`,然后你就会得到一个内容为 `<p>Hello World</p>` 的 `index.html` 文件。[^1]
正式开发之前,我们大概是需要一个跑在本地的 Web 服务器的。如果是 JavaScript 技术栈,可以用 `npm run dev` 或者 `pnpm dev` 这样的命令。这样一个有些原始的项目应该怎么办呢?
@@ -186,9 +180,7 @@ open http://localhost:8080
```
-其中 `h/raw` 函数可以防止 Hiccup 自动将 `<` 和 `>` 转义,用来声明 `DOCTYPE` 是必要的。
-
-最后就得到了:
+其中 `h/raw` 函数可以防止 Hiccup 自动将 `<` 和 `>` 转义,用来声明 `DOCTYPE` 是必要的。最后就得到了:
```HTML
<!DOCTYPE html>
@@ -230,7 +222,7 @@ open http://localhost:8080
[:style style]])
```
-然后就可以愉快地用 Lisp 写 CSS 了!至于 JavaScript,我暂时还不打算添加。如果你需要,可以使用 [ClosureScript](https://clojurescript.org/),也就是用 Clojure 来写 JavaScript。这样一来就能用 Lisp 写遍 HTML、CSS 和 JavaScript,实在是惬意。
+然后就可以愉快地用 Lisp 写 CSS 了!至于 JavaScript,我暂时还不打算添加。如果你需要,可以使用 [ClojureScript](https://clojurescript.org/),也就是用 Clojure 来写 JavaScript。这样一来就能用 Clojure 写遍 HTML、CSS 和 JavaScript,实在是惬意。
## 生成多个文件
@@ -267,7 +259,7 @@ open http://localhost:8080
page_map)))
```
-然后就能得到这样的目录结构了。
+然后就能得到这样的目录结构。
```
public
@@ -276,4 +268,32 @@ public
└── index.html
```
+再讨论细节就有些繁琐了,总之,现在有一个能够生成静态网站的框架了,并且一共才写了不到 100 行代码。为了方便后续开发,我把上述功能拆成了几个模块,源码可以在[我的 Forgejo 实例](https://src.eltr.ac/eltrac/capricorn/src/branch/master/src/capricorn)上查看。
+
+## 最后
+
+Capricorn 的实现不是最优的,也不能称得上真正的静态网站生成器,但对于目前的问题规模(生成只有一两个页面的简单网站)而言,已经很好了,不需要再扩张,后续可能会加上自动生成的导航栏,用 ClojureScript 写一两个小组件获取 NeoDB 和 WakaTime 数据等等。要添加这些功能,在 Lisp 里就显得很容易,只需要用 `(defn)` 定义一个函数就好了。*这就是代码即数据的威力啊(并非)*。
+
+我学 PHP 时,写出来的第一个程序是[一个 Typecho 主题](https://github.com/BigCoke233/miracles);学 Lua 时,写出来的第一个程序是[一个简陋的肉鸽游戏](https://eltrac.itch.io/heart-hunter);用 Go 语言写的第一个程序是[一个简单的 CLI 工具](http://github.com/BigCoke233/wthis)。现在学了 Clojure,写的第一个程序竟然是生成固定内容的网页生成器。*这么看来,我果然是退步了吧!*
+
+学 Lisp 的确有很奇妙的感受。《代码简洁之道》这本书里,作者认为函数里最好只有两三行代码,并且所有代码都要在一个抽象层次上,这需要克制、熟练和清晰的头脑。神奇的是,在 Lisp 里,这是非常自然的,因为一旦我写出了不太优雅的代码,代码的结构也会随之变得扭曲起来,比如这样:
+
+```clojure
+(def pages ["index" "contact"])
+(def nav
+ [:nav.nav [:ul
+ (for [p pages] [:li
+ [:a {:href (str "/"
+ (if (= p "index") "" p))} p]])]] )
+```
+
+在 Lisp 里面写出太长或者耦合度太高的函数,代码的结构就会变得很奇怪,带来清晰的视觉反馈;写出简单小巧的函数才是自然的。虽然不知道这是不是所有 Lisp 程序员的感受,但 Lisp 的确让我开始思考程序结构的美感。
+
+*此外,如果你是个资深的 Lisp 程序员,读到我在文章里写出的 Lisp 代码,还请不要气昏过去。*
+
+先这样吧。各位,我要去研究 [Paredit](https://paredit.org/) 了。
+
[^1]: 第一次觉得 `<p>` 标签特别可爱,像一张吐舌头的脸:`:p`。还有 `<a>` 标签:`:a` XD
+
+[^2]: 如果要创建目录(`mkdir`)的话,还是要引入 Java 的 I/O 库的,`spit` 只提供了创建和写入文件的能力,不能处理目录。如果要读文件,还有一个对应的 `slurp`(吸溜)——*真的是…… 好简洁明了的命名方式*。
+