POSTS / Hello Hakuba
通过 Github 自制 Blog 是很久之前的想法,之前也曾试过使用 Hexo, Gatsby 和 Docusaurus,但是因为各种原因失败了,然后,Hakuba 它来了。
因为我一直想去白马北阿尔卑斯徒步,并且在我开始写 Hakuba 的时候女票正在白马旅游~~所以名字就有了~~
初衷
之前的 Blog 是基于 Hexo,其实仅仅也是选用了社区的主题,部署一下而已,但是也有很多东西让我不满,比如:
繁琐的写作流程
我需要打开对应文件夹,创建,写作,为了 preview 还需要确定打开的支持的编辑器,然后提交到,然后自动部署。
其实说非常繁琐,可能不算是,但是每一次都会有一种奇怪的仪式感伴随着我,在我看来这变得是一件非常正式的事情,如果是长文章还好,假如我某天只想分享某个技巧,也需要一样的流程。
(虽然说其实变得更简单了我可能也不会写多少文章并且如果希望在移动设备上简单编辑,虽然相关的解决方案不少,但是总是变得麻烦。
但是如果可以通过 CMS 去写作就会变的方便不少,市面上有很多 CMS,其中最出名应该就是 WordPress 了,也有人用 notion 去写作。
我甚至有个朋友用 notion 去做 api。质量参差不齐
当时使用的是 hexo-theme-cactus 主题,风格非常得我心,并且 Hakuba 也被它影响。
但是如果 测速一下 评分就有点不忍直视,这仅仅是一个 Blog,没有复杂的交互,图片也不多,我认为这个速度和体积不能接受。
预期
对于 Blog 的预期,在我看来有以下几个:
- 必须要有良好的 SEO
- 必须要体积非常小
- 必须支持 hydrate
- PageSpeed 评分接近满分
- 方便的编写方式
技术选型
以往由于我一直对 Vue 没有好感,所以我的备选只有 Gatsby,Docusaurus 和 Hexo,按照常理来讲,几乎这些都是最好的选择,但是对我来讲有各种问题。
Gatsby
对于 React 技术栈,这是一个非常好的选择,但是不支持 Markdown,并且必须使用 Graphql,我感觉有点不灵活。
Docusaurus
良好的 Markdown 支持,对于文章有代码的 Blog 很好,官方也支持了 Blog 的功能,但是毕竟原本是针对文档,而不是博客,所以在样式改动方面比较麻烦。
Hexo
如果只是选择一个主题,然后部署,这也很赞。
但是在此之上开发了解的东西就更多了,比如如何使用 react 去开发?一些常用的社区插件?
当然可以自己来都,但是一旦使用 Hexo,做某个功能可能就存在最佳实践,单纯怼代码可能能用,但不一定没坑。
这种包袱让我感觉到非常难受。
SPA
SEO 效果非常差,这一点不能忍,只是一个尝试。
另外会非常容易触发 Resource limitations。
这些我都尝试过一下,总结下来就是,我只想简单写个 Blog,但是你需要了解的东西太多了,或者说某程度上不灵活,这并不是我想要的。
CMS
选择 Github Discussion 的原因很简单
- 基础文章属性,如发布日期,编辑日期等
- 支持 Markdown,这点对于程序员应该还挺重要的
- 分类和 label 支持
- 移动端支持
- 评论功能,并且有回复的 thread
- 图片上传功能
- 通知功能
- 可能是程序员最熟悉的网站
在这之前也非常多人用 issue 去写作,但是这些都是讨论项目问题的地方,所以一直心里会有点纠结,直到 Discussion 推出。
Svelte
最早了解到 Svelte 是在 The State of JS 2021 了解到,热度,对他的评价只有:生态位和 React 框架重叠,但是编译行为完全自己控制。真的是 Web 开发者的浪漫啊。
既然是那么浪漫的东西,而且吸引我就是几乎绝对的体积优势,在一个周末我突然决定要开始认真了解了。(指看了一下文档就面向 Google 开发,甚至直到今天我文档都没看完
关于 Svelte 就不在赘述了,网上介绍非常多。
技术栈
-
SvelteKit 基本上是 svelte 的第一方支持,提供了 ssr 和 ssg 等开发模式(当然也可以选择 spa 不过态度都是不建议)。
在打包 static 的情况,简单设置就可以实现良好的 seo 支持,和 hydrate 功能,让一个 static site 也有 spa 的感觉。
-
这个几乎没什么好说的,ts 的类型系统可以说是目前最强。
-
我最常用的 css 方案,复用 Utility 规则,以及编译时包含只需要的 css 减少尺寸。
选择它对于我有很多原因
- class 命名令我非常头痛,并且经常写 wrapper,container 实在没什么意义
- 良好的预设 reset css,让我不用操心浏览器之间的差异,字体方面也处理好了
- JIT 支持,在默认提供不满足的情况下也能灵活实现
- 最近支持的 变体,减少了很多时候重复写相同 class 的问题,当然这也不是银弹,这就导致会多一个 css 出来,用的太多的话或许需要优化。
-
Svelte 的 Markdown 预处理器,非常令人惊喜的是他甚至可以直接使用 Svelte 的 component,虽然目前并没有用上,但是 page 和 post 都天然支持了这一点。
比如:
<script> import { Chart } from "../components/Chart.svelte"; </script> # Here’s a chart The chart is rendered inside our MDsveX document. <Chart />
但是它对于编写 md 存在一些限制,就是不能用 4 个空格去缩进代码,这一点对我影响不大。
其实有些问题我还没解决,我希望实现 Hexo
<!-- more -->
的功能,但是目前还没有开始解决。 -
这是一个通过评论组件,数据来源于 Discussion,完美契合 Hakuba。
存在的问题是样式目前和正文并不一致,关于这一点其实大部分评论插件都有,这个我不知道他们怎么忍的,这个未来必须得解决了
实现
预处理数据
构建过程大概是,通过 script 预先分析目前的 Discussion,从中分离出 config,page,和 post。
对于 post 和 page,通过 Discussion 增加 front meta,然后输出对应的 md 文件。
config 数据写入到 .env.local 中,这样只会在构建时就使用了,并且开发过程中也不影响也可以跟 .env 共存。
路由匹配
一般路由匹配只需要对应路径下创建文件即可,但是希望一个路由可以匹配多个路径,这个时候就需要做一些处理。
原本希望实现 /[dynamic] 动态匹配 pageNumber,page 和 post,尽量减少 path 长度,但是后来想想感觉也没必要,并且这会造成一定混乱。
在目前 Hakuba,首页支撑了 labels,posts 功能,又可以分页,那么
- /
- /page/[page]
- /label/[label]
- /label/[label]/page/[page]
全部指向一个页面文件,就需要自定义 Matcher 了。
这一点也是我认为开发体验很差的地方,如果可以直接在 route 里面定义,那么会舒服很多,因此实现大致如下
matcher.ts,包装了一层 path-to-regexp,减少了信息返回。
import { match } from 'path-to-regexp';
export const generateMatcher = <P extends object = Record<string, string>>(param: string) => {
const m = match<P>(param, { decode: decodeURIComponent });
return (path: string) => {
const result = m(path);
if (!result) {
return undefined;
}
return result.params;
};
};
pageMatcher.ts 就是具体实现了,暴露出 matcher,方便之后获取数据。
import type { ParamMatcher } from '@sveltejs/kit';
import { generateMatcher } from '$lib/helper/matcher';
const labelsMatcher = generateMatcher('label/:label{/page/:page(\d+)}?');
const indexMatcher = generateMatcher('{page/:page(\d+)}?');
export const listMatcher = (param: string) => indexMatcher(param) || labelsMatcher(param);
export const match: ParamMatcher = (param) => {
const result = listMatcher(param);
if (result && !result?.page) return true;
if (Number.parseInt(result?.page ?? '') <= 1) return false;
return !!result;
};
rss 支持
使用了 SvelteKit 的 Endpoints 功能,数据塞到 feed 输出即可。
atom.xml.ts 是具体实现文件。
lang 切换
目前 lang 切换比较麻烦,没有提供类似 <svelte:head>
的功能,所以只能使用 hook 的 transformPage
方式来实现。
说是在我认为是比较丑陋的,但是也没办法,毕竟 Svelte 作者 Rich-Harris 都这样说了
mdsvex 的 rehypePlugins
- rehypeSlug 增加 id 到
<h[number]>
标签 - rehypeAutolinkHeadings 增加跳转 link
- addClasses,因为使用了 group-hover,所以需要在
<h[number]>
标签上加上group
class
- addClasses,因为使用了 group-hover,所以需要在
- rehypeExternalLinks 文章链接外部打开
日期处理
Hakuba 文章日期都是 ISO 格式,但是 SSG 在服务器 build,时区和用户显示的不一定相同,所以需要指定时区。
原生 Date 并没有提供这样的功能,实现这方面大多数我会是用 dayjs(以前就是 moment.js),但我不希望再增加一个依赖,所以直接使用了 Date.toLocaleDateString(locals, options)
来实现。
Date.toLocaleDateString(locals, options)
不提供任何参数的情况下是按照当前环境时区来决定,格式化也是,我认为 ISO 格式是相对标准,不过 locals
并不支持 'ISO'
这个参数,这里有一个小技巧就是是用瑞典的格式,locals
为 'sv'
,输出就是 ISO 格式了。
时区方面默认是用 UTC 时区,用户可以配置默认时区,或者在 post 上面增加一个 timezone
front meta,来指定时区。
btw,其实 Date.toLocaleDateString(locals, options)
非常复杂,具体可以在这里查看,里面提到 Unicode BCP 47 Locale Identifiers 复杂到我完全没兴趣去看(
滚动条导致跳动问题
首页设计在桌面上,一般情况下不会有滚动条(除非 label 太多了,到了这个地步估计我会重新改改首页),然后进入到 post 可能出现滚动条导致页面跳动一下,这里只是主要参考了张鑫旭的做法。
这个真的很麻烦,所以特意分享。
Other
剩下就是常规开发了,如果有人感兴趣我再更新本文。
设计
总体设计基于 hexo-theme-cactus,然后自己做减法。
希望 Hakuba 呈现的是一个内容为主的 Blog,甚至文章大纲都没有,我不确定是否需要,但是某程度上来讲不确定,就是不需要。
Hakuba 定位和未来
这个主要还是一个风格鲜明的 Blog 启动器,目前体验还好,但是还有优化的地方,图片的懒加载,占位还没解决,因为 hydrate 太快导致切换有点突兀,也没加转换动画等等。
但是这已经很让我满意了,对比原来的 Blog,资源体积从 700kb 变成 100kb 以下,并且支持 hydrate。
未来有空就会完善一下,近期需要完善一下 readme(已完成。
总结
使用 SvelteKit 开发一个 blog,完全可以把他当成 pug 等模版引擎在使用,Hakuba 甚至没有用到什么 store,context 等高级功能(我甚至没有去看怎么用,只需要简单了解一下 layout 和 matcher 就可以做出来。
但是是不是意味着可以用 Svelte 代替大部分前端开发场景呢?我认为大部分情况下可以,但是也有一些局限性,比如 children,在 React 可以很轻松做到遍历 children,然后 wrap 其他 component,在 svelte 这是与设计相违背的,要做到类似效果会让人感觉非常扭曲。
这或许就是没有 Virtual DOM 的代价,这一点未来其实也可以改善,某些语法上增加 Virtual DOM 的支持?
要是愿意,也可以 fork svelte 支持 Virtual DOM,只是这又有什么必要呢?
不过我还是非常喜欢 svelte,未来前端相关可能都是优先使用,React 还没出来 React forget 之前其实真的有点心智负担重,这长期来讲是不那么健康的,开发者和用户都体验良好,svelte 可能是最佳选择。
BTW
有点懒将以前的文章迁移过来,更改为 https://old.yeungkc.com/