﻿---
title: 为Hexo添加热更新
date: 2026-04-08
excerpt: 通过一个轻量脚本，为 Hexo 本地开发服务器增加文章保存后自动刷新浏览器的能力。
tags:
  - Blog
  - Hexo
  - JavaScript
  - Workflow
---

`{sh} hexo server` 虽然原生支持监听文件变化并重新生成页面，但遗憾的是它不会主动通知浏览器刷新。每次修改完文章，我们还是得手动切回浏览器按下 <kbd>F5</kbd> or <kbd>⌘ + R</kbd>。

之前用过 Astro，它能做到这一点。既然是可行的，于是就让 CodeX 输出脚本：用户更新 `.md` -> Hexo 重新生成页面 -> 浏览器会自动刷新当前 `http://localhost:4000` 页面。

虽然没系统学过前端，不过之前用Astro、Vite 等前端工具时，也体验过“热更新”这个 feature，大概能确认这个需求是可行的，于是就让CodeX 帮忙写一个脚本，实现：**用户更新 .md -> Hexo 重新生成页面 -> 浏览器自动刷新。**

## Experiences：相见恨晚

引入脚本后，现在也可以电脑分屏，右边放  修改 `md` 文件 ，左边放 Edge/Chrome/Zen/Safari 看实时预览。体验方面的提升可以这样概括：

1. 最直接的，不需要手动刷新。过去由于 Blog 不能在浏览器热更新，通常就凑合着用编辑器内置的预览
2. 在浏览器中预览，是真正的、开箱即用的**所见即所得**

### 所见即所得：Browser Or Editors?

关于所见即所得这点，现在主流编辑器都内置Markdown预览，但效果差强人意。Typora 比较美观，但编辑体验略逊于 Obsidian/VSCode/NeoVim/Zed。

另外，为了追求更好的 [UI/UX](/tags/UIUX)，我经常会在 Markdown 里缝合一些 **HTML 代码**。大多数编辑器的预览引擎在处理这些自定义标签时都会“翻车”，无法正确渲染，当然这不是编辑器本身的缺点。

列出一些无法正确渲染的情况：

- Shiki 代码高亮的 Transformers： 目前在 Obsidian 中使用插件可部分渲染，但语法采是基于Expressive Code 的标准
- 部分markdown-it 插件：其中比较流行的 callout/math/mermaid 在 Obsidian/Typora 均能预览，但是相对小众但却很实用的 tab 组件却不能。此外 Obsidian 的双链也是如此
- 自定义的 HTML Elements(Web Components): 自定义 HTML Elements 的用法可以见[[custom_elements|这篇文章]]；本站所自定义的组件陈列在[[test_components|该页面]]
- Markdown 内嵌入的 CSS 样式同样不能预览

## Appendix: Source Code

Hexo 会自动加载项目根目录/主题目录下 `scripts/` 里的脚本。这里选择在主题目录下的 `/scripts` 目录中新建 [` scripts/hot-reload.js `](https://github.com/Efterklang/gnix-blog-theme/blob/main/scripts/hot-reload.js)

>  考虑到 GitHub 的代码阅读体验更好，更新更及时，这里不贴代码

## Appendix: How it Work

既然是 vibe，那么实现原理自然是一概不知，能用即可🤓

直接让 AI 给我一个不饶、不躲的解释，~~需要吗？~~

BTW，输出进行了适当调整/删减；这里实际问得是 AI Studio 中的 Gemini，不过它的 Logo 太丑了，暂且换成 OpenAI 的 Logo 吧

<script data-swup-reload-script type="module" src="/js/components/chat.js"></script>
<x-chat>
  <chat-message name="GnixAij" avatar="https://assets.vluv.space/avatar.webp" is-me>
    解释一下工作原理，使用的技术
  </chat-message>
  <chat-message name="ChatGPT" avatar="https://cdn.svglogos.dev/logos/openai-icon.svg">

<div class="chat-heading">核心技术：SSE (Server-Sent Events)</div>

脚本的核心使用了 **SSE (服务器发送事件)**。这是一种允许服务器向浏览器推送实时通知的技术。

<div class="chat-heading">实现流程</div>

1. **环境检测 (`isHexoServerCommand`)**

   脚本会首先判断当前是否运行在 `hexo server` 模式下。这样可以确保在执行 `hexo generate` 静态构建或部署到生产环境时，不会注入这些开发专用的代码。

2. **监听 Hexo 生命周期**

   - `hexo.on('generateAfter')`：监听 Hexo 页面重新生成完成。每次 Hexo 完成页面生成后，脚本就会触发刷新。由于 Hexo server 会在文件变化后自动触发重新生成，所以只需要监听这一个事件即可。

3. **防抖与冷却期 (Debounce & Cooldown)**

   为了防止编辑器频繁保存（如自动保存功能）导致浏览器疯狂刷新，脚本加入了 `150 ms` 的防抖和 `500 ms` 的冷却间隔，确保刷新动作既及时又稳定。

4. **中间件注入 (`server_middleware`)**

   脚本在 Hexo 内置服务器中开辟了一个特殊的"暗号"接口：`/__hexo_live_reload`。浏览器会一直连接这个接口，等待服务器发令。

5. **前端注入 (`injector`)**

   通过 Hexo 的 `injector` 功能，脚本会自动在每个页面的 `<body>` 末尾插入一段极其微小的 JavaScript。这段代码负责监听来自服务器的 `reload` 信号，并在接收到信号后调用 `window.location.reload()`。

  </chat-message>
  <chat-message name="GnixAij" avatar="https://assets.vluv.space/avatar.webp" is-me>

`window.location.reload` 是？ `__hexo_live_reload` 是？

  </chat-message>
  <chat-message name="ChatGPT" avatar="https://cdn.svglogos.dev/logos/openai-icon.svg">

<div class="chat-heading">1. <code>window.location.reload()</code> 是什么？</div>

这是浏览器原生 JavaScript 提供的一个方法，它的作用非常简单直接：**刷新当前页面**。

- **效果：** 相当于你在键盘上按下了 <kbd>F5</kbd> 键，或者点击了浏览器地址栏旁边的"刷新"按钮。
- **在脚本中的作用：** 当浏览器接收到服务器传来的"文件已更新"的消息时，它会执行这行代码。这样你就不用手动去点刷新，页面会自动变到最新的状态。

<div class="chat-heading">2. <code>/__hexo_live_reload</code> 是什么？</div>

这是一个**虚拟的通信地址（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')`。这行代码的意思是让浏览器一直"监听"这个地址，就像在等一个电话。

---

<div class="chat-heading">总结：两者是如何配合的？</div>

1. **连接：** 浏览器打开你的博客，自动连接到 `/__hexo_live_reload` 管道。
2. **等待：** 浏览器在管道另一头等着，无事发生。
3. **触发：** 你修改了 `.md` 文件，Hexo 重新生成了页面。
4. **发令：** Hexo 服务器往 `/__hexo_live_reload` 管道里塞入一条消息：`{"type": "reload"}`。
5. **动作：** 浏览器在管道这头收到了消息，立刻执行 `window.location.reload()`。
6. **结果：** 页面变新了。

  </chat-message>
</x-chat>
