为博客添加站内搜索和暗黑模式
立泉2024年06月12日更新:站内搜索已由
Pagefind
实现,参见博文《基于Pagefind实现静态博客站内搜索》。
博客自2016年创建一直使用Materialize作为UI
框架,它实现了Material Design的几乎所有组件,但在三年前发布v1.0
正式版后就不再更新。而Material Design
本身却在进化,从过度强调层级阴影变得趋向扁平,所以切换到一个更有活力的现代框架势在必行。
我不是Web
开发者,对HTML
、CSS
、JavaScript
和Node.js
仅浅尝可用,花不少时间解决问题,终于用Google
的Material Design Components完成重构。在继承原有设计的同时削减阴影、统一配色,使简洁淡雅风格更加协调,虽然还未达到设想中的完美状态,但以我当前审美来看,十分是可以给八分的。而且因为这次重构,我开始接触到Web
端的开发思路和主流技术栈,之前计划的站内搜索
和暗黑模式
顺便水到渠成。
站内搜索
Jekyll原生支持给文章添加Tag
标签,可以用Liquid语言获取页面的所有Tag
和标记相同Tag
的文章列表。比如这篇标记的CS
、Google
和DarkMode
等Tag
,点击顶部Chip
会弹出包含相关文章列表的Dialog
。只是Tag
毕竟有限,不可能把文章里每个词都设置为Tag
,所以站内搜索是一个非常诱人的功能点。写作日积月累,我有时候也想知道自己在多少文章中提及过某些特定内容🤔。
实现搜索无非两种方案,使用搜索引擎提供的站内搜索或完全自建搜索服务。后者更灵活且不依赖搜索引擎缓慢的索引更新,优势很大,但有太多前后端问题需要解决。所以最终选定Google
的站内搜索,一是因为博客早就被索引,可以直接使用,二来也是一个接触当前“最好搜索引擎”的契机。
用过Google
应该知道,可以像这样在搜索时限定网站:
此外它提供面向开发者的Programmable Search服务,允许用户创建自定义范围的Google
搜索,可以自定义部分UI
,也支持把搜索框和搜索结果嵌入网页中。我试着嵌入但发现组件风格和博客相去甚远,而且存在搜索结果不定引起的元素尺寸变化问题,所以虽然我并不擅长Web
技术栈,踌躇后还是决定手动调用搜索API
来搭建站内搜索。
配合Material Design
的最终成果还不错,调用API
会产生每千次5美元的费用,这个数字对于访问量很小的站点其实与“免费”无异。
用JS
请求数据填充页面要注意参数中的鉴权API KEY
,它来自Google Cloud Platform
里一个启用Custom Search API
的Project
,搜索服务的账单就是与它绑定。
此KEY
默认权限极大,可以控制整个Project
,非常危险。当我无意中把包含它的JS
文件Push
到GitHub
上时立刻收到Google
发来的安全警告邮件:
We have detected a publicly accessible Google API key associated with the following Google Cloud Platform project: Project JetSnail (id: jetsnail-****) with API key ****
We believe that you or your organization may have inadvertently published the affected API key in public sources or on public websites (for example, credentials mistakenly uploaded to a service such as GitHub.)
一定要按说明在GCP
上限定它只用于搜索。
2022年12月25日更新:注意搜索API可能被滥用引起的高额账单风险。
暗黑模式
我作为Android
开发者可以轻松实现Android
软件的暗黑模式
,但在不熟悉的Web
平台则花一段时间才理清楚。
系统/浏览器级别的暗黑模式
有一个标准CSS
触发选择器,prefers-color-scheme: dark
。当用户开启暗黑模式
时浏览器会加载此代码块中定义的暗色主题去覆盖原有CSS
属性,但这种方式只能响应全局主题切换,不能实现仅针对当前页面的暗色主题,不够灵活。
/* 默认主题样式 */
.title {
color: black;
}
/* 暗黑模式下的主题样式 */
@media (prefers-color-scheme: dark) {
.title {
color: white;
}
}
另一种方式是创建表示暗色主题的dark
类,在其中定义组件新样式或全局变量新值,这样切换暗黑模式
只需JS
监听触发事件给<body>
添加dark
类即可。同理它也适用于切换其它不同配色的主题,如.red
和.blue
。
/* 默认主题样式 */
body {
/* 表示字体颜色的全局变量 */
--text-color: black;
}
.title {
/* 应用`--text-color`变量对应的字体颜色 */
color: var(--text-color);
}
/* 在dark类中定义该变量的新值,如果dark类被添加到<body>中,则变量新值就会生效,页面也随之变化 */
.dark {
--text-color: white;
}
/* 也可以直接定义叠加dark类的新样式 */
.dark .title {
color: white;
}
监听主题切换按钮的示例:
const THEME_LIGHT = "light"
const THEME_DARK = "dark"
const KEY_THEME = "theme"
// 读取保存的用户主题设置
const savedTheme = localStorage.getItem(KEY_THEME)
console.log("saved theme = " + savedTheme)
const bodyE = document.body
// 根据用户设置显示对应主题
if (savedTheme == THEME_DARK) {
bodyE.classList.add("dark")
showThemeDarkIcon(true)
} else {
showThemeDarkIcon(false)
}
const btnTheme = document.querySelector("#topbar_btn_theme")
// 监听主题切换按钮点击事件,决定是否给body添加或删除dark类
btnTheme.addEventListener("click", () => {
if (bodyE.classList.contains("dark")) {
bodyE.classList.remove("dark")
showThemeDarkIcon(false)
localStorage.setItem(KEY_THEME, THEME_LIGHT)
} else {
bodyE.classList.add("dark")
showThemeDarkIcon(true)
localStorage.setItem(KEY_THEME, THEME_DARK)
}
})
// 根据当前主题配色显示主题按钮的图标
function showThemeDarkIcon(dark: boolean) {
const btnTheme = document.getElementById("topbar_btn_theme")
if (btnTheme == null) return
if (dark) {
btnTheme.innerHTML = "light_mode"
} else {
btnTheme.innerHTML = "dark_mode"
}
}
基本实现如此,只是我在把它与SASS
和Material Design Components
组合的过程中遇到很多问题,一些甚至困扰几天都百思不解,有太多陌生细节要逐个试错…过程曲折但所幸都能一一解决,所以当我看到最终成品的时候心里自然十分欣慰。
新问题
启用暗黑模式
使用几天发现页面跳转有时会出现“闪烁”,从一个页面进入另一个页面,浏览器会先加载默认的亮色主题再由JS
驱动切换到暗色主题,两个暗色之间的短暂亮色会“闪一下”。这个闪屏在本地测试中不出现,而在网络环境不佳的情况下大概率出现。
原因是主题代码放在统一的外部JS
文件中,为避免DOM
树构建好之前执行JS
可能出现的异常,其初始化时机被延迟到DOMContentLoaded
之后。如果网络不佳,浏览器会先用默认样式渲染页面,等待DOM
构建完成再触发JS
加载暗色主题,就是这个时间差造成“闪烁”。
那么只要消除时间差即可,把这部分代码嵌入HTML
的<body>
节点之下,使它在节点加载时立刻执行主题切换(添加dark
类),之后浏览器渲染页面会直接应用暗色主题,也就不会存在闪烁。
<html>
<head></head>
<body>
<!-- 切换主题的Button -->
<button id="topbar_btn_theme">Change theme</button>
<!-- 切换主题的JavaScript -->
<script>
function checkTheme() {
...
}
// 在body中立即执行主题切换
checkTheme()
</script>
<body>
</html>