Ciallo~(∠・ω< )⌒☆

基于 Caddy 蓝绿部署实现异构博客的随机主题架构

泰裤啦!现在我的博客花里胡哨的。

我一直苦于找不到一种优雅的方案来实现 hexo & hugo & astro 的多主题配置计划。

(无须修改主题、低配置文件、节省资源)

现在我想到了一种基于容器挂载卷 + Caddy 负载均衡实现的轻量级多主题配置方案。

前置知识

碎碎念

在之前,我通过:

  1. 用多个 docker 容器实例分别部署主站 + 从站的方式
  2. 通过修改主题代码实现各种的跳转到外部
  3. 用主站引导 + 网关路由

的方式实现个人网站的部署,但是我注意到一个问题。

每次我更新文章的时候,需要往指定从站上传,再在主站发一篇文章用来引导。

好吧,我承认这真的很蠢。那个时候我的设计重心在于如何将 wordpress、hugo、hexo 这种异构的站点部署在一起。

借着这次抛弃 wordpress 站点的机会,我又开始折腾我的博客了。

最初,我找到了一个新的 wp 主题 puork,我觉得这简直是 wp 上最优雅的主题。

久而久之我的热情就褪去了,wp 逃不了笨重的问题,而我写博客纯粹是给自己看的,不存在所谓的社交互动。

而且 puork 作为 wp 主题虽然已经足够简化了,但是它的配置界面谈不上优雅。

各种红点、推广和莫名其妙的专业配置让我看的糟心,我终于注意到了它臃肿的一面。

bye ~ Wordpress,是时候离开我的博客架构了。

我从今年开始使用 hugo,hugo 真的很快,而且我找到了我崭新的白马主题 hugo-theme-stack

哪个程序员能拒绝这么快的博客架构呢 ?

可行性猜想

在抛弃了 wordpress 以后,hugo 和 hexo 的博客都是基于 md 语法生成的,这意味着网站内容格式实现了统一。

hugo 和 hexo 的主题作者,都将 post 路径作为渲染目录。

所以我们完全可以将一个纯粹的 markdown 库挂载为存储卷。

对于不同的主题格式,他们通常都是通过 front-matter 实现的参数注入,我只需要在每次写文章的时候。

把主题需要的参数一股脑的录入即可,即使遇到兼容问题,我也可以以主站为准。

我有一些担心,hugo 和 hexo 的目录管理结构可能会导致一些问题。

hugo-theme-stack 使用 page bundles 做内容管理,直接将 title.md 直接丢进 /content/_post 是否可行呢 ?

答案是可行的

我将 page bundles 形式直接复制到 hexo,也是可行的。

https://www.nihilx.com/post/email-architecture-analysis/

https://www.nihilx.com/email-architecture-analysis/index/

对比一下两者的 url,我心中有了一个大胆的想法:

同一篇文章(同一个 .md 文件)是否可能在网关实现用户无感的随机渲染(用不同的主题实例)呢 ?

这也是这片文章诞生的根本原因。

使用 permalinks 配置实现内容 url 对齐 / 标准化

前缀都不是问题,因为 hexo 和 hugo 都提供了 permalinks 这一配置项。

https://hexo.io/zh-cn/docs/permalinks

https://gohugo.io/configuration/permalinks/

但是 page bundles 末尾的 /index/ 怎么解决呢 ?

我想要保留 page bundles 的收纳特性。

我相信 caddy 一定可以做到,我寄希望于 Caddyfile 并开始着手实际测试,并尝试解决问题 1。

基于 caddy + cookies 在网关实现状态约定

这一过程还是很有收获的,经过我 3 个小时的调试、测试和学习。

恶补了 Caddy 的各种 matcher / directives / syntax …

实现了基于 Caddy 网关会话的简单状态机,具体思路如下:

在入口处,基于 uuid 为每一个请求分配状态,在 cookie 实现会话级的持久化。

在尝试初期,我希望 hugo 和 hexo 的 url 异构能够更好的解决,我的想法是根据状态来重写 url。

令人抓狂的是,在 hugo 的 page bundles 特性下,hugo 多出的 /post/ 是头,而 hexo 多出的 /index/ 是尾巴。

而且,多个主题下还需要考虑静态资源混乱的问题,起初我甚至没有发现这个 bug。

当时我希望随机主题的交互覆盖能够最大化:在主题 1 访问文章,网关随机代理到其他主题。

(最初我没有引入网关状态)

直到我逐渐发现不对,我意识到了一些问题,并且打开了 cloudflare 的开发模式,让所有请求都走源站了。

我释怀了,原来是 CDN 在帮我们做缓存,这也让我选择用 cookie 做一个状态。

OK,我后来想起来可以在网关做缓存控制 Cache-Control: no-cache

经过漫长的 debug ,我发现交互覆盖最大化的想法是错误的,这实现起来一点儿也不优雅。

我退而求其次,决定在网关设计一个基于 cookie 的状态:

这样一来,网关只需要负责判断状态,解决了静态资源混乱的问题。

客户端请求时,html 总是最先的,因此在请求 html 的时候,我们就决定好未来一段时间的服务端状态。

处理 url 尾部 /index/ 解决 page bundles 格式兼容

让 hexo 兼容 hugo 的 page bundles 格式,只需要解决 hexo 实例的 /index/ 尾巴。

 1@is_kratos vars {final_theme_type} kratos
 2reverse_proxy @is_kratos {
 3  import kratos_service
 4  @not_found status 404
 5  handle_response @not_found {
 6    @loop_check {
 7      path /old-archives/posts/*
 8      path */
 9      not path */index/
10    }
11    header Location {scheme}://{host}{path}index/
12    respond @loop_check 301
13  }
14}
15
16@is_stack vars {final_theme_type} stack
17reverse_proxy @is_stack {
18  import stack_service
19  @not_found status 404
20  handle_response @not_found {
21    @loop_check {
22      path /old-archives/posts/*/index/
23      path_regexp @title /old-archives/posts/(.*)/index/
24    }
25    header Location {scheme}://{host}/old-archives/posts/{re.title.1}index/
26    respond @loop_check 301
27  }
28}

虽然实现了,但是会有问题,如果请求的路径不是一个合法的文章路径,这段逻辑就有冗余。

而且就在这时,我又注意到一些问题,不仅仅是 page bundles,文章自身携带的素材该怎么解决。

在我原本的设想中,每次请求文章意味着客户端生命周期的开始,在这一阶段网关根据状态值来判断渲染的主题。

因此,我需要通过 permalink 来让文章页具备 html 特征,已实现网关的判断。

1@is_html {
2  path /old-archives/posts/*
3  path *.html
4}
5header @is_html +Set-Cookie "theme={final_theme_type};"

考虑到防止在搜索界面、分类界面等非文章界面跳转,我直接将 /post/ 作为区分用的入口路径。

模板之间使用不同的 Tag plugins 会让 hexo 报错

我查询了相关资料,发现不同版本的 hexo,不同主题自带的 tag plugins 如果 hexo 检测不到会导致渲染停止。

即 { % something % },这类语法。许多主题都有自己的 tag plugins 语法。

marked:
  disableNunjucks: true

禁用 hexo 自己的 Nunjucks 渲染即可,大多数主题都会自行渲染,实在不行只能取消了。

设计失败:放弃实现异构级别的文章级随机主题

我休息了一段时间,吃了顿饭。

我大概花了一整天在这个无聊的事情上,我从下午 4 点,直到现在,现在是 23:58 。

10 个小时,我意识到这个伟大的念头大概是无法在对主题源码无侵入下实现的。

PS:我甚至在后来又花了一个晚上的时间,来尝试这个实现,现在我浪费了 18 个小时了。

我终于意识到,网关本身设计是无状态的,我正在作困兽之斗。

我虽然通过 cookie 实现了网关与客户端的一个弱状态:

即每次请求文章 dom 的时候,更新状态

但是我在上文中,写了一个机制,来解决 hugo 和 hexo 在 permalink 尾部异构的问题。

也就是说,当这个机制生效的时候,网关会去额外请求一次文章。

那么如果我把更新状态设置在请求文章,直接会导致两次请求后状态错位。

也就是说,我不能将文章地址,作为状态更新的触发帧。

(假设一个页面有 30 个资源,即客户端会请求 30 次,每 1 次为 1 帧,第 1 帧必然是 dom)

稍微分析一下就可以知道,由于这个不确定的机制,可能会浪费整个客户端状态周期的 1 帧。

(可能会浪费掉客户端的第 2 帧,但是触发了机制,服务端打开了新的状态周期,客户端不知道服务端更新了一个新的周期,即错位)

如果不加逻辑判断或者是计数器之类的 loop-check ,这一设计永远也不可能实现。

这样的设计显然是冗余的、不优雅的、缺陷的。

所以我退求其次,实现了会话级别的异构随机主题。

会话级别的随机主题

放弃了文章级别的状态周期以后,由于不需要再考虑异构问题,非常简单。

最终实现如下:

 1fluid_service) {
 2	to localhost:32301
 3}
 4(icarus_service) {
 5	to localhost:34001
 6}
 7(kratos_service) {
 8	to localhost:34002
 9}
10
11www.nihilx.com {
12	header * X-Robots-Tag "noindex, nofollow"
13
14	reverse_proxy :31313
15
16	map {cookie.theme} {theme_type} {
17		fluid fluid
18		kratos kratos
19		icarus icarus
20		default {uuid}
21	}
22
23	map {theme_type} {final_theme_type} {
24		fluid fluid
25		kratos kratos
26		icarus icarus
27		~^[0-4] fluid
28		~^[5-9] kratos
29		~^[a-f] icarus
30		default fluid
31	}
32
33	@no_theme {
34		not header_regexp Cookie "theme=(fluid|kratos|icarus)"
35	}
36	
37	header @no_theme +Set-Cookie "theme={final_theme_type}; Path=/;"
38	
39	handle /old-archives/set-random-theme {
40		map {uuid} {new_theme_type} {
41			~^[0-4] fluid
42			~^[5-9] kratos
43			~^[a-f] icarus
44			default fluid
45		}
46		header +Set-Cookie "theme={new_theme_type}; Path=/;" defer
47		redir {scheme}://{host}/old-archives/
48	}
49
50	handle /old-archives/debug {
51		respond "Raw:{cookie.theme} | Type:{theme_type} | Final:{final_theme_type} | {uuid}"
52	}
53	
54	handle /old-archives/go-home/ {
55		redir {scheme}://{host}
56	}
57
58	handle /old-archives/* {
59		@is_root path /old-archives/
60		header @is_root Cache-Control "no-cache"
61		
62		@is_fluid vars {final_theme_type} fluid
63		reverse_proxy @is_fluid {
64			import fluid_service
65		}
66
67		@is_kratos vars {final_theme_type} kratos
68		reverse_proxy @is_kratos {
69			import kratos_service
70		}
71		
72		@is_icarus vars {final_theme_type} icarus
73		reverse_proxy @is_icarus {
74			import icarus_service
75		}
76	}
77}

docker run

一些临时笔记

 1docker run -d --name hugo_i2 \
 2-v hugo_v2:/app \
 3-v markdown_note:/app/content/post/ \
 4-p 34002:443 \
 5-e baseURL=https://www.nihilx.com \
 6-e port=443 \
 7hugo-docker:local
 8
 9# volume hexo_v2 should be complete new (not exists)
10docker run -d --name hexo_i2 \
11-v hexo_v2:/app \
12-v markdown_note:/app/source/_posts/ \
13-p 34001:4000 \
14hexo-docker:local
15
16docker run -d --name hexo_i3 \
17-v hexo_v3:/app \
18-v markdown_note:/app/source/_posts/ \
19-p 34002:4000 \
20hexo-docker:local
21
22docker run -d --name hugo_i2 \
23-v hugo_v2:/app \
24-v markdown_note:/app/content/post/ \
25-p 34003:443 \
26-e baseURL=https://www.nihilx.com/old-archives/ \
27-e port=443 \
28hugo-docker:local
29
30npm install --save hexo@^7.1.1 hexo-util@^3.2.0 inferno@^8.2.3 inferno-create-element@^8.2.3 bulma-stylus@0.8.0 deepmerge@^4.3.1 hexo-component-inferno@^3.1.2 moment@^2.30.1 semver@^7.5.4 hexo-renderer-inferno@^1.0.2

#解决方案 #蓝绿部署 #异构 #网关