summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/posts/static-site-with-clojure.md279
-rw-r--r--content/posts/weekly/68.md13
2 files changed, 292 insertions, 0 deletions
diff --git a/content/posts/static-site-with-clojure.md b/content/posts/static-site-with-clojure.md
new file mode 100644
index 00000000..4a3a64c9
--- /dev/null
+++ b/content/posts/static-site-with-clojure.md
@@ -0,0 +1,279 @@
++++
+title = '如何优雅地从零构建一个静态网站'
+subtitle = ''
+slug = ''
+tags = []
+categories = 'Coding'
+date = 2026-02-12T16:12:59+08:00
+draft = true
+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/),只能看到一个很简陋的页面。既然要把这个域名用作身份标识,那在万维网层也要有一个足够有个性的页面吧,就算仅仅是当作导航也好。
+
+## 编程语言的选择
+
+一开始我用 [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 也感到厌烦了。*自动补全固然好用,但难道就没有更优雅的解决方案了吗!*
+
+前端开发者应该都熟悉「DOM 树」的概念,HTML 所代表的就是一种树形结构,根节点是 `<html>` 标签,`<head>` 和 `<body>` 组成了第一级的子节点,然后继续往下生长。这样想就很简单了,现代编程语言里能够代表树的数据类型多了去了,只要用某种语言先编写好 DOM 树的结构,再转换成 HTML 字符串就好了。那么,能够表示 DOM 树的最简单的结构是什么呢?
+
+广义表就是个例子,这是数据结构里的概念,更接地气的名字是数组、元组和列表。比如,用 JavaScript 就可以这样写:
+
+```JavaScript
+const html = [
+ { name: "head", innerHTML: [
+ { name: "meta", attribute: {
+ rel: "stylesheet",
+ href: "/main.css"
+ } }
+ ]},
+ { name: "body", innerHTML: [ ... ] }
+]
+```
+
+停停停,用 JavaScript 数组和对象写 HTML,还不如直接手写呢。有没有办法全部简化成数组(列表),不使用对象呢?比如这样:
+
+```JavaScript
+const html = ["html",
+ ["head", [ ... ]],
+ ["body", [
+ ["p",
+ {
+ "class": "paragraph font-sans",
+ "id": "paragraph"
+ },
+ "Hello World!"
+ ],
+ // ...
+ ]]
+]
+```
+
+你等一下,你再这么写就是在抄袭别人的 Lisp 传教文了!
+
+是的,既然我们已经知道嵌套的列表实际上就是树的结构,那为什么不可以直接用列表来写 DOM 树呢?既然我们已经把编写 HTML 页面变成了对列表的处理,那四舍五入就得到了「列表处理」,List Processing,所以,答案就是 Lisp!
+
+*老实说,除了我本来就对这门编程界的拉丁语感兴趣之外,促使我学习一门 Lisp 方言的最大推动力就是我发现它竟然可以用来写 HTML,这简直是太天才了!*
+
+## 技术栈的选择
+
+我选择的 Lisp 方言是 [Clojure](https://clojure.org/),至于为什么选它,原因是…… 呃,[矩阵](https://matrix.org/)上有一只[狐狸](https://blog.southfox.me/)诱拐我进了这个兔子洞。无论如何,我就先从 Clojure 开始用着吧。要使用 Clojure 编写程序,第一步是——
+
+*安装 Java?!*
+
+没错,Clojure 是运行在 JVM(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。
+
+现在准备就绪了,开始创建项目吧!
+
+顺带一提,我准备把这个新网站命名为 `capricorn`(摩羯座),至于原因,仅仅是因为它是十二星座里首字母和 Clojure 相同的一个。
+
+```shell
+lein new app capricorn
+cd capricorn
+nvim project.clj
+```
+
+编辑 `project.clj`,把需要的依赖添加进去,也就是刚才提到的 Hiccup 和 Garden。
+
+```clojure
+:dependencies [
+ [org.clojure/clojure "1.12.2"]
+ [hiccup "2.0.0"]
+ [garden "1.3.10"]]
+```
+
+哦对了,如果你还不了解 Lisp 的语法,让我花点时间来给你解释一下:列表的第一个元素是函数名,剩下的都是参数,比如 `(println "Hello World")` 就是打印 `Hello World` 字符串。在 Clojure 里,列表有多种类型,除了最基本的作为程序结构的 `()`,Clojure 还用其他的括号表示不同的数据容器:`'()` 是列表,`[]` 是向量,`{}` 是映射(也就是键值对),`#{}` 是集合。Clojure 里还有一种特殊的数据类型叫作「关键字」,写成这样 `:keyword`,我喜欢把它理解为不需要提前定义的枚举类型。
+
+好了,现在你可以理解 Clojure 程序了,我们继续吧。
+
+## 手搓静态网站生成器
+
+静态网站生成器(Static Site Generator,SSG)这个东西可谓是遍地开花,[Cryogen](https://cryogenweb.org/) 就是用 Clojure 写的一个 SSG。不过,由于我的需求不是额外制作一个博客,不需要将 Markdown 文件构建为 HTML 的功能,我只需要能把用 Clojure 写的 HTML 结构编译成 HTML 字符串,然后写入一个文件即可。听起来不难,所以动手自己写吧!
+
+用 Hiccup 编写 HTML 结构非常简单,以下是一个例子:
+
+```clojure
+(require '[hiccup2.core :as h])
+(str (h/html [:span {:class "foo"} "bar"]))
+```
+
+看起来有点难懂,没事,拆开来就好了:
+
+1. `require` 显然是引入,我们这里把 Hiccup 的核心库以 `h` 这个名字引入了。
+2. `str` 是 Clojure 标准库里的函数,顾名思义,就是用来把其他数据类型转换为字符串的。
+3. `h/html` 中的 `h` 是我们刚才引入的 Hiccup 核心库,`html` 是这个库提供的函数,用于把 Clojure 的数据结构转换为 HTML 表示;这里接收一个向量类型的数据,也就是 `[]` 包裹的列表。
+4. `[:span ...]` 显然是 `<span>` 标签的表示,这个向量由三部分组成:
+ 1. `:span` 是我们刚才提到的关键词类型,这里作为 HTML 标签名。
+ 2. `{:class "foo"}` 是一个映射,`:class` 关键词是键(key),`"foo"` 字面量是值(value),这个映射会被解析为 `class="foo"`;我们还可以继续写更多的属性,比如 `{:class "foo" :id "bar" :width 200}`,奇数个元素是键,紧跟其后的偶数个元素就是对应的值。
+ 3. `"bar"` 是这个 `<span>` 标签的内容,也可以替换成向量,也就是继续嵌套 HTML 结构。
+
+总而言之,这段 Clojure 代码构建了形如 `<span class="foo">bar</span>` 的 HTML 结构,并将它转换为字符串输出。Hiccup 还提供了非常好用的语法糖,如果要编写 `<span class="foo" id="bar" />`,可以写成 `[:span.foo#bar]`。 尽管看起来有点奇怪,但你很快就会意识到这样写 HTML 有多爽了。
+
+我们先来解决一个关键的问题,静态网站生成器除了把某种形式的数据转换为 HTML 字符串,还应该把得到的结果写入 `.html` 文件。接下来我们要研究一下如何用 Clojure 进行 I/O 操作,文档里写的是使用 `spit` 函数将内容写入文件,然后……
+
+*等等,就没了?!*
+
+看起来是这样的,在 Clojure 里,你不需要创建 `Writer`,也不需要创建什么 `File` 对象,也不用额外引入一个 I/O 库,你只需要使用 `spit` 函数,把内容「吐」出来。这个函数接收两个参数,第一个参数是文件路径名,第二个参数是要写入的内容。所以,我们可以编写这样的 Clojure 程序:
+
+```clojure
+(defn -main
+ [& args]
+ (spit "public/index.html"
+ (str (h/html
+ [:p "Hello World"]))))
+```
+
+接下来,运行 `lein run`,然后你就会得到一个内容为 `<p>Hello World</p>` 的 `index.html` 文件。*Wow!*[^1]
+
+正式开发之前,我们大概是需要一个跑在本地的 Web 服务器的。如果是 JavaScript 技术栈,可以用 `npm run dev` 或者 `pnpm dev` 这样的命令。这样一个有些原始的项目应该怎么办呢?
+
+```shell
+cd public
+python -m http.server 8080
+open http://localhost:8080
+```
+
+*There you go.*
+
+如果不介意没有热重载,每次更改之后需要手动运行程序然后手动刷新浏览器的话,就已经可以上路了。
+
+## 模板!组件!
+
+我们先把头拆出来吧。
+
+```clojure
+(def head
+ [:head
+ [:title "Eltrac"]
+ [:meta {:charset "utf-8"}]
+ [:meta {:name "viewport" :content "width=device-width,initial-scale=1"}]])
+```
+
+`def` 是 Clojure 里定义符号(变量)的方式;如果要定义函数,就用 `defn`。由于我这个小网站只有几个页面,不在乎标题的话,`<head>` 可以直接复用,所以就不需要定义成函数了。
+
+再把刚才的 `Hello World` 放到 `<body>` 里。由于不同页面的内容肯定是不一样的,所以把 `body` 定义为函数比较合适。
+
+```clojure
+(defn body [content]
+ [:body
+ [:main content]])
+```
+
+然后拼接在一起,放在 `<html>` 里,最后再转成字符串写入文件。
+
+```clojure
+(defn -main
+ [& args]
+ (spit "public/index.html"
+ (str (h/html
+ (h/raw "<!DOCTYPE html>")
+ [:html head (body
+ [:p "Hello World!"])]))))
+
+```
+
+其中 `h/raw` 函数可以防止 Hiccup 自动将 `<` 和 `>` 转义,用来声明 `DOCTYPE` 是必要的。
+
+最后就得到了:
+
+```HTML
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Eltrac</title>
+ <meta charset="utf-8" />
+ <meta content="width=device-width,initial-scale=1" name="viewport" />
+ </head>
+ <body>
+ <main>
+ <p>Hello World!</p>
+ </main>
+ </body>
+</html>
+```
+
+显然,完全不需要什么 `defineProps()`,也不用额外学习怎么在父子组件之间传递数据。在 Clojure 和 Hiccup 的世界里,页面组件就是函数。怎么给函数传参数,就怎么给组件传参数。我们直接用 Clojure 处理向量,最后再把向量们汇总在一起,用 Hiccup 处理为 HTML,再转成字符串输出到文件里。这样一来,我们就能用 Lisp 编写 HTML 页面了。
+
+## Build website with Style
+
+接下来要给网站添加 CSS,使用之前添加的 Garden 库。这个库的语法和 Hiccup 非常相似,最关键的函数是 `css` 函数,和前面的 `html` 函数一样,它接收向量作为参数,输出的是 CSS 字符串。
+
+我一直是行内 CSS 的信奉者,少一个 `.css` 文件能减少服务器需要传送的文件数量,并且不会被缓存,用户能够在网站更新后立刻看到改动。只要 CSS 文件不是太大,我都建议使用行内 CSS,也就是 `<style>` 标签。
+
+那给这个 Clojure 静态网站添加样式就非常简单了,只需要在 `head` 变量里加一个 `<style>` 标签,为了方便,我们再定义一个 `style` 变量。
+
+```clojure
+(require '[garden.core :as g])
+
+(def style (g/css
+ [:body {:margin 0}]) )
+
+(def head
+ [:head
+ [:title "Eltrac"]
+ [:meta {:charset "utf-8"}]
+ [:meta {:name "viewport" :content "width=device-width,initial-scale=1"}]
+ [:style style]])
+```
+
+然后就可以愉快地用 Lisp 写 CSS 了!至于 JavaScript,我暂时还不打算添加。如果你需要,可以使用 [ClosureScript](https://clojurescript.org/),也就是用 Clojure 来写 JavaScript。这样一来就能用 Lisp 写遍 HTML、CSS 和 JavaScript,实在是惬意。
+
+## 生成多个文件
+
+刚刚我们把所有的 HTML 汇总到了 `main` 函数里,生成了一个 `index.html`,一个网站显然不能只有一个页面。我们需要这样的结构:
+
+```clojure
+(def page_index (base [:p "Hey!"]) )
+(def page_contact (base [:p "Contact me!"]))
+
+(defn -main [& args]
+ (site {:index page_index
+ :contact page_contact}))
+```
+
+简单来说,我们在主函数里调用名为 `site` 的函数,传入一个映射,映射的键是页面名字,值是对应的 HTML 结构。定义 HTML 结构时还用到了自定义的一个 `base` 函数,理解成模板就好了。
+
+`site` 是怎么实现的呢?
+
+```clojure
+(defn pathname [pname]
+ (if (= pname "index") "public/index.html"
+ (str "public/" pname "/index.html")))
+
+(defn page [pname, html]
+ (.mkdir (io/file "public"))
+ (when-not (= pname "index")
+ (.mkdirs (io/file (str "public/" pname))))
+ (spit (pathname pname) (str html)))
+
+(defn site [page_map]
+ (doall
+ (map (fn [[pname html]]
+ (page (name pname) html))
+ page_map)))
+```
+
+然后就能得到这样的目录结构了。
+
+```
+public
+├── contact
+│   └── index.html
+└── index.html
+```
+
+[^1]: 第一次觉得 `<p>` 标签特别可爱,像一张吐舌头的脸:`:p`。还有 `<a>` 标签:`:a` XD
diff --git a/content/posts/weekly/68.md b/content/posts/weekly/68.md
index f600fe60..2b460cae 100644
--- a/content/posts/weekly/68.md
+++ b/content/posts/weekly/68.md
@@ -124,6 +124,19 @@ Well, that's pretty fucked up.
在面对这样一个教育问题时,最应该关注的不是对儿童的法律约束,而是成年人的意识问题:成年人愿不愿意承担起教育子女应有的责任?又及,我想要扩展一下:在不结婚生子就是异类的社会中,人们真的在意教育问题吗?
+### Foo 的词源
+
+> [!note] 📃
+> [RFC 3092 - Etymology of "Foo"](https://datatracker.ietf.org/doc/html/rfc3092)
+
+看到 RFC(Request For Comment)就会以为这是什么严肃的互联网技术标准,就算是研究词源,那应该也是偏严肃的吧。我在 Hacker News 上看到这篇文章时就是这么想的,直到我在里面读到了「Foomobile」这个词。
+
+*抬头,看见日期:1 April 2001*
+
+好,好,好。在 2 月份把一篇二十多年前的愚人节文章发到 Hacker News 上的人到底是谁!不过,虽然是一篇幽默的文章,但内容还是有考据的,不是胡编乱造,串联起了各种有关 Foo 词源的说法,以及一些漫画里对 Foo 这个词的使用,奇妙的是,竟然还有说法是 Foo 实际上是中国汉字「福」。
+
+如果你不知道 Foo 是什么:这个词常常出现在各种编程教程或文档的示例代码中,用作没有实意的符号名,比如 [ClojureDocs](https://clojuredocs.org/clojure.core/defn) 里就有着这样的例子 `(defn foo [a b c] (* a b c))`,使用 Foo 来演示如何在 Clojure 中定义函数。
+
## 星群
### RustCast