为Hexo添加热更新
hexo server 虽然原生支持监听文件变化并重新生成页面,但遗憾的是它不会主动通知浏览器刷新。每次修改完文章,我们还是得手动切回浏览器按下 F5 or ⌘ + R。
之前用过 Astro,它能做到这一点。既然是可行的,于是就让 CodeX 输出脚本:用户更新 .md -> Hexo 重新生成页面 -> 浏览器会自动刷新当前 http://localhost:4000 页面。
虽然没系统学过前端,不过之前用Astro、Vite 等前端工具时,也体验过“热更新”这个 feature,大概能确认这个需求是可行的,于是就让CodeX 帮忙写一个脚本,实现:用户更新 .md -> Hexo 重新生成页面 -> 浏览器自动刷新。
Experiences:相见恨晚
引入脚本后,现在也可以电脑分屏,右边放 修改 md 文件 ,左边放 Edge/Chrome/Zen/Safari 看实时预览。体验方面的提升可以这样概括:
- 最直接的,不需要手动刷新。过去由于 Blog 不能在浏览器热更新,通常就凑合着用编辑器内置的预览
- 在浏览器中预览,是真正的、开箱即用的所见即所得
所见即所得:Browser Or Editors?
关于所见即所得这点,现在主流编辑器都内置Markdown预览,但效果差强人意。Typora 比较美观,但编辑体验略逊于 Obsidian/VSCode/NeoVim/Zed。
另外,为了追求更好的 UI/UX,我经常会在 Markdown 里缝合一些 HTML 代码。大多数编辑器的预览引擎在处理这些自定义标签时都会“翻车”,无法正确渲染,当然这不是编辑器本身的缺点。
列出一些无法正确渲染的情况:
- Shiki 代码高亮的 Transformers: 目前在 Obsidian 中使用插件可部分渲染,但语法采是基于Expressive Code 的标准
- 部分markdown-it 插件:其中比较流行的 callout/math/mermaid 在 Obsidian/Typora 均能预览,但是相对小众但却很实用的 tab 组件却不能。此外 Obsidian 的双链也是如此
- 自定义的 HTML Elements(Web Components): 自定义 HTML Elements 的用法可以见这篇文章;本站所自定义的组件陈列在该页面
- Markdown 内嵌入的 CSS 样式同样不能预览
Appendix: Source Code
Hexo 会自动加载项目根目录/主题目录下 scripts/ 里的脚本。这里选择在主题目录下的 /scripts 目录中新建 scripts/hot-reload.js
考虑到 GitHub 的代码阅读体验更好,更新更及时,这里不贴代码
Appendix: How it Work
既然是 vibe,那么实现原理自然是一概不知,能用即可🤓
直接让 AI 给我一个不饶、不躲的解释,需要吗?
BTW,输出进行了适当调整/删减;这里实际问得是 AI Studio 中的 Gemini,不过它的 Logo 太丑了,暂且换成 OpenAI 的 Logo 吧
核心技术:SSE (Server-Sent Events)
脚本的核心使用了 SSE (服务器发送事件)。这是一种允许服务器向浏览器推送实时通知的技术。
实现流程
环境检测 (
isHexoServerCommand)脚本会首先判断当前是否运行在
hexo server模式下。这样可以确保在执行hexo generate静态构建或部署到生产环境时,不会注入这些开发专用的代码。监听 Hexo 生命周期
hexo.on('generateAfter'):监听 Hexo 页面重新生成完成。每次 Hexo 完成页面生成后,脚本就会触发刷新。由于 Hexo server 会在文件变化后自动触发重新生成,所以只需要监听这一个事件即可。
防抖与冷却期 (Debounce & Cooldown)
为了防止编辑器频繁保存(如自动保存功能)导致浏览器疯狂刷新,脚本加入了
150 ms的防抖和500 ms的冷却间隔,确保刷新动作既及时又稳定。中间件注入 (
server_middleware)脚本在 Hexo 内置服务器中开辟了一个特殊的"暗号"接口:
/__hexo_live_reload。浏览器会一直连接这个接口,等待服务器发令。前端注入 (
injector)通过 Hexo 的
injector功能,脚本会自动在每个页面的<body>末尾插入一段极其微小的 JavaScript。这段代码负责监听来自服务器的reload信号,并在接收到信号后调用window.location.reload()。
1. window.location.reload() 是什么?
这是浏览器原生 JavaScript 提供的一个方法,它的作用非常简单直接:刷新当前页面。
- 效果: 相当于你在键盘上按下了 F5 键,或者点击了浏览器地址栏旁边的"刷新"按钮。
- 在脚本中的作用: 当浏览器接收到服务器传来的"文件已更新"的消息时,它会执行这行代码。这样你就不用手动去点刷新,页面会自动变到最新的状态。
2. /__hexo_live_reload 是什么?
这是一个虚拟的通信地址(Endpoint)。
在正常的 Hexo 预览中,你访问 http://localhost:4000 会看到你的博客。但这个脚本在后台偷偷开辟了一个隐藏的"专线":http://localhost:4000/__hexo_live_reload。
它的工作原理如下:
它是"管道": 这个地址并不存在于你的磁盘上,它是通过 Node.js 脚本拦截请求而创建的一个长连接通道(基于 SSE 技术)。
服务器端(Hexo): 当你在
scripts/live-reload.js中看到req.url === LIVE_RELOAD_PATH时,就是在告诉 Hexo:“如果有浏览器请求这个地址,不要给它网页内容,而是保持连接不断开,随时准备发信号。”客户端(浏览器): 脚本在页面里写了
new EventSource('/__hexo_live_reload')。这行代码的意思是让浏览器一直"监听"这个地址,就像在等一个电话。
总结:两者是如何配合的?
- 连接: 浏览器打开你的博客,自动连接到
/__hexo_live_reload管道。 - 等待: 浏览器在管道另一头等着,无事发生。
- 触发: 你修改了
.md文件,Hexo 重新生成了页面。 - 发令: Hexo 服务器往
/__hexo_live_reload管道里塞入一条消息:{"type": "reload"}。 - 动作: 浏览器在管道这头收到了消息,立刻执行
window.location.reload()。 - 结果: 页面变新了。
为Hexo添加热更新