深色模式自动注入 — 技术方案与实现详解

作者:Htryone
日期:2026-06-13
适用场景:静态 HTML 站点需要统一注入深色模式切换脚本,且希望新增/修改的文件能自动完成注入,无需手动执行命令。

本文档完整记录从需求分析、方案选型、代码实现到踩坑解决的完整过程,适合分享给有类似需求的开发者参考。


一、需求背景

1.1 原始问题

有一个纯静态 HTML 站点(Typora 导出 + 手写的 HTML),希望所有页面都支持深色模式切换。

解决方案是写一个 dark-mode-toggle.js,在每个 HTML 的 </body> 前插入一句:

1.2 痛点

痛点描述
文件多手动改每个文件不现实
Typora 覆盖Typora 重新导出 HTML 会覆盖掉已注入的 <script> 标签
容易忘记新增文件容易忘记跑注入脚本

1.3 最终需求(用户原话)

"一个监控脚本,启动监控脚本默认启动一次注入脚本,然后监控文件改动,把没有注入的自动注入,导出的文件可能会覆盖这个也需要重新导入"

拆解后就是三步:


二、环境准备

2.1 Python 版本

本脚本在 Python 3.13 下开发和测试。建议使用 Python 3.10+。

2.2 依赖安装

只需要一个第三方库:

依赖版本用途
watchdog≥ 2.0跨平台文件系统监控

2.3 虚拟环境(推荐)

推荐使用虚拟环境隔离依赖,避免污染全局 Python:

激活后命令行前面会出现 (.venv) 提示符,表示已在虚拟环境中。

2.4 工作区结构要求

脚本通过 __file__ 自动定位工作区,不需要手动配置路径

⚠️ 重要dark-mode-toggle.js 必须放在 scripts/ 目录下,否则注入的 <script src> 路径会 404。

2.5 验证环境


三、整体方案设计

3.1 技术选型

需求选型理由
文件监控watchdogPython 生态最成熟的跨平台文件监控库
注入标记HTML 注释(自定义唯一标记)唯一标记,不误判代码示例
相对路径计算os.path.relpath标准库,处理各层级目录无压力
防抖时间戳字典避免编辑器保存时触发多次 on_modified

3.2 为什么用 HTML 注释做注入标记?

这是本方案最核心的设计决策,直接决定了是否会"死循环"。

错误做法(会导致死循环):

技术文档(本文件)的代码示例里本身就出现了 dark-mode-toggle.js 这个字符串,MARKER in content 会返回 True,导致脚本误判"这个文件已经注入过了",实际上它没有。

正确做法(本方案采用):

这个注释只有注入脚本自己会写,正文里的代码示例不可能包含它,彻底避免误判。

3.3 文件结构


四、核心实现详解

4.1 启动扫描(scan_all

目标:脚本启动时,把所有"还没有注入脚本"的 HTML 文件补注入一次。

关键点inject_file() 函数内部判断"是否已注入",已注入的文件直接跳过,不会重复写入。


4.2 注入逻辑(inject_file

这是最核心的函数,逻辑必须清晰:

设计原则:先判断,再动手。判断是只读操作,不动文件;确认没有才写。


4.3 注入标记(INJECT_TAG

注入后 HTML 文件末尾(`` 前)会多出这两行:

标记命名建议:使用足够特殊的字符串(如包含随机符号或长字符串),确保不会出现在正文内容中。


4.4 相对路径计算

不同层级的 HTML 文件,<script src> 的路径不一样:

HTML 文件位置script src 值
index.html(根目录)scripts/dark-mode-toggle.js
personal/foo.html(一级子目录)../scripts/dark-mode-toggle.js
a/b/c.html(三级子目录)../../scripts/dark-mode-toggle.js

实现:


4.5 在正确位置插入 <script>

规则:</body> 标签之前插入,这样 JS 能操作完整的 DOM。

rfind(从右向左找)而不是 find,防止 HTML 里有多个 `` 注释或字符串时的误判。


4.6 文件监控(watchdog

为什么用 watchdog 而不是轮询?

事件处理

watchdogFileSystemEventHandler 提供这些事件:

事件触发时机本需求是否需要
on_created文件新建✓ 需要
on_modified文件内容修改✓ 需要
on_moved文件移动/重命名✓ 需要(Typora 覆盖可能触发)
on_deleted文件删除不需要

本实现用 on_any_event 统一处理(简化代码),内部再判断文件类型:

为什么要在 on_any_eventsleep(0.8)

很多编辑器(包括 Typora)保存文件的方式是:

  1. 写入临时文件

  2. 删除原文件

  3. 把临时文件重命名为原文件名

这个过程中 on_modified 可能触发时文件还没写完open() 读到的内容是残缺的。加 sleep(0.8) 等文件稳定后再读,是最简单的规避方式。


4.7 防抖(Debounce)

问题:某些编辑器保存一个文件会触发多次 on_modified 事件。

如果不防抖,inject_file() 会被调用多次,虽然幂等(第二次会跳过),但浪费 IO。

解法:用字典记录每个文件最后处理时间,5 秒内只处理一次。


五、完整代码

5.1 watch-dark-mode.py

5.2 start-watch.bat(Windows 双击启动)


六、使用方法

6.1 启动监控

方式一:终端启动

方式二:Windows 双击启动

双击 scripts/start-watch.bat,会弹出终端窗口,监控日志直接显示在里面。

6.2 停止监控

6.3 验证监控是否正常工作

  1. 启动监控

  2. 打开一个未注入的 HTML 文件,手动删掉注入标记和 <script> 标签

  3. 保存文件

  4. 观察监控窗口,应该看到类似输出:

  5. 再次保存同一文件,应该不再看到 [注入] 输出(防抖 + 标记判断生效)


七、踩坑记录(重要)

坑 1:MARKER in content 误判代码示例

现象apply-dark-mode.py 运行时,部分已注入的文件被判定为"未注入",导致重复注入;或者反过来,未注入的文件被判定为"已注入",跳过不处理。

原因has_script() 最初的实现是简单字符串匹配:

技术文档(本文件)的代码示例里本身就出现了 dark-mode-toggle.js 这个字符串,MARKER in content 会返回 True,导致脚本误判"这个文件已经注入过了",实际上它没有。

正确做法:用唯一 HTML 注释标记判断,不解析 <script> 标签:


坑 2:inject_file() 里"判断"和"清理"耦合

错误写法

如果 remove_old_script 有 bug,这里就会误判。

正确做法:先判断,再动手,两个步骤完全独立:


坑 3:Windows 上 watchdogon_moved 事件

Typora 导出 HTML 时,实际行为是「写临时文件 → 替换原文件」,watchdog 在 Windows 上可能触发 on_moved 而不是 on_modified

所以 on_any_event 里要同时处理 src_pathdest_path


坑 4:监控脚本本身也会触发文件修改事件

现象:启动监控后,脚本注入了文件,文件修改触发 on_modified,脚本又尝试注入,死循环。

原因inject_file() 写入文件后,watchdog 捕获到 on_modified 事件,又调用 inject_file()

正确做法

  1. 注入标记写在文件里,inject_file() 第一行就是检查标记,已有标记直接返回 False

  2. 防抖机制:5 秒内同一文件只处理一次

  3. 两个机制双重保险,彻底避免死循环


坑 5:重置仓库时要注意运行中的 Python 进程

现象git reset --hard 后,git status 仍然显示文件被修改。

原因watch-dark-mode.py 还在运行,它监控到文件没有注入标记,又注入了一次。

正确做法:重置仓库前,先关掉所有 Python 进程:


八、扩展方向

需求实现思路
开机自启,不需要每次手动运行taskschd(Windows 任务计划)或写成 Windows 服务
支持更多文件类型(如 .php修改 _is_target() 里的后缀判断
注入位置可配置(<head></body> 前)inject_file() 加参数
多 JS 脚本注入SCRIPT_FILE 改成列表,循环注入
推送前自动注入(不常驻监控)用 Git Hook(pre-commitpre-push

九、总结

目标方案
所有 HTML 支持深色模式注入 dark-mode-toggle.js
新增文件自动注入watchdog 监控 on_created
覆盖导出自动重新注入watchdog 监控 on_modified / on_moved
避免死循环唯一注入标记 + 防抖
避免误判代码示例用 HTML 注释标记,不解析 <script> 标签

核心经验:判断"是否已注入"的逻辑必须 100% 可靠,这是整个方案的基础。用唯一标记(HTML 注释)比解析 <script> 标签简单且可靠得多。


文档整理自真实开发过程,包含需求分析、实现细节和踩坑记录。欢迎分享给有类似需求的开发者参考。