技术选型
不再选择静态网站生成工具
我的博客本来是用 Jekyll 生成的静态网站,因为网站架在 GitHub Pages 上,而 Jekyll 是 GitHub Pages 默认的构建工具。但是 Jekyll 的核心开发者年事已高,有的甚至已经去世,所以我感觉这个项目未来的活力堪忧。
Jekyll 是用 Ruby on Rails 写成的,其他编程语言,和 Jekyll 功能类似的工具有:
- Python 的 Pelican
- JavaScript 的 Hexo
但是我还想做些更复杂的事情,预计需要靠服务端来实现。所以与其花时间移植到一个和 Jekyll 功能类似的工具,不如直接一步到位,学习一个全栈框架。
Next.js
虽然不同的编程语言也都有自己的全栈框架,比如 Python 有 Django,但是既然浏览器主要支持 JavaScript,所以索性前后端都用 JS 比较方便,而且这样想的人很多,社区规模使得遇到问题更容易找到答案(这个优势在生成式语言模型的时代似乎没那么重要了)。
JavaScript 语言之下,也存在至少 React 和 Vue 两大阵营。之所以选择 Next.js 这样一个基于 React.js 的框架,主要是路径依赖,很久很久以前学过赫尔辛基大学的全栈公开课,那里教的就是 React。
对于 Next.js 本身,Youtube@Fireship 有一个很简洁的介绍:https://www.youtube.com/watch?v=Sklc_fQBmcs
官网提供的教程在这里:https://nextjs.org/learn,和我学习的时候已经不一样了,那时候只有 page router,没有 app router。开发这一框架的 Vercel 公司也提供 Next.js 的云服务。但是鉴于其有过把用户引诱到 app router 架构然后给自家服务提价的黑历史,所以短期内不打算学和用 app router.
TailwindCSS
早就学过了,但是一直没有机会用。这次本来也可以不用的,直到决定使用 Shadcn.ui 组件库,因为 TailwindCSS 是它的一个依赖项。既然已经安装了,那不用白不用。
Shadcn.UI 而非 HeadlessUI
一套组件库,也就是网页中经常出现的功能单元。这个库相对于竞品的最大优势,是允许直接复制粘贴代码而不用安装,用多少抄多少~
TailwindCSS 自家也有一个组件库 HeadlessUI,但是主要是 <form/>
表单及其成员,侧重于向后端传数据的 HTML 元素的封装。而 Shacn 有很多炫酷的交互方面的组件,突出一个现成且好看。
没用 Figma
有用的功能都收钱,免费的功能不如直接 next dev
实时预览。
踩过的坑 学到的经验
组件化作为一种思路
正常的网页,HTML 负责内容,CSS 负责装饰,JavaScript 负责交互。这种分工,专业上叫做解耦。
工程实践表明,这种解耦方式非常反人类。用户看到和使用的网页,是以功能上的相似、空间上的相邻为组织的,而要对其修改时,则需要到不同源代码的不同位置去;反之,某处代码的改动,无意中可能对远处的另一部分视觉效果和功能造成破坏。
于是较新的全栈框架,其解耦的方式都是以组件为单位的,内容、样式、交互逻辑都写在一起。
JavaScript/JSX 语法中的 {}
JSX 是对 JavaScript 语法的扩展,添加了类似于 HTML 标记的写法 <MyComponent></MyComponent>
,用来表示 react 组件。
在 jsx 语言的内部写 Javascript 时,需要将 JS 外面包裹一层 {}
JavaScript 里类似 Python f-string 的结构写作${}
,名叫 template literal: this is var: ${var}
——以上规则结合起来,会产生让初学者迷惑的现象:
<MyComponent className=’dark’/>
: 一个正常的类名,直接用引号<MyComponent className={dark}/>:
类名是一个 JS 字符串变量- 类名的一部分根据一个变量取值:
<MyComponent className={`bg-${dark}`}/>
JavaScript 箭头函数中的 {}
和 ()
JavaScript 的箭头函数类似于 python 的 lambda 纯函数,但是有不同。
(var) => (expression(var))
相当于 Python 中的 lambda x: expression(x)
但是箭头函数的右侧可以是 {}
包裹的若干表达式,此时需要显式 return:
(var) => {
expression1(var);
expression2(var);
return expression3(var);
}
Python 的 lambda 必须是单一表达式的纯函数,不允许上面第二种写法。
对象解包中的 {}
当有一个包含若干键值对的对象时
obj = {
k1: "value1",
k2: "value2",
k3: "value3",
...
}
可以用 const { k2 } = obj;
的方式拿到 obj[’k2’]
的值,赋给 k2
。
结合上一节,
(k1,k2)=>(k1+k2);
是一个两个自变量的函数;({k1,k2})=>(k1+k2);
是以一个 Object 为自变量的函数,这个 Object 的名字无所谓,也不确定一共有多少个属性,但属性中至少包含k1
和k2
.
TailwindCSS 的 arbitrary value、JavaScript 的模板字符串、Shadcn 中的 cn()
函数
TailwindCSS 的很多属性都允许在方括号中使用任意值,比如背景色 bg-[#a4b4c4]
本以为可以直接在 className
里面用 template literal <div className={
bg-[${myColor}]}>
,但是并不总是生效。
这个“并不总是”是个大坑,一开始在开发模式用得好好的,结果某次刷新页面之后就挂了,简直莫名其妙。
好在 Shadcn 提供了一个 cn()
函数,接受一个或多个 TailwindCSS 类名字符串作为输入,就可以正常使用 template literal 了,例如 <div className={cn("block","border-0",
bg-[${myColor}])}></div>
HTML + CSS 布局
两类套路:
理论上后者新一些,消耗的脑力也更少一些,应该是更好的选择。
但是前者也有一些优势场景。比如现在整个博客页面的上边栏和剩下的部分就是一个上下结构的 flex 布局,这导致屏幕最右侧的滚动条其实是页面一部分的,而不是整个页面的的滚动条。在 iOS 的 Safari 浏览器下,会导致网址栏不能自动隐藏,浪费很大一片屏幕空间。之后可能会换回传统功夫。
而像是目录,文章跳转开头和评论区的按钮等等,需要在页面滚动时相对屏幕静止的元素,就不得不用 position,而且为了锚定在正文上,还需要嵌套好几层。
按照传统功夫——
display
可选的取值有 4 个: block | inline | inline-block | none- block: 竖排,哪怕同一行内仍有空间容纳 html 中的下一个元素。
- inline: 横排,像文字内容一样,没有盒模型
- inline-block: 横排,但是有盒模型
- none: 不显示,和
visibility:hidden
的区别是,后者依然占有显示时的空间。
position
可选的取值有 5 个: static | relative | fixed | absolute | sticky- static: 默认值,按照 html 文件的顺序排列。
- relative: 相对于 static 默认值进行偏移,偏移量由
top
,right
,bottom
,left
四个性质决定。所谓left: 50px
的意思是左侧 margin 外多出 50 像素的空间,实际是向右偏移的效果。 - fixed: 相对于视窗的位置固定,位置由
top
,right
,bottom
,left
四个性质决定。left: 50px
的意思是该元素的左侧 border 外沿距离窗口左边 50 像素。 - absolute: 相对于最近一层父元素的位置固定,位置由
top
,right
,bottom
,left
四个性质决定。left: 50px
的意思是该元素的左侧 border 外沿距离父元素左侧内沿 50 像素。设计的时候需要考虑 border 宽度。 - sticky: 网页加载时按照 html 文件的顺序排列,直到网页滑动到某一位置,之后该元素固定在视窗,就像 fixed 一样,行为改变的位置由
top
,right
,bottom
,left
四个性质决定。
float
: none | left | right | inherit- none: 默认值,按照 html 文件的顺序排列。
- left: 保持在父元素左侧,其他元素环绕之。
- right: 保持在父元素右侧,其他元素环绕之。
- inherit: 和父元素的 float 的取值一致。
Unified.js 将 Markdown 文档转换为基于 JSX 的 HTML
Jekyll 等静态网站生成器的核心功能,就是把 markdown 文档翻译成 html 网页文档。在 Next.js 框架下,这一工作由以 unified.js 为基础的一群第三方库来完成。
最早看到这个框架是在 DIYGOD 的博文《如何优雅编译一个 Markdown 文档》里,但是直接抄他在 xlog 里面的代码的话,在 next.js 之下好像会报错。所以又去官网仔细读了一下文档,现在可以说是略懂。
这个话题本身值得专门写一篇文章,所以不在这里展开了。
Giscus 评论区切换黑夜模式
自己写的组件的亮暗切换,是通过在 <html/>
元素添加和删除 dark
类,然后搭配 TailwindCSS 的 dark:
来实现的。
Giscus 官方支持切换黑夜模式:https://github.com/giscus/giscus/blob/main/ADVANCED-USAGE.md#parent-to-giscus-message-events。这套方法的关键,在于服务端返回的 iframe 有一个名为 giscus-frame
****的类。
Giscus 为 react 提供了一套组件可以直接使用,但是在这套组件里面并没有这个类。
所以只能弃用官方的组件,自己用 useEffect
模拟官网的 <script/>
export function MyGiscus() {
useEffect(
() => {
const onPageLoad = () => {
console.log("<MyGiscus/>: activated on page load.")
// START real business
const script = document.createElement('script');
script.src = "https://giscus.app/client.js";
script.setAttribute('data-repo', '');
script.setAttribute('data-repo-id', '');
script.setAttribute('data-category', '');
script.setAttribute('data-category-id', '');
script.setAttribute('data-mapping', '');
script.setAttribute('data-strict', '');
script.setAttribute('data-reactions-enabled', '');
script.setAttribute('data-emit-metadata', '');
script.setAttribute('data-input-position', '');
script.setAttribute('data-theme', '');
script.setAttribute('data-lang', '');
script.crossOrigin = 'anonymous';
script.async = true;
document.getElementById("comments").appendChild(script);
// END real business
};
// Check if the page is already loaded
if (document.readyState==='complete') {
onPageLoad();
} else {
// Add event listener for page load
window.addEventListener('load',onPageLoad);
// Cleanup the event listener on component unmount
return () => { window.removeEventListener('load',onPageLoad); }
};
},
[]
);
return (<div id='comments'></div>);
}
Next.js 的构建参数部分
要让 next.js 构建静态网站,需要在 next.config.js 中写:
module.exports = {
basePath: '/blog',
output: 'export',
generateBuildId: async () => "buildID",
// i18n: {
// locales: ['zh-CN', 'en'],
// defaultLocale: 'zh-CN',
// localeDetection: false,
// },
}
basePath
是因为博客的 GitHub 仓库 blog 不是默认的个人网站仓库;需要注意的是,next.js 自己的 Link 组件的 href
参数不需要包含这个值,但是图片等等的 src
参数需要。
generateBuildId
函数的返回值是随便写的,不设定的话会导致输出里的 _next/
文件夹里有很多哈希值为名的文件夹,在 git 下会被当成不同的 blob 一直留在项目里。
i18n
参数被注释掉了,因为静态生成的 next.js 项目不支持自动 i18n.
RSS 源和 sitemap
RSS 由 feed 这个 npm 包来构建;
sitemap 则是在 ChatGPT 的帮助下手写字符串。
写好的字符串,通过在主页或者历史归档页面的 getStaticProps()
函数,写入 next.js 项目的 public/
文件夹。
还没解决的问题
手机端搜索框的汉字输入问题
页面顶端的搜索框在手机触摸屏上,用汉字输入法输入关键词之后,直接按回车键,会导致已经输入的汉字被当成拼音,传递给搜索引擎。
暂时的办法是在输入汉字之后,按回车键之前按一下空格。
Shadcn.Drawer 组件的上游代码报错;Google Ads
新博客把谷歌广告撤了。赚不到多少钱不说,它还会往网页里动态添加元素,破坏原来的排版。
话虽如此,每篇文章的右下角还是有个要饭的图标,计划用 shadcn 的 Drawer,放赞赏二维码,或者交换来的友站链接。
但是目前 Drawer 的上游代码会报错,看起来作者已经在修复了,等更新。
Google Analytics
next.js 提供了 Google analytics: https://nextjs.org/docs/pages/building-your-application/optimizing/third-party-libraries
但是加入之后没有反应,google 后台看不到,数据一落千丈。
据说把相应的代码放到 pages/_app.js
可以解决问题,还没试。