重构博客网站,也就是该网站的记录。

多用 CSS

近期订阅了一些英语的周刊,主要是前端相关,里面收集了很多外网博客平台上的优秀文章。其中不乏有一篇文章吸引到了我:很多时候你根本不需要使用 JavaScript

吸引我的理由很简单。我的前端技术栈主要是 React.JS。相较于传统的「网页三剑客」,React.JS 这类现代前端框架需要客户端下载并执行更多的 JavaScript 代码。其核心的虚拟 DOM 技术虽然在过去带来了性能优势,但在现代浏览器性能已大幅提升的今天,其初始化和运行时成本有时反而不如原生方法来得直接高效。

但是,对过去的我而言,功能就是要用 JavaScript 才能做到。纯 HTML 和 CSS 能做到什么?它们又不是脚本语言。 不过在那个文章里,这个想法是片面的。现代的 CSS 技术进化很快、有着性能优异的各类方法,完全可以代替 JavaScript 来实现一些功能。

举个例子,我们想要实现主题切换功能。

如果是网页三剑客的话,我们可能会想到用 JavaScript 监听切换按钮,该按钮被点击了我们就改变被监听的类或者元素的样式。

在 React.JS 上的话,那就是存一个主题状态:如果是 light 模式就怎么怎么样;如果是 dark 模式就怎么怎么样。如果用的还是 Material-UI 或者其他 UI 框架,那大概率还会有个 theme 配置文件。

这些方案对我们而言应该都很熟悉:对啊,怎么了,这样做不是很正常吗。

其实可以更简单一些,用 CSS 的 color-mix() 方法就可以办到:

scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:root {
// 核心品牌色
--color-primary: #5454f8;
--color-secondary: #267B54;
--color-accent: #4088b8;

// 使用 color-mix() 生成交互状态
--color-primary-hover: color-mix(in srgb, var(--color-primary) 85%, black);
--color-secondary-hover: color-mix(in srgb, var(--color-secondary) 85%, black);
--surface-interactive-hover: color-mix(in srgb, var(--surface-interactive) 80%, var(--color-primary));
}

// 深色主题
[data-theme="dark"] {
--color-primary: #818cf8;
--color-secondary: #34d399;
--color-accent: #60a5fa;

// 深色模式下的交互状态
--color-primary-hover: color-mix(in srgb, var(--color-primary) 80%, white);
--color-secondary-hover: color-mix(in srgb, var(--color-secondary) 80%, white);
--surface-interactive-hover: color-mix(in srgb, var(--surface-interactive) 70%, var(--color-primary));
}

又或者,我们想要创建模态框。通常我们会写一个 <div>,然后写 JavaScript 来控制它的显示和隐藏、背景遮罩、锁定用户的键盘焦点、处理 Esc 键退出等等等等。实际上,可以直接用 <dialog> 来写、用 ::backdrop 伪元素给背景遮罩添加样式和动画、用 @starting-style 来实现更流畅的入场动画。

其他交互效果,例如按钮的颜色变化就更不用说了。这里还有一个很有意思的考量:JavaScript 很可能带来安全问题,而 CSS 是完全安全的。

带着这些全新的知识点,我重构了我的博客网站。其实之前我对于网站设计是没有一丝考量的,也不在乎 CSS 这些让我觉得「不如 React.JS 优雅高级」的技术,因此我的博客网站性能一般,对比原本主题的设计还添加了许多非必需的东西。我的顶部菜单栏出现过明显的元素堆积过多的情况,一些按钮很丑很奇怪、像是四不像、哪儿哪儿都不挨上,所以我决定先在博客网站上将部分功能撤下。其中就有搜索功能:首先它的样式我一直没有设计、难看得很;其次是这个功能我认为没必要留、不够精简。未来如果需要的话,我会想一个更好的方案。

不过在 Hexo Theme Ares 里,搜索功能还会保留,并且进行了一次小重构。

我还撤走了评论区的 Disqus 评论区,因为这东西多多少少会给我带来一些影响:今天有没有人发评论?今天有没有人作反应? 其实很没必要,本身看的人就不多。

字体

正确导入字体

我不只是将一些功能用 CSS 重构了,我还改了一下字体的导入方式,参考的是另一篇 优质的文章,具体讲了现代网站是如何错误地导入字体的。

我的博客主题原先会导入足足四个字体:

  1. 英语的 Open Sans
  2. 简体中文的 Noto Sans(其实就是思源黑体,不过我在考虑换成其他的;我个人阅读时喜欢用霞鹜文楷,但还没尝试换过,有可能和我的主题不搭配)
  3. 用于特殊标题的 Dosis
  4. 代码块用的 JetBrains Mono

这些字体都是用 Google Fonts 的 CDN 导入的。其实这种方式并非最佳实践,很容易导致性能问题:浏览器会发起两次请求,第一次用于请求 fonts.googleapis.com 这个地址、获取一个 CSS 文件,第二次则是请求 CSS 文件内的真实字体文件地址。这个过程至少会有两次网络往返,第一次请求的 CSS 文件会阻塞渲染,这意味着在它下载完成之前,页面可能是一片空白或者没有应用任何样式。为了解决这些问题,我是这么做的:

scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 加载真正的 Open Sans 字体
@font-face {
font-family: 'Open Sans';
src: url('...') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

// 2. 创建“替身”字体
@font-face {
font-family: 'Open Sans Fallback';
src: local('Arial'); // 使用人人都有的 Arial 字体
font-weight: 400;
font-style: normal;
font-display: swap;
// 强行让 Arial 的尺寸变得和 Open Sans 一样
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
scss
1
2
// 3. 在字体栈里,让“替身”字体紧跟在真正的 Open Sans 字体之后
$base-font-family: 'Open Sans', 'Open Sans Fallback', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;

这个思路很简单:先让浏览器用这个调整好尺寸的 Arial 字体把字体显示出来,因为是本地字体,所以速度会很快、文字的位置会迅速固定。当真正的 Open Sans 字体下载好之后,浏览器就会立刻用它替换掉临时的 Arial「替身」。由于位置和尺寸早就被 Arial 字体固定好,所以整个替换过程在视觉上是无缝的,页面完全不会跳动。

再就是字体格式,相较于 TFF 或 OTF 等传统格式,用 WOFF2 更好。WOFF2 的压缩率极高,兼容性也特别好,尽量开发网页时都采取这个字体格式。不过有些字体不让转换成这个格式,需要特别注意一下。

你也可以看出来,我用了 unicode-range 控制了字体的字符范围、只让页面加载它实际需要的字符。比如说 Open Sans 只加载基本拉丁字符、标点符号和货币符号,Noto Sans 则只包含中文汉字字符范围(unicode-range: U+4E00-9FFF, U+3400-4DBF, U+20000-2A6DF;)。

不同字体的视觉对齐

作为一个中文技术博客,文章中难免会出现英文单词以及代码片段。为了达到最佳的阅读体验,我会为中文字符、西文(拉丁)字符以及代码用的字符分别指定一个字体。然而这里会面临一个问题:不同的字体,即时 font-size 是一致的,但它们在视觉上的大小、重心和基线位置往往是不一样的。当这些中英字符同时出现在一行时,就会显得大小不一、高低错落,破坏了排版的和谐感。

此处令我想到高中时、一位我很喜欢的老师。当时她还不是正式教师,考核时给我们上的课讲的就是排版和字体。当时并没有认真听讲。

CSS 提供了一些属性,恰好可以让我们解决这个问题:

scss
1
2
3
4
5
6
7
8
@font-face {
font-family: 'JetBrains Mono';
// ...
size-adjust: 94%;
ascent-override: 92%;
descent-override: 22%;
line-gap-override: 0%;
}

我通过 size-adjust: 94% 将 JetBrains Mono 的整体视觉大小缩小了一点,然后用 ascent-override 等属性微调了它的垂直对齐基线,最终让它和我的正文字体放在一起时,看起来没那么突兀了。

流式排版

每个人阅读博客的设备都各不相同,有用电脑的,有用平板的,有用手机的,有用小天才手表的(有吗?)。不同设备的屏幕尺寸不同,我们通常需要在多个端点处使用媒体查询来手动调整 font-size。例如:

css
1
@media (min-width: 768px) { body: font-size: 17px; } }

这很繁琐,要知道现在世界上有多少种屏幕尺寸,一个个适配过去会累死掉。更优雅的解决方案是采用流式排版,即让字体大小像液体一样,随着屏幕宽度的变化而平滑、无缝地缩放,确保在任意设备宽度下都有最佳的视觉表现。实现这种效果,要用到 clamp() 方法。

css
1
2
3
4
5
:root {
--font-size-body: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); /* 从16px平滑过渡到18px */
--font-size-h1: clamp(2rem, 1.5rem + 2.5vw, 3rem); /* 从32px平滑过渡到48px */
// ...
}

clamp() 的三个参数分别是:

  • 最小值
  • 首选值
  • 最大值

告别 jQuery 和 Font Awesome

优化完字体系统后,可以想想图标字体该怎么办。我的原先方案是用 Font Awesome,实际上也不是一个很好的选择。Font Awesome 是用传统的网络请求加载的,也会导致不必要的性能和阻塞渲染问题。最好的方案是使用 SVG。很多时候我们的网站只会用到一些图标字体,为此下载一整个完整字体库很没必要,SVG 的可访问性也更好。

我同时又检查了一遍博客的代码,发现主题的所有功能都被原生 JavaScript 实现。因此我移除了 jQuery 依赖。其实我都不是很明白这个主题哪里用到了 jQuery,可能是主题作者先前留下的。

提升交互体验

在阅读了 这篇文章 之后,我为我的网站添加了一些可以交互的内容。主要添加的是一个「返回顶部」按钮,移动端上它会显示在顶部居中的位置,桌面端则固定显示在右下角。先前在 这一节 里的例子里提到的动画、基于 <details> 的菜单我也实现了。

无障碍增强

我几乎是不会考虑「无障碍」的,过去的时候。在阅读了一些博客网站的设计想法时我意识到,实际上用着「不寻常设备」的读者可能比想象中还要多,总是要确保所有的用户都能良好地使用博客功能。当然,不只是博客,未来开发的网站也要想到这一点才行。这次重构中,我添加了一个跳过导航的按钮,它平日会被藏在屏幕视图的上方,需要使用 Tab 才可以将其唤出。Tab 也可以用来快速导航到下一个标题、链接、代码块等。

我也为正文链接添加了下划线,有用到 text-decoration-skip-ink: auto 这个属性,可以自动跳过字形,例如拉丁字符 gj 等。