2022-09-06

网站更新 4.0

暑假开始立的 flag,完成!

虽然上个版本的网站是基于经过自己大量修改的 Material for MkDocs 主题生成的,但因为每次拉取新的代码都要手动解决大量冲突,并非长久之计,于是放弃了此方案,考虑把网站重构,全部手写,没有使用任何前端框架,因此就有了现在的版本。

网站更新后,原先的应用例如 LaTeX 计算器、自动打卡等仍可以正常使用

这篇文章将记录新网站的设计、架构和部署的大致思路,因此又名为《2022 年,如何从 0 开始手动做 1 个网站(指引)》。

1. UI 设计

新版网站的设计理念是 Keep it simple. 在保证 UI 美观的前提下,每个页面都只提供最小化的功能,多余的功能全部移除。

1.1 主题颜色

颜色是很好的表达情绪和性格的工具。新版网站的主题颜色和之前并没有变化,仍然是最喜欢的稍微有一点点紫色的蓝色、216 度色相、低饱和的hsl(216, 55%, 58%),简单、冷静、自由、略微神秘。

不过,深色模式的主题色有一些微调。因为在设计之前,遇见了这个很棒的详细解释颜色理论的网站,并通过对颜色理论的分析提供 UI 设计上的建议。其中有一节是 Abney Effect,即相同色相的颜色在不同的亮度下,色相看起来会发生偏移。应对这一现象的方法就是在创建不同亮度的色阶时刻意改变一点点色相。

说到创建色阶,使用了一个叫做 Palettte 的好工具。新版网站的主题色阶如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
:root {
    --b0: hsl(211, 100%, 98%);
    --b1: hsl(212, 91%, 90%);
    --b2: hsl(213, 82%, 82%);
    --b3: hsl(214, 73%, 74%);
    --b4: hsl(215, 64%, 66%);
    --b5: hsl(216, 55%, 58%);
    --b6: hsl(217, 46%, 50%);
    --b7: hsl(218, 37%, 42%);
    --b8: hsl(219, 28%, 34%);
}

其中--b5是浅色模式的主题色,--b7是深色模式的主题色,其他颜色用来作为辅助色,用于链接、:hover效果、文本高亮等。此外,为了在浅色和深色模式下着色前景文字和背景,也创建了一个灰度色阶。可以在main.css:root组中查看。

1.2 字体和图标

由于上个版本的网站需要加载 Google Webfont,在国内有一定概率会间歇性抽风,因此新版网站弃用了 Webfont,而使用系统字体。

对于 macOS / iOS 系统,优先使用最好看的苹果系统 UI 字体,对于 Safari 需要设为-apple-system,对于 Chrome 需要设为BlinkMacSystemFont。值得注意的是,如果直接手动指定苹方字体 PingFang SC,则英文字母和数字也会用该字体,而系统默认的英文和数字字体是 SF Pro Text,因此反而不太好看。

对于其他系统,先尝试较好看的 Helvetica 英文字体是否被安装,否则使用 Arial 英文字体,最后中文使用默认的无衬线体(Windows 默认是微软雅黑,Android 默认是 Noto Sans CJK SC)。

1
2
3
4
5
6
7
:root {
    font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
}

:is(pre, code) {
    font-family: ui-monospace, "Roboto Mono", Consolas, "Courier New", DengXian, monospace;
}

对于代码字体,也是同样的思路,首先尝试苹果的 UI 等宽字体,之后不断 fallback,直到最后使用系统默认的等宽字体。这里前面加了一个等线,是因为代码中出现中文时,Windows 会默认用难看的宋体渲染。

对于图标,右上角的三个图标来自阿里矢量图标库,其余图标来自 FontAwesome。在选择时要注意每个图标的线条宽度、空白宽度应接近,以保证视觉上的一致性。

此外,对于中英文混排时的情况,之前遇到了一个有意思的為什麼你們就是不能加個空格呢项目,其中说到:

打字的時候不喜歡在中文和英文之間加空格的人,感情路都走得很辛苦,有七成的比例會在 34 歲的時候跟自己不愛的人結婚,而其餘三成的人最後只能把遺產留給自己的貓。畢竟愛情跟書寫都需要適時地留白

但是因为不想用任何前端框架,因此特别检查了一遍之前的文章,手动在中文和英文字符之间加了空格。其实也不是手动,可以通过下面的正则表达式快速发现大部分需要加空白的地方:

1
2
(\w|\.|\$|$|-)([\u4e00-\u9fa5])
([\u4e00-\u9fa5])(\w|\.|\$|^|-)

\w和后面那一串匹配的是英文字母,以及不影响 Markdown 排版的英文字符,[\u4e00-\u9fa5]匹配中文汉字,但不匹配中文标点,因为英文字符和中文标点之间不需要加空格。替换时只要输入$1 $2即可。

1.3 页面布局

首先是导航栏。原先的导航栏中,搜索、语言切换和颜色模式切换占据了菜单的大部分空间,剩下的栏目链接大部分时间都是隐藏的,在移动端上用户甚至要点击左上角才能弹出栏目链接,导致移动端用户往往不知道怎么访问其他页面。因此,新版网站删除了搜索、颜色主题切换、语言切换的功能,从而将导航栏目减少到 3 个,可以始终显示在页面右上角,并且配有直观的图标。对于移动端,由于宽度不足而仅显示图标时,也不影响用户理解图标含义并点击图标跳转。

桌面端导航栏

桌面端导航栏

移动端导航栏

移动端导航栏,隐藏文字,仅显示图标,并将图标略微放大

删除搜索、颜色主题切换和语言切换功能是很合理的,体现了 Keep it simple 的设计理念。首先,搜索功能原本的设计是面向程序文档的,几乎没有人会在个人网站内进行搜索,即使有偶然的搜索需求,也可以通过搜索引擎的site:功能实现,因此这个搜索自然要被砍掉。对于颜色主题切换,跟随系统的颜色主题即可,无需用户手动设置,也很少有用户使用浅色系统主题但想要浏览深色主题的页面或反之,所以也被砍掉。对于语言切换,因为实践下来并没有那么多精力翻译中文页面,英文页面就只有一两个,因此也砍掉,不过后续可能会像颜色主题切换一样以无感知的方式加入。

对于文章内容页面,由于 Material for Mkdocs 的布局效果已经很棒了,因此基本没有修改,仿照其样式手动编写了 CSS,但是删除代码块复制的功能,因为展示的代码除了 shell 命令外,其他代码大部分都很长,都不会是给人复制粘贴用的。并且 shell 命令往往也很短,手动复制并不需要费多大力气。

对于文章侧边栏和网站底部,删除了侧边栏的导航和底部“上一篇”和“下一篇”的导航,这是因为首先页面右上角已经有跳转到三个栏目的图标链接了,并且由于本站的文章仅按照日期排序,上下文并不存在逻辑上的关联性,因此跳转到上一篇或下一篇自然没什么道理,删去之后还能让用户专注在被分享的文章内容本身。

文章内容页面

文章内容页面,删去了左侧导航、底部导航,以及代码块复制的功能

对于三个栏目(笔记、想法、相册)的主页,重新进行了设计。因为原先的 Material for Mkdocs 并没有专门展示文章列表的页面,只能依靠侧边栏导航实现,现在侧边栏导航被删去了,就可以用更大的空间专注的展示文章 / 相册列表了。文章列表在桌面端分两栏展示,在移动端合并成一栏展示,相册列表则根据网页宽度不同依次分为 3、2、1 栏展示,且可直接显示一张主题图片,隐藏了标题,当鼠标划过或被触摸滑动时才会加深背景图片,展示标题。

相册页面

对于网站主页也进行了修改。删去了原来耗费大量 CSS 的粒子动画,改为设计了一个简单的 webshell,与网站图标对应,并将原先的“关于”页面删除,信息移到 webshell 的文件中展示,因此又减少了一个导航栏目!Webshell 有一些有意思的玩法,可以去试试看!

2. 网站架构

设计完成之后,就可以开始手写网站了!

2.1 前端

对于 HTML 文件,由于网站导航栏、底部页面需要在多个页面中复用,因此采用模块化的方式独立写,后续通过 HTML 模版渲染来拼接。编写时使用了 HTML5 的语义标签,这样除了可以针对搜索引擎优化外,还能减少后续编写 CSS 时不好指定元素而只能添加额外的 id 标签并使用#ID选择器的情况。

自己手写的几个模版,增加代码复用

HTML 写好后,接着就是最麻烦的手写 CSS 环节……(略过),以及一小部分 JS。因为网站的页面种类很少(只有 4 种:文章页、栏目主页、主页、错误页面),因此本站决定不拆分 CSS 和 JS,将所有页面的 CSS 和 JS 整合到一个大的main.cssmain.js中。这样虽然会增大第一次访问网站的响应(不到 1KB),但在跳转到其他类型的页面时,由于缓存策略的原因,就不再需要下载额外的 CSS 和 JS 文件,起到了预加载的作用。

404

404 错误页面展示

JS 主要包括了文章右侧目录自动生成、下滑滚动时导航栏标题切换、加密文章的密码验证、自动将指向站外的链接添加target="_blank"以在新标签页中打开等功能。

2.2 后端

由于几乎都是静态网站,因此后端不是大问题,直接放在 nginx 后面就行。但还有一小部分页面需要用到一点后端接口,例如主页的 IP 回显,以及部分应如 LaTeX 计算器。这些需要用到后端接口的页面采用前后端分离技术,前端仍然按照普通页面模版渲染,后端接口统一放在/api/路径下,用 nginx 反向代理。

后端也已启用 HTTP/2(上个版本的网站就已启用),并考虑添加 HTTP/3(QUIC)支持,可以优化遇到连续请求时的性能。

3. 网站部署

手写网站完成之后,就需要“编译”网站,成为最终发布的版本。下面的编译过程均通过脚本自动化实现。

3.1 Markdown 渲染

所有的博客无非都是在考虑如何把 Markdown 优雅的转化成 HTML
(我自己说的)

Markdown 渲染使用和 Mkdocs 相同的方案,即通过 python-markdownpython-markdown-extension 渲染 Markdown 到 HTML。但最终的效果好不好看是取决于 2.1 节中文章页面的 CSS 写的如何。

此外,对于 LaTeX 公式的渲染,python-markdown-extension 的方案是留给前端的 MathJax 解决,这就与新网站的“不使用任何前端框架”冲突,并且有的页面只有一两个公式,但却要先加载超过 1MB 的 MathJax 脚本和字体,很不划算。因此本站采用预渲染的技术,用 nodejs 编写如下脚本调用 MathJax,从argv接收待渲染的 LaTeX ,渲染成 SVG,并输出到 stdout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { argv } from 'node:process';

import { mathjax } from 'mathjax-full/js/mathjax.js';
import { TeX } from 'mathjax-full/js/input/tex.js';
import { SVG } from 'mathjax-full/js/output/svg.js';
import { LiteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor.js';
import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html.js';
import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages.js';


const adaptor = new LiteAdaptor();
RegisterHTMLHandler(adaptor);

const html = mathjax.document('', {
    InputJax: new TeX({ packages: AllPackages }),
    OutputJax: new SVG({ fontCache: 'none' })
});

const node = html.convert(argv[argv.length - 1] || '', {
    display: argv.findIndex(v => v == '-d') != -1
});

console.log(adaptor.innerHTML(node));

渲染的过程中,如果遇到 LaTeX 代码,则调用上面的脚本,将代码替换成脚本的输出。

3.2 HTML 模版渲染

渲染 Markdown 到 HTML 后,还没有结束,因为 python-markdown 仅输出文章内容对应的 HTML,不额外输出 body、head 等标签,因此这里就用上了 2.1 节中说到的模版渲染,将 python-markdown 的输出结果和其他模版进行拼接,形成完整的页面。

对于主页和栏目主页,并非是通过 Markdown 渲染生成,其本身就是一个手写的 HTML 模版。

在模版渲染的过程中,还需要变量替换一部分模版内容,例如网页标题、日期、canonical link、栏目主页的文章 / 相册列表等,这里采用了类似 Jinja2 的方案,但并没有使用 Jinja2,而是手动通过正则表达式替换,毕竟只有两三个待替换变量的地方。

3.3 静态资源优化

静态资源优化在上一个版本的网站中就已开始使用。渲染完所有的 HTML,并复制其他静态资源(CSS、JS、图片等)之后,下面这三个步骤能大幅度提升网站性能。

首先是最小化 HTML、CSS、JS,分别通过 html-minifiercleancssuglify-js 实现,其原理是移除多余的空格、换行、变量、标签属性、CSS 规则、化简变量名、混淆 JS 代码。最小化之后可以显著减小文件体积,降低网络的传输时延。

其次是为服务器开启 gzip 和 brotli 压缩,尤其是 brotli 压缩,也能显著减小文本文件的体积,但需要手动编译相关模块安装,可参考 ngx_brotli 的文档。此外,由于默认的压缩选项是 on-the-fly 的,对于这种性能差一点的单核服务器,压缩过程仍然消耗了一点时间,因此可以提前压缩,并启用gzip_staticbrotil_static选项,这样服务器在响应时会直接找到已提前压缩的.gz.br文件返回。

最后是将图片通过 cwebp 压缩成 WebP 格式,虽然损失了一点点画质,但是对于普通的网页浏览没有太大影响,并且也能显著减小图片的体积,对于这种小带宽的服务器也有很大帮助。

3.4 缓存策略

缓存策略在上一个版本的网站中也已经开始使用。静态资源因为不经常改变,因此设置合适的缓存策略可以减少重复的请求给服务器的带宽带来的压力。本站将所有的静态文件设为 30 天缓存,通过 nginx 的expires 30d;指令实现。

但是,假如某次的更新不小心在 CSS 或 JS 中引入了一个 BUG,希望用户下次访问的时候立即修复,该如何实现呢?第一种方法是改变文件名,例如main.随机数.js,但这样频繁改名太麻烦,第二种方法是添加一个不存在的 URL 参数,需要改动时就仅需更改这个 URL 参数,无需修改文件名。本站就采用第二种方法,在指向 CSS 和 JS 的链接中附带了ts参数,值为当前 UNIX 的时间戳,每次部署网站渲染模版时都会去更新这个ts,这样每次渲染后用户就能获取到最新的 CSS 和 JS 而不受缓存的影响。

1
2
<link rel="stylesheet" href="/main.css?v={{ ts }}">
<script src="/main.js?v={{ ts }}"></script>

3.5 搜索引擎优化

为了方便搜索引擎抓取网站,可以顺带在渲染过程中记录每个页面的链接,并最后生成一份 sitemap.txt 文件,并在 robots.txt 文件中说明该网站地图的存在,以便让搜索引擎爬虫抓取。

经过上述的各种优化,包括启用 HTTP/2,挑选了最近的一篇文章页来测试,在 GTmetrixPagespeed Insight 上都取得了比上个版本网站的主页还要好的性能表现。

4. 总结

现在的工具越来越方便,框架越来越多,几行代码就能搭一个服务器生成一个网站,自动帮你做好各种优化,生成 sitemap,甚至不需要你熟悉任何 Web 的知识,只要会写 Markdown 就可。也有人觉得如果不用框架,手写一定很麻烦。但是实践下来,感觉并没有复杂多少,反而对底层的各种逻辑关系更加了解,有一种 take full control 的感觉。而且在日常上网的过程中,我逐渐发现很多相同主题的 Blog,比如一看就是用 Hexo / Wordpress / Mkdocs 的某个主题生成的。同样的问题,这是更加便捷了呢,还是更加千篇一律了呢?

喜欢下面的这几段话,上个学期在图书馆无意看到的,出自 2000 年陈彤的《没有人知道你可以坏多久》。2000 年啊!可以说是很准的预言了!

节奏越快,新产品越多,我们就越手忙脚乱,紧迫的都没有工夫品味,更不要说享受其中的妙处
(到底是学 A 框架有前景呢,还是学 B 框架有前景呢,听说最近又出了个 C 框架?)

现在的多功能一般是折腾人的,表面上智能的东西往往越智障

算是预言了这几年拍立得的火爆吧

不知不觉这个小站过了 2 年多了,2 周年的时候还忙着毕业,忘了庆祝一下……暑假有动力抽空重构网站,离不开大家的支持(也会认真的看邮件)!

下周就要去南大了,应该又会有一段新的经历吧。期待开学!