好久没写博客了,每当我有些想写代码的情绪无处发泄的时候,我就会开始折腾之前写的老项目,而我的博客又是一个稍微有一点复杂的项目(Hugo + SolidStart),项目的复杂度一旦升高,难免会在不同需求之间做权衡,难以做到尽善尽美,所以博客一般是我折腾次数最多的。这次折腾的起因是我发现 SolidStart 在前段时间发布了 1.0 版本,相比之前的 beta 版本有不少改动,借着更新博客的机会,正好水一篇文章:)
除开 URL 路径终于支持 UTF-8 字符外,本次服务端渲染方式相比之前变化是最大的,花了点时间做了相关的适配。
目前博客改成了使用 Hugo 输出 JSX 格式的内容文件,本质上还是 JS 的 Object,只是在内容开头加上了 export default
,这样 SolidStart 就会根据文件的 [.jsx] 后缀把它当成页面去渲染,而不需要我再指定文件后缀去渲染页面;然后我再通过自定义的 Vite 插件把 JSON 内容都渲染成 JSX 内容。这样的构建流程会相比之前的更直观一点,也方便排查问题。
SolidStart 1.0 版本页面的加载顺序变成和 RemixJS、NextJS 一样了,即页面所有的 JS 文件,无论是动态导入还是静态导入都会同时加载。在之前的版本,有一些动态导入的文件,要等它的依赖项(比如 entry-client.js)全部加载完成之后才会发出请求。
但是现在版本打包的静态资源大小会相比之前更大一些,主要是包含了完整的 vite manifest(这一点我不太喜欢,因为我阅读源码之后发现是可以避免的),而且这个 manifest 映射是直接插入在 HTML 文件的 script 标签里的!虽说 gzip 后也就几 KB 的差距,但是会让 DOMContentLoaded 的耗时加长,等看后续 SolidStart 版本更新看看是否会有改进。
不过我手动测试了好几次 DOMContentLoaded 和 Load 的耗时,测试结果上来看和之前倒没啥差别,终究还是我有点强迫症了……
我博客目前的英文文章是通过 URL 来区分的,而 SolidStart 现在只有在 Route
内部才能使用 useLocation
判断当前渲染的页面是什么,这让我有点犯难:
Route
内部,而应该和作为 Route
同级的元素;Route
内部使用在服务端渲染的时候会报错;所以我不得不使用了一个有点别扭的办法:通过渲染页面的数量来判断当前的页面是否是英文页面。但是这也有一个前提,就是需要确保所有英文页面是先被渲染的。好在 SolidStart 的页面预渲染顺序可以通过 prerender.routes
来指定,所以我目前是把英文的页面以及自定义 404 的页面在该参数里指定,然后剩余的页面让程序构建时自动爬取即可。
在上次更新博客时,我就去掉了标签云页面改成了在页面底部随机展示其中一部分标签。而我最近在查看博客统计时,发现标签详情页面的访问数还是少得可怜——近半年来,超过 10 次访问的标签页面都寥寥无几。
因此这次更新我就直接把标签详情页面去掉了,底部的随机标签展示部分也去掉了,文章详情页的标签倒是还留着,不过点进去已经不是标签详情页面了,而是 搜索页面 根据标签过滤的结果。我很满意搜索功能的实现,但是发现搜索页面的访问量还是很低,干脆给它引流一下。
Service Worker 最广泛的应用应该是根据不同规则缓存页面的静态资源,以便在不发生 HTTP 请求的情况下返回缓存的资源,来提高网页的性能。
这个骚操作就用到了 Service Worker 最关键的两点功能:拦截和返回。具体来说,Service Worker 会拦截本站除图片外所有的静态资源的 HTTP 请求,然后在 Service Worker 内部根据请求的资源名同时去不同的 CDN 取数据,其中有任意一个 CDN 节点先返回完整的资源即代表本次请求成功。同时把其他正在等待返回的请求 abort。如果不 abort 的话,当同时请求的资源比较多的时候,浏览器可能会限制住后续的请求发送。
Chrome 是限制同一个 tab 每个 host 只能最多有 6 个 tcp 链接。
以下是代码实现:
const ASSETS_PREFIXES = [
`CDN_HOST_1`,
`CDN_HOST_2`,
`CDN_HOST_3`,
``
]
const fetchAsset = (url: string, signal: AbortSignal) => {
return new Promise((resolve, reject) => {
fetch(url, { signal })
.then(async res => res.ok ? resolve(res) : reject())
.catch(() => reject())
})
}
const catchAssets = async (pathname: string) => {
const controller = new AbortController(),
signal = controller.signal;
return Promise.any(ASSETS_PREFIXES.map(prefix => fetchAsset(`${prefix}${pathname}`, signal)))
.then(async res => {
const body = await res.text();
controller.abort();
return { headers: res.headers, body: body, status: res.status }
})
.catch(err => console.log(err))
}
registerRoute(({ request }) => (request.destination === 'script' || request.destination === 'style'),
async ({ event }) => {
const parsedUrl = new URL(event.request.url);
const { body, ...rest } = await catchAssets(parsedUrl.pathname)
return new Response(body, rest)
}
);
这个功能之前在饿了么的 NPM 镜像网站还没有下架的时候挺好用的,因为饿了么用的是阿里云的 CDN,在国内的访问速度非常快,后来饿了么的镜像失效后,其他的一些公益镜像速度就不太行了。也导致了我也一直没写文章介绍这个功能,感觉确实在如今有些鸡肋了。
标题和正文的字体家族相较之前做了交换,标题改成了使用衬线体而正文改成了使用非衬线体,这样在文章详情的页面会显得没有那么锐利,阅读长文的时候眼睛不会那么累。
目前博客首页加载的必要静态资源(HTML、CSS,JS,不含图片)只有 65 KB!请求也只有 12 个!强迫症表示很满意。