想写一个新的个人主页很久了,甚至基于 Next.js、Nuxt.js 分别做过雏形,但是都半途而废。寒假接触到了早有耳闻的 Astro.js,这个框架简洁优雅的设计吸引了我。于是一发不可收拾,开发了全新版本的主页,并将各种页面和博客系统也集成进了这个主页。在设计和开发的过程中有无数的纠结和思考,特此记录下来。

Bento 式布局

我是看了少数派的《何为 Bento 式布局,怎么生产力工具网站都在用?》这篇文章,想到用 Bento 的风格做一个个人网站的。Bento 式布局各种信息平级,非常适合「自我介绍」。相比一大段文字的自我介绍,用这种布局更有意思,也更吸引人阅读。

很早之前试用了 Bento.me 这个广受好评的工具,虽然功能比较有限,但是其 UI / UX 设计精雕细琢,非常精致。这次可以说我的网站 Bento 部分其实很大程度仿制了 Bento.me 的风格。

由于 Bento 布局不是线性的,所以没法像平常的网页一样采用响应式的逻辑,当页面宽度减小时自动调整。Bento.me 对于这个问题的解决办法是:提供大屏幕(四列)、小屏幕(两列)两套布局,让用户分别配置调整。参考这一方法,我的 Bento 组件也针对四列、两列分别指定了布局。

在使用 Astro 编写这一部分组件的时候,最麻烦的其实是确定一个 Box 的抽象层级。每个 Box 看似都可以写成一个组件、可复用,但是许多 Box 又有不同的背景、背景位置、前景色、hover 行为、对齐方式…… 这些都只能在 Box 的最外围元素上指定,如果通过 props 传递,则过于冗长,代码会十分丑陋。综合考虑,我定义每个 Box 最外层用一个 BoxWrapper 组件,专门负责 positioning,分别指定四列、两列模式的大小、位置;在 BoxWrapper 的 slot 中放置内部的组件,干脆分为多种组件:纯文字的 Box、带背景的 BgBox、地图组件 Map(其实这是个图片)以及 MBTI 组件等等。虽然各个组件之中还有不少重复代码,不符合 DRY 原则,但是暂时想不到更加合理的解耦合方式。

最终的 Bento 效果

字体的选择

对于一个有设计的网站,字体的选择其实非常重要。它是网站 personality 的重要部分(参考《Refactoring UI》)。

我一直钟情于 serif 字体,因为它们看起来文艺且有些复古,非常适合个人博客。所以,这次我还是全局使用了思源宋体。为了在不同平台呈现相同的字体体验,我使用 Google Fonts 并使用 loli.net 的镜像。

此外,对于引言(blockquote)中的字体,我其实希望使用楷体(因为感觉很多出版物都是这么做的)。然而由于中英文字体体系分类的不同(serif 对应宋体,sans 对应黑体,什么对应楷体呢?),楷体似乎没有被纳入 Web 字体世界的一等公民,在 Google Fonts 中也没有提供。我只能尽量尝试使用用户本地的楷体。

.font-kai {
  font-family: "KaiTi", "KaiTiGB2312", "STKaiti", "Noto Serif SC", serif;
}

需要注意一个离谱的问题:macOS Safari 浏览器不支持本地楷体。原因是「为了保护隐私,防止通过用户安装的字体追踪用户」,Safari 中 font-family 不能使用本地安装的所有字体,只能使用系统字体的一个子集……而这个子集不包括楷体(参考)。所以,macOS Safari 浏览器无法使用楷体。看来,又 get 了 Safari 的一个逆天特性 😇。

Darkmode 支持

让网站支持 darkmode 是我的一个执念。因为:1)所有浏览器、操作系统都有了 darkmode 的功能,如果不去兼容这个功能,会感觉自己的网站是「功能残缺」的;2)我既想要纯白的简洁设计,又想要在被窝里看着不伤眼睛的暗色设计。同时做两套主题能够满足我这样的要求。

然而 darkmode 设计和实现起来并不容易。为了保证颜色的协调,往往需要对两套主题单独调整颜色,并不是简单的「反色」。

比如,我们天然会认为「颜色较浅(较亮)的元素是突出的」。如果要绘制一个按钮或卡片,不管白天黑夜,前景都必须比背景更浅(更亮)。所以在白天就要采用「浅灰色背景、白色前景」,夜晚就要使用「黑色背景、深灰色前景」,这样看起来才会统一且协调。比如,看下面四种配色方案下的按钮,显然 2 和 3 是比较正常的,而 1 和 4 则比较奇怪。(然而事实上,1 和 3、2 和 4 分别互为反色。)

四种配色方案中的按钮

由此可见,darkmode 不能是单纯的反色,而是对色彩方案的单独设计。这一点在 Apple Developer 的《人机界面指南》中《深色模式》一篇也有提及:

深色模式下的调色盘包含较暗的背景颜色和较亮的前景颜色。需要注意的是,这些颜色不一定是其对应的浅色颜色的反转:虽然很多颜色是被反转的,但有些颜色则不是。有关更多信息,请参阅规范

还有另一个问题,黑夜模式的背景使用 0x000000 的纯黑并不是一个很好的选择。纯黑俗称「A 屏黑」,在夜晚看久了眼睛会非常不舒服。作为替代,我必须选用一种接近 black 的更浅的背景色。

从白到黑:gray 与 neutual

查看了 Tailwind 提供的颜色列表,才知道从 white 到 black,并不止 gray 一种过渡方式。Tailwind 提供了 gray、neutral、cool、warm 四种方式,每种都有 100 到 900 从白到黑的过渡值。

我并不懂一些复杂的色彩理论,只是从视觉上凭感觉而言,我觉得 darkmode 更适合用 neutral 系列的颜色。它看起来更温暖和舒服,也给人一种文艺的感觉,非常适合个人网站。相比之下,gray 整体偏蓝。我的 Daydream Typecho 主题的 darkmode 背景色就是 pico.css 提供的 gray 系列颜色,相比之下可以明显感到 gray 作为背景色更蓝一点。

而白天 lightmode 应该用哪种灰色呢?还是凭感觉,我认为白天用 neutral 系列则会感觉偏暖。因为在白天我希望传达出的是一种「富有执行力、富有活力」的感觉,所以似乎用 gray 更加合适。(当然这都是我极其主观的感受……)

所以最终决定:白天用 gray 系列颜色,夜晚用 neutral 系列颜色。

白天和晚上的 shadow

下一个棘手的问题是阴影。阴影能够给页面元素添加立体感,Tailwind 也提供了方便的 class 应用阴影,所以我非常喜欢用。然而,darkmode 下如何应用阴影,值得仔细思考。

从现实生活的经验来说,阴影产生于对光线的遮挡。所以,白天光线充足的时候,会产生黑色的阴影。然而,夜晚没有光线的时候,就不会产生阴影。夜晚并不会产生白色的阴影,这再次说明了「darkmode 颜色方案不能是对 lightmode 的反转」。

然而,「夜晚不会产生阴影」的前提「夜晚没有光线」,这一点很奇怪,因为如果没有光线,我们就看不见任何东西,怎么能看见页面中的各种元素呢?

这提示我重新思考 lightmode、darkmode 和整个网页对应我们现实生活经验中的具体场景。想象一个开着灯的房间,地面上摆满了网页里的各种元素。当开着灯的时候(lightmode),所有按钮、卡片都呈现白色,并自然地投射出黑色的阴影;当关了灯后(darkmode),所有按钮、卡片本身会发出微弱的光(否则无法解释为什么还能看到它们)。

因此,我调整了夜间模式阴影的颜色。

白天和夜晚的按钮

这不是最好的设计,但应该至少是逻辑可以自洽的设计……

主题切换按钮?

还有一个比较犹豫的点,就是是否要在网站中加上主题切换按钮,即在「跟随系统、亮色主题、暗色主题」之间切换。这是不少网站流行的做法。

其实我已经初步实现了这样的组件,但是在测试中有如下几个问题:

仔细想想,其实在网页中提供主题切换按钮并没有必要。对于知晓 darkmode 概念的用户,他们自然会在浏览器中设置好自己适应的主题;对于不知晓 darkmode 概念的用户,他们不在乎这一功能,设置这一按钮也会带来困扰。

Apple Developer 在《人机交互指南》中《深色模式》的最佳实践里也写道:

避免提供 App 特定的外观设置。App 特定的外观模式选项会额外增加用户的工作量,因为他们必须调整多项设置才能得到想要的外观。更糟糕的是,用户可能会觉得你的 App 是有问题的,因为 App 没有使用他们选择的系统范围外观。

综上所述,我没有添加主题切换按钮。

GitHub Actions 自动构建和发布

最后,我希望我的网站使用 GitHub Actions 自动构建和发布。

如果不使用 GitHub Actions,我能够想象到我滑稽的手动构建流程:在本地 npm run build,然后将生成的 /dist 目录打包 scp 上传到服务器,在服务器上删除原来的网站文件,解压压缩包放进网站目录……这一套全手动流程太不优雅了,也缺乏标准化。

如果将网站部署到 Vercel 之类的 Serverless 平台,对应的平台都提供了非常方便的一键设置。但是,我希望将构建完毕的网站部署在自己的服务器上。这就需要手动编写自己的 Action。具体来说,参照 GitHub Pages 的实现,运行 npm run build 之后将 /dist 目录生成的文件部署到 pages 分支。在我服务器上的网站目录中,使用 git 克隆 pages 分支的文件,每次网站更新之后只需要 pull 即可。

name: Deploy Stable Version

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: write

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: main
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 21
      - name: Install dependencies
        run: npm install
      - name: Build project
        run: npm run build
      - name: Deploy to pages branch
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist
          publish_branch: pages

在构建的时候会遇到网络问题。因为我的博客系统(blog.skywt.cn)使用了又拍云的 CDN,对海外线路的支持非常差。对于海外用户来说,使用又拍云 CDN「加速」实际上是「减速」,并且经常出现无法访问的现象。GitHub Actions 的 runner 当然都在海外,在构建的过程中,需要访问我博客的 API 获取大量数据,这个过程中难免会出现网络问题,导致构建失败。

解决方法灵感源于这篇《出海业务如何免费做到全球加速》。我的 DNS 是阿里云提供的,阿里云 DNS 也提供了分线路解析的功能。可以将「境外」线路直接解析到服务器 IP,「默认」配置(即其他国内线路)解析到又拍云 CDN。这样,境外线路不会通过又拍云 CDN,虽然延迟不小,但不会再出现网络问题。(不过更好的解决方案是国内走又拍云 CDN,国外走 Cloudflare 或者 Cloudfront。改天配置好了这个可以单独写篇文章。)

最后:「黑客与画家」

某天在玩 Cities: Skylines(一款城市设计规划游戏)的时候突然想到:我在做的事情和搞开发好像。都是创造一个东西,看这个东西运行起来,并获得某种成就感。甚至前者的入门门槛和后者一样高……

之前《电路与电子学》课程的期末设计,用 Quartus II 设计一个原型机,需要自己设计组件之间的各种连线。做这个大作业前几天我也沉迷 Cities: Skylines,当时在给各种组件之间连线的时候,我恍惚有一种神奇的感受:这好像在 Cities: Skylines 中修建道路。没错,这也和游戏很像。

本质上,这些给我们带来的都是创造的快乐

其实 Cities: Skylines、Minecraft 这一类游戏,要求玩家在其中创造城市或建筑,和城市规划、建筑设计的工作非常类似,只是简化了流程、缩短了反馈时间、降低了门槛。许多人戏称玩 Cities: Skylines 是「上班」。而玩家这些本来应该是「生产」的行为,却成了「消费」的行为。

这是因为人们在真正的工作中无法获得满足感,每天都做着自己也认为毫无意义的工作,所以只能在下班时间在这些游戏里寻找有意义的「创造」的快乐。

所以马克思写道:

人只有在运用自己的动物机能——吃、喝、生殖,至多还有居住、修饰等——的时候,才觉得自己在自由活动,而在运用人的机能时,觉得自己只不过是动物。

于是,动物的东西成了人的东西,而人的东西成为动物的东西。

说回搞开发,我越来越感觉到,开发本质上和这些游戏不是一样的吗?一样是在创造一个东西,一样能够得到即时的反馈。只是 Skylines 里面创造的是城市,软件开发创造的是软件。软件开发者也是创作者。

黑客与画家的共同之处,在于他们都是创作者。与作曲家、建筑师、作家一样,黑客和画家都是试图创作出优秀的作品。

——《黑客与画家》

如果未来能在自己的事业中体会到「创造」的成就感,那将是十分幸运和幸福的事情。

Daydreamer

这整套程序,我将其命名为 Daydreamer,它将作为一个持续开发的 playground,加入各种好玩的功能。

其实《Daydreamer》是 AURORA 的一首歌,这也是 Apple WWDC 2020 的开场曲。那是 Apple 第一次因疫情线上举办 WWDC。

Then we become night time dreamers
Street walkers, small talkers
When we should be daydreamers
And moonwalkers, and dream talkers

——《Daydreamer》

是呀,我们慢慢在夜里才敢做梦,慢慢成为了没有梦想的平凡之辈。

但是我们本可以在白天做着白日梦,我们可以登上月球,可以大声谈论自己的梦想。