MoreRSS

site iconHCLonely修改

爱好: JavaScript, 动漫,游戏,小说
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

HCLonely的 RSS 预览

TrayPilot:隐藏 Windows 托盘图标,让托盘清爽起来

2026-09-18 11:17:18

同步工具、截图软件、硬件管理程序、各种后台助手……随着常用软件越来越多,Windows 任务栏右下角的托盘也容易变得拥挤。有些图标每天都要点击,有些几乎不会用到,却一直占着位置。

把图标收进折叠菜单后,它们仍然留在那里。对于只想让软件安静运行的人来说,或许还需要更进一步:隐藏不需要的托盘图标,同时保留软件的后台运行。

这就是 TrayPilot 想解决的问题。

它是一款面向 Windows 11 的托盘图标管理工具,支持单图标控制、自动隐藏规则、批量操作和全局快捷键。你可以决定哪些图标留在眼前,哪些暂时退到幕后。

TrayPilot 操作演示

隐藏图标,后台工作照常继续

TrayPilot 的隐藏操作会让目标图标从任务栏及折叠菜单中消失,不会关闭目标软件。

例如,对于能够被 TrayPilot 识别和控制的同步工具,你可以隐藏它的托盘图标,让它继续在后台工作;需要打开菜单或查看状态时,再恢复图标即可。

双击就能管理,还能只隐藏其中一个图标

打开 TrayPilot 后,可以通过列表或网格查看识别到的托盘图标。日常操作很直接:

  • 双击图标或所在行,切换当前图标的显示与隐藏。
  • 按住 Ctrl 或 Shift 多选,批量隐藏或恢复。
  • 输入软件名称、进程或路径,快速找到目标。
  • 悬停或打开属性,查看进程、PID、完整路径及图标标识等信息。

有些程序会创建多个托盘图标,而你可能只想保留其中一个。TrayPilot 支持分别控制这些图标,双击只影响当前选中的图标;需要统一处理时,也可以通过右键菜单中的 此程序的所有图标 一次操作。

隐藏后的项目会以淡化样式留在管理界面里,方便找到并恢复。

设置一次规则,让常驻图标自动隐藏

如果某个图标长期不需要显示,可以右键选择 仅自动隐藏此图标。之后,TrayPilot 会根据保存的规则自动处理匹配的图标。

规则提供两种范围:

规则范围 适合的需求
单图标规则 同一程序有多个图标,只想隐藏其中一个
整程序规则 希望隐藏指定程序的所有托盘图标

整程序规则按可执行文件的完整路径匹配;单图标规则还会结合图标标识,在标识保持不变时可继续匹配重启后的进程。软件更新后若改变了图标标识,则需要重新添加对应规则。

规则和临时操作也可以配合使用:手动恢复图标不会删除规则。单独恢复一个命中规则的图标后,本次运行期间会暂时跳过该图标的自动隐藏,方便你使用它的菜单。需要重新执行规则时,使用 隐藏规则命中 即可。

平时留在托盘,需要时随手唤回

TrayPilot 默认在关闭窗口后继续驻留托盘,让未暂停的自动隐藏规则保持运行。右键点击它的托盘图标,就能打开快捷管理菜单,直接切换已识别软件的图标状态,无需每次打开主窗口。

如果你习惯键盘操作,可以在设置中启用以下全局快捷键,并按自己的习惯修改组合:

功能 预置组合
打开主界面 Ctrl + Alt + M
显示所有 Ctrl + Alt + S
隐藏规则命中 Ctrl + Alt + H

快捷键默认不启用,需要在设置中分别开启并保存。 “显示所有”会保留规则并暂停自动隐藏,方便临时找回图标;“隐藏规则命中”则会重新应用规则并恢复自动隐藏。

此外,TrayPilot 还提供浅色、深色及跟随系统主题,支持简体中文和 English。开机启动可以按需开启,让它在当前用户登录 Windows 后自动运行。

如果连 TrayPilot 自己的托盘图标也想隐藏,同样可以设置。之后再次运行程序,就能唤回已有主窗口。

想恢复原状,也有明确入口

整理托盘时,恢复入口同样重要。

主界面的 全部恢复 可以恢复本工具隐藏的图标,同时保留已保存的规则。如果恢复失败,会保留恢复记录并取消退出,便于重试。

程序会在隐藏前保存恢复记录,并在下次启动时尝试恢复。若遇到强制结束,可以重新打开 TrayPilot 后点击 全部恢复,或重新启动对应软件,让它重新创建图标。

TrayPilot 不修改其他软件的配置,也不写入 Windows 托盘的 NotifyIconSettings 设置。

如何开始使用

  1. 前往 TrayPilot 项目主页,查看程序包与使用说明。
  2. 使用 Windows x64 程序包时,解压后运行 TrayPilot.exe。
  3. 找到一个不常用的图标,双击试试隐藏和恢复。
  4. 确认适合自己的使用习惯后,再添加自动隐藏规则,按需开启快捷键和开机启动。

如果你喜欢自己编译,项目使用 C#、Windows Forms 和 Win32 API,源码及 .NET 构建命令均可在仓库中查看。

目前,TrayPilot 是面向 Windows 11 25H2 的原型项目。其他系统版本、未来 Windows 补丁及特殊软件的兼容性仍需实际验证;音量、网络、电池、时钟等系统内建控件也在管理范围内。

把常用的留下,让其余图标安静退场

如果你也希望后台软件继续工作,又不想让它们的图标挤满托盘,可以试试 TrayPilot。从隐藏一个很少点击的图标开始,逐步整理出适合自己的托盘布局。

欢迎到 GitHub 项目主页 查看源码、点个 Star,或通过 Issues 分享使用反馈。反馈兼容性问题时,附上 Windows 版本、目标软件及具体操作,会更便于定位。

项目地址:HCLonely/TrayPilot
详细说明:中文使用文档

记一次Alt-Tab导致资源管理器崩溃(0xc0000005)原因排查

2026-08-05 11:12:36

先说结论

这次 explorer.exe 反复崩溃,最终确认是 QQ 输入法引发的问题。

最初怀疑过 Breeze Shell、Windhawk、Start11、zspace 的 MountImageThumb 外壳扩展,甚至怀疑近期 Windows 更新,但逐项禁用后崩溃依旧。完整转储最终把故障范围收敛到了 Windows 文本服务框架(TSF):崩溃代码所在的动态内存中包含 TF_ThreadMgr,调用链也经过 user32.dll、combase.dll、twinui.pcshell.dll 和 SHCore.dll。

在停用 QQ 输入法、改用微软输入法后,资源管理器不再崩溃。结合转储证据和 A/B 对照结果,可以确定 QQ 输入法是本次问题的触发因素。

值得注意的是:崩溃转储中没有出现 QQ 输入法 DLL 的常规加载记录。因此,如果只看“故障模块”或 Explorer 已加载模块,很容易把它误判成 Windows 自身的 Shell 崩溃。


问题表现

资源管理器会在没有明显固定操作的情况下突然退出并自动重启。Windows 错误报告中的主要特征基本保持不变:

程序:explorer.exe事件类型:BEX64异常代码:0xc0000005异常参数:8(执行访问冲突)故障签名:StackHash_76a1故障地址偏移:始终以 0341 结尾

0xc0000005 并不只代表普通的内存读写越界。异常参数为 8 时,表示处理器试图执行一个无效或不可执行的地址,也就是 execute access violation。

由于故障模块只显示 StackHash,事件查看器无法直接指出真正的触发组件,必须获取完整进程转储继续分析。

第一阶段:排查 Explorer 外壳扩展

Explorer 崩溃最常见的原因之一,是第三方软件把 DLL 注入资源管理器进程,例如右键菜单、缩略图、桌面美化、任务栏修改和文件预览扩展。

当时机器上存在多种可能影响 Explorer 的软件,因此先后进行了以下排除:

  1. 关闭 Breeze Shell 注入。
  2. 退出并禁用 Windhawk。
  3. 关闭 CLaunch。
  4. 禁用 Start11 相关组件。
  5. 禁用 zspace 的 MountImageThumb 外壳扩展。
  6. 结束 Explorer,并启动一个全新的 explorer.exe 实例。

zspace 扩展对应的 CLSID 为:

{11E1057D-8AE0-4D09-AEF5-21EEB962D2BE}

这里有一个容易踩坑的地方:禁用扩展不代表它会立刻从现有 Explorer 进程中卸载。旧进程可能仍保留已经载入的 DLL,所以每次修改后都应彻底重启 Explorer,最好直接重启电脑。

然而,在新启动的 Explorer 中已经看不到非 Windows DLL 后,崩溃仍然出现,而且异常代码和 ...0341 偏移完全相同。这说明继续盲目禁用传统外壳扩展意义不大。

第二阶段:检查系统更新和系统文件

崩溃开始时间与近期 Windows 更新比较接近,因此一度怀疑是 Shell 组件版本变化或系统文件损坏。

常规修复命令如下,需要在管理员终端中运行:

DISM /Online /Cleanup-Image /RestoreHealthsfc /scannow

这一步仍然值得执行,但它只能验证和修复 Windows 组件,不能证明第三方输入组件没有参与故障。因此,在没有转储证据前直接卸载更新,风险较大,也容易走错方向。

第三阶段:为 Explorer 开启完整转储

为了保留线程、模块和动态内存,需要给 explorer.exe 配置完整用户态转储。可在管理员 PowerShell 中执行:

$dumpKey = 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\explorer.exe'New-Item -Path $dumpKey -Force | Out-NullNew-ItemProperty -Path $dumpKey -Name DumpFolder -PropertyType ExpandString `    -Value '%LOCALAPPDATA%\CrashDumps' -Force | Out-NullNew-ItemProperty -Path $dumpKey -Name DumpCount -PropertyType DWord `    -Value 10 -Force | Out-NullNew-ItemProperty -Path $dumpKey -Name DumpType -PropertyType DWord `    -Value 2 -Force | Out-Null

其中:

  • DumpType = 2 表示完整转储;
  • DumpCount = 10 表示最多保留 10 份;
  • 转储默认生成在 %LOCALAPPDATA%\CrashDumps。

下一次崩溃后,得到了约 837 MB 的完整转储:

explorer.exe.<PID>.dmp

第四阶段:WinDbg 定位真正的故障点

使用 WinDbg/CDB 和微软符号服务器分析转储,核心结果如下:

ExceptionCode: c0000005ExceptionInformation[0]: 8ExceptionAddress: 00000000`0ca40341Failure.Bucket:SOFTWARE_NX_FAULT_INVALID_POINTER_EXECUTE_c0000005_SHCore.dll!_WrapperThreadProc

故障线程的指令指针为:

RIP = 0x0ca40341

这个地址不属于任何已加载的 EXE 或 DLL,而是位于一块 4 KB 的私有动态内存中:

Base address : 0x0ca40000Region size  : 0x1000Type         : MEM_PRIVATEProtection   : PAGE_EXECUTE_READWRITE

也就是说,Explorer 不是在普通模块代码里崩溃,而是在一段运行时生成或注入的代码中发生了无效执行。

故障位置附近的反汇编如下:

0ca40336  mov  rax, qword ptr [rcx]0ca40339  lea  rdx, [rsp+50h]0ca4033e  call qword ptr [rax+18h]0ca40341  test eax, eax    ; 返回到这里时发生执行访问冲突

代码正在通过 COM 虚函数表调用对象方法。原始调用栈已经因动态代码而不完整,但扫描栈地址后仍能恢复出大致路径:

user32.dll  → win32u.dll / ntdll.dll  → combase.dll  → twinui.pcshell.dll  → SHCore.dll!_WrapperThreadProc

这条路径与 Explorer 的窗口消息、Shell UI、COM 线程和文本输入服务密切相关。

最关键的线索:TF_ThreadMgr

进一步检查那块私有动态内存,发现其中嵌入了以下 GUID:

{529A9E6B-6587-4F23-AB9E-9C7D683E3C50}

查询注册表后得到:

HKEY_CLASSES_ROOT\CLSID\{529A9E6B-6587-4F23-AB9E-9C7D683E3C50}    (Default) = TF_ThreadMgrInProcServer32    %SystemRoot%\System32\msctf.dll

TF_ThreadMgr 是 Windows Text Services Framework 的线程管理器。输入法、文字服务和应用程序之间的交互都要经过这套框架。

至此,排查方向从“文件缩略图和右键菜单扩展”转向了“输入法/文本服务”。

为什么转储里没有直接显示 QQ 输入法

这份转储的已加载和已卸载模块列表中都没有非微软 DLL,因此 WinDbg 无法直接给出类似下面的结论:

Faulting module: 某个 QQ 输入法 DLL

但这并不能排除输入法。

输入法和文本服务并不一定要以一个长期驻留、容易被模块列表识别的 DLL 出现在故障线程中。它可能通过 TSF、COM、窗口消息、进程外组件或短生命周期的动态代码参与调用。转储能够证明的是:

  • 崩溃发生在私有动态执行页,而不是普通系统 DLL 的固定代码段;
  • 该动态页明确引用了 TF_ThreadMgr;
  • 上层路径经过 Windows Shell、COM 和窗口消息系统;
  • 传统 Explorer 外壳扩展已经被逐项排除。

因此,转储给出了“文本服务路径异常”的方向,但最终归因仍需要实际 A/B 测试。

最终验证

将 QQ 输入法停用,改用微软拼音或微软英文键盘,并彻底重启电脑后继续观察,Explorer 不再复现相同崩溃。

对照关系如下:

测试条件 结果
禁用 Breeze Shell 仍然崩溃
禁用 Windhawk、Start11 等工具 仍然崩溃
禁用 zspace MountImageThumb 仍然崩溃
新 Explorer 中无第三方 DLL 仍然崩溃
停用 QQ 输入法,改用微软输入法 不再崩溃

这组结果与转储中的 TF_ThreadMgr 线索相互印证,最终确认 QQ 输入法是触发因素。

严格来说,转储证明了故障发生在 TSF/COM 相关的动态代码路径;“QQ 输入法是根因”则由停用前后可重复的 A/B 结果确认。二者结合,比单纯看到 SHCore.dll 就判断为 Windows Bug 更可靠。

处理办法

如果遇到相似问题,可以按以下顺序处理:

  1. 将默认输入法临时切换为微软拼音或微软英文键盘。
  2. 从语言和输入设置中删除或停用第三方输入法。
  3. 在任务管理器中结束第三方输入法相关后台进程。
  4. 完整重启电脑,避免旧 Explorer 和文本服务进程继续保留状态。
  5. 更新或重新安装第三方输入法;如果重新安装后复现,就继续使用其他输入法并向厂商反馈转储特征。

如果停用输入法后仍然崩溃,再继续执行 DISM/SFC、干净启动和 Windows 更新回退等系统级排查。

这次排查带来的经验

1. “故障模块是系统 DLL”不代表系统 DLL 是根因

SHCore.dll 只是承载线程或接住异常的位置。真正的异常指令位于私有动态内存,直接把锅归给 SHCore.dll 会误导排查。

2. 禁用外壳扩展后必须启动全新的 Explorer

扩展已经载入后,修改注册表并不会让旧进程立刻卸载 DLL。若没有重启进程,测试结果可能完全无效。

3. 完整转储比事件查看器的 StackHash 有价值

事件查看器只能看到 BEX64、StackHash 和异常码。完整转储则保留了动态内存、寄存器和线程上下文,最终正是内存里的 TF_ThreadMgr GUID 改变了调查方向。

4. 转储负责缩小范围,A/B 测试负责最终归因

调试器通常只能告诉我们“在哪里、以什么方式崩溃”。要确认具体软件,还要逐项停用并确保测试条件真正生效。

总结

这次问题表面上是一个典型的 Explorer BEX64 / StackHash 崩溃,最初也很像第三方外壳扩展或近期 Windows 更新导致。但完整转储显示,故障发生在一段与 TF_ThreadMgr、COM 和窗口消息处理有关的私有动态代码中。

顺着文本服务框架这条线索继续测试,停用 QQ 输入法后问题消失,最终确认触发因素是 QQ 输入法。

如果你的 Explorer 也出现 0xc0000005 执行访问冲突,尤其是故障地址每次都保持相同页内偏移,不妨除了右键菜单和美化工具,也检查一下输入法与 TSF 组件。它们通常不会出现在“资源管理器崩溃”的第一怀疑名单里,却可能正是问题所在。

让OpenWrt控制台进入赛博空间:luci-theme-cyberpunk

2026-07-09 16:55:50

路由器管理后台,未必只能是单调的灰白界面。

luci-theme-cyberpunk 是一款为 OpenWrt LuCI 打造的深色主题。它以赛博朋克 HUD 为设计语言,将电青、玫红和状态绿融入导航、表单、表格、弹窗与系统状态,让每天都要打开的管理界面拥有更鲜明的个性,同时保留 LuCI 熟悉的操作方式。

如果你正在搭建一台OpenWrt软路由、家庭网关或旁路由,希望它不仅稳定实用,也能在视觉上更符合自己的桌面风格,那么这个主题值得一试。

第一眼:进入赛博空间

主题登录页采用深色 HUD 背景与专属 Cyberpunk 标识。高对比度的电青线条和玫红点缀带来明确的视觉焦点,又没有让装饰元素盖过登录操作本身。

luci-theme-cyberpunk 桌面端登录页

进入 LuCI 后,主题会将同一套视觉语言延伸至状态总览、顶部导航、侧边菜单、信息卡片和数据表格。熟悉的功能仍在原来的位置,但整个控制台获得了更完整、更统一的深色观感。

luci-theme-cyberpunk 桌面端状态总览

不只是“换个背景”

luci-theme-cyberpunk 并非简单叠加一张壁纸,而是一套独立的 LuCI 主题包。当前版本已经覆盖:

  • LuCI 导航与菜单;
  • 表单、按钮和输入控件;
  • 状态卡片与数据表格;
  • 弹窗及提示元素;
  • 登录页面;
  • 不同状态的颜色反馈。

整体采用深色优先配色。电青用于主要交互与结构强调,玫红负责视觉点缀,绿色用于状态提示。三种颜色共同构成赛博朋克氛围,同时让不同类型的信息更容易区分。

桌面与移动端,都保持同一种风格

路由器后台并不只会在电脑上打开。临时查看设备状态、调整无线设置或者重启服务时,手机往往更加方便。

luci-theme-cyberpunk 针对响应式界面进行了适配。在较窄的屏幕上,登录区域、导航和内容布局会随可用空间调整,尽量保持信息清晰与操作顺手。

移动端登录页 移动端状态页
luci-theme-cyberpunk 移动端登录页 luci-theme-cyberpunk 移动端状态总览

无论是在桌面浏览器中管理整套网络,还是用手机快速检查状态,都可以获得一致的视觉体验。

独立架构,便于安装与维护

对于开发者和喜欢折腾 OpenWrt 的用户,这个主题也保持了清晰的项目结构:

  • 独立软件包:luci-theme-cyberpunk;
  • 独立 ucode 模板目录:themes/cyberpunk;
  • 独立静态资源目录:/luci-static/cyberpunk;
  • 独立菜单模块:menu-cyberpunk;
  • 独立 UCI 配置命名空间:cyberpunk;
  • 壁纸 RPC helper:luci.cyberpunk_wallpaper。

主题资源与配置均使用独立命名空间,便于在 OpenWrt buildroot 中作为普通 LuCI 软件包构建和维护。

安装主题

前往 GitHub Releases 下载与你的 OpenWrt 版本匹配的软件包,然后将文件上传到设备并通过终端安装。

OpenWrt 24.10 及以前版本

opkg install luci-theme-cyberpunk-*.apk

OpenWrt 24.10 之后的版本

由于签名密钥问题,需要允许安装未受信任的本地 APK 软件包:

apk add --allow-untrusted luci-theme-cyberpunk-*.apk

安装完成后,进入 LuCI 的主题设置页面,选择 Cyberpunk 并保存即可。

安装前请确认软件包与设备架构和系统版本匹配。建议保留一个可用的 SSH 会话,以便在界面配置异常时进行检查或恢复。

从源码构建

如果你维护自己的 OpenWrt 固件,可以将 luci-theme-cyberpunk 放入 OpenWrt buildroot 的 package feed,并按普通 LuCI 软件包进行编译:

make package/luci-theme-cyberpunk/compile V=s

给熟悉的 LuCI,换一种表达方式

一款好的主题不应改变工具的核心用途,而应让信息更清楚、操作更愉快,也让设备真正带有使用者自己的风格。

luci-theme-cyberpunk 保留了 LuCI 的功能与操作逻辑,并用克制的 HUD 设计重新组织视觉体验。从登录路由器的第一屏,到每天查看的系统状态,它都在提醒你:网络基础设施也可以拥有属于自己的美学。

现在,让你的 OpenWrt 控制台进入赛博空间。

Artitalk V4: 从Leancloud迁移至Vercel

2026-06-18 15:46:47

Artitalk v3 的数据存储、账号登录和内容操作都依赖 LeanCloud。但 LeanCloud 即将停止服务,这意味着仍在使用 Artitalk v3 的站点将无法继续正常读取和写入数据。

与此同时,原项目 ArtitalkJS/Artitalk 已经不再更新,无法等待上游提供新的后端方案。

因此,我在原项目的基础上继续维护 Artitalk,并完成了这次 v4 更新。

Artitalk 仍然是那个可以嵌入博客的轻量级“说说”组件。发布动态、Markdown/HTML 渲染、图片和视频内容、评论、登录与删除等主要功能都得到了保留。不过,从 v4 开始,Artitalk 不再需要 LeanCloud 的 appId 和 appKey,而是通过一个由用户自行部署的 Vercel 服务端运行。

v4 的新架构

Artitalk v4 采用以下结构:

博客页面中的 Artitalk        ↓Vercel Serverless API        ↓Neon Postgres

前端不再直接操作数据库,而是将登录、查询、发布、编辑、删除和评论请求发送到 Vercel 服务端。服务端负责访问 Neon Postgres,并将结果转换成前端能够识别的数据格式。

这套方案有几个直接的变化:

  • Vercel 负责运行 Artitalk 的服务端 API;
  • Neon Postgres 负责保存说说、评论和管理员数据;
  • 管理员用户名、密码和数据库连接信息通过 Vercel 环境变量配置;
  • 可以使用 ALLOW_ORIGIN 限制允许访问接口的博客域名;
  • 前端只需要配置公开的 serverURL。

新的前端配置如下:

<script src="https://unpkg.com/@hclonely/artitalk"></script><div id="artitalk_main"></div><script>new Artitalk({  backend: 'vercel',  serverURL: 'https://your-vercel-app.vercel.app'})</script>

其中,serverURL 是部署后的 Vercel 项目地址,不是 Neon 数据库连接字符串。

历史数据可以继续保留

架构迁移最重要的问题不是部署新服务,而是如何带走旧数据。

Artitalk v4 提供了专门的 LeanCloud 数据迁移入口。用户可以从 LeanCloud 控制台导出旧应用数据,然后上传以下两个文件:

  • shuoshuo.0.jsonl:已经发布的说说;
  • atComment.0.jsonl:说说下的评论。

迁移程序会将数据写入 Neon,并尽量保留原有的 objectId、createdAt、updatedAt 和其他业务字段。这样不仅可以保留历史发布时间,也可以继续维持评论与说说之间的关联。

LeanCloud _User 中的账号和密码不会迁移。v4 会根据 Vercel 环境变量重新创建管理员账户,这样可以避免继续依赖旧平台的用户系统。

升级需要做什么

从 v3 升级到 v4,主要需要完成以下步骤:

  1. 从 LeanCloud 导出 shuoshuo 和 atComment 数据;
  2. 将 Artitalk 服务端部署到 Vercel;
  3. 为 Vercel 项目连接 Neon Postgres;
  4. 配置管理员账号、数据库和允许访问的站点域名;
  5. 在服务端初始化页面导入 LeanCloud JSONL 文件;
  6. 初始化新的管理员账户;
  7. 将博客中的旧版 appId、appKey 配置替换为 serverURL;
  8. 核对说说数量、评论数量、发布时间和登录发布功能。

迁移完成并验证无误前,请不要删除 LeanCloud 应用,也不要删除原始导出文件。保留旧环境可以在新服务出现配置问题时快速回滚。

不变的使用体验,更可控的后端

Artitalk v4 尽量保持了原有组件的使用体验,但其运行基础已经发生变化。

以前,Artitalk 是一个直接依赖 LeanCloud 的前端组件;现在,它由前端组件、Vercel 服务端和 Neon 数据库共同组成。用户需要多完成一次部署,却也因此获得了更清晰的配置边界和更独立的数据存储方案。

这次迁移首先解决的是 LeanCloud 即将停止服务带来的生存问题,也让这个已经停止更新的项目能够继续使用和维护。未来即使需要更换数据库或部署平台,前端也不必再次跟随底层服务进行大规模重写。

如果你仍在使用 Artitalk v3,建议先备份 LeanCloud 数据,再按照迁移文档完成升级:

记一次hexo-bilibili-bangumi分时函数渲染优化

2026-06-15 20:11:24

在前端页面里,长列表渲染是一个很常见的性能问题。数据量不大时,直接 map 生成 HTML 再一次性插入 DOM 通常没有明显问题;但当列表变长、模板渲染逻辑变复杂、图片和元信息较多时,这种同步渲染方式就容易占满主线程,导致页面切换、滚动和点击出现明显卡顿。

本文记录一次对 hexo-bilibili-bangumi 追番页面分页渲染的优化:把原本一次性同步完成的模板渲染,改成在浏览器空闲帧中分批执行,从而降低单帧压力,让页面先保持可交互。

背景

追番页面会先渲染每个分类前 10 条数据,剩余数据通过 bangumis.json 异步加载。旧实现大致是这样的:

const html = {  wantWatch: data.wantWatch.slice(10).map((item) => renderItem(item)).join('\n'),  watching: data.watching.slice(10).map((item) => renderItem(item)).join('\n'),  watched: data.watched.slice(10).map((item) => renderItem(item)).join('\n')};document.querySelectorAll('#bangumi-item1>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.wantWatch);document.querySelectorAll('#bangumi-item2>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.watching);document.querySelectorAll('#bangumi-item3>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.watched);

这段代码的问题不在于写法复杂,而在于它把三类数据的模板渲染集中在一个任务里完成。浏览器主线程在执行这段 JavaScript 时,不能同时处理用户输入、样式计算、布局和绘制。如果数据量增加,单次任务耗时变长,就会出现掉帧和交互延迟。

对于博客页面来说,用户最直接的感受不是“渲染总耗时是多少”,而是“页面是不是能立刻响应”。因此优化目标不是把所有 HTML 更快地一次性生成出来,而是把大任务拆小,让浏览器有机会在任务之间处理渲染和输入。

原理

浏览器页面的 JavaScript、样式计算、布局、绘制和用户输入处理大多运行在主线程上。如果一个 JavaScript 任务长时间不结束,浏览器就没有机会进入下一帧,也就无法及时响应滚动、点击和视觉更新。

分时函数的核心思想是:

  1. 把一个大任务拆成多个小任务。
  2. 每次只执行一部分工作。
  3. 当前帧还有空闲时间时多做一点,没有空闲时间时让出主线程。
  4. 下一次空闲时继续处理剩余任务。

浏览器提供了 requestIdleCallback,它会在主线程空闲时执行回调。回调参数里的 deadline.timeRemaining() 可以告诉我们当前空闲周期大概还剩多少时间。利用这个信息,可以把长列表渲染拆成多批。

不过 requestIdleCallback 并不是所有环境都支持,因此实现时还需要准备一个降级方案:如果浏览器不支持,就使用 setTimeout 延后执行。这样虽然不能精确感知空闲时间,但仍然可以避免在一个同步任务中渲染全部内容。

方法实现

这次优化拆成三步。

1. 封装空闲调度函数

先封装一个 runWhenIdle,优先使用 requestIdleCallback,否则退化到 setTimeout:

function runWhenIdle(callback) {  if (typeof requestIdleCallback === 'function') {    requestIdleCallback(callback);    return;  }  setTimeout(() => {    callback({      timeRemaining: () => 0    });  }, 16);}

这里的降级实现给了一个 timeRemaining() 为 0 的 deadline。后面的批处理逻辑会保证即使没有剩余时间,每一轮也至少处理一条数据,避免任务永远无法推进。

2. 复用编译后的模板函数

原来每条数据都直接调用 pug.render。如果运行时支持 pug.compile,可以先把模板编译成渲染函数,然后每条数据复用这个函数:

function createBangumiPageRenderer() {  if (hexoBilibiliBangumiOptions.pug && typeof hexoBilibiliBangumiOptions.pug.compile === 'function') {    const render = hexoBilibiliBangumiOptions.pug.compile(hexoBilibiliBangumiOptions.pugTemplate);    return function hexoBilibiliBangumiRenderPage(item) {      return render({        item,        ...hexoBilibiliBangumiOptions.pugOptions      });    };  }  return function hexoBilibiliBangumiRenderPage(item) {    return hexoBilibiliBangumiOptions.pug.render(hexoBilibiliBangumiOptions.pugTemplate, {      item,      ...hexoBilibiliBangumiOptions.pugOptions    });  };}

这一步不是分时渲染的必要条件,但它能减少每条数据的重复开销。长列表优化通常要同时关注两个方向:减少总计算量,以及避免单次计算阻塞太久。

3. 在空闲帧中分批渲染

核心批处理逻辑如下:

function renderItemsInIdle(items, renderPage, onComplete) {  const html = [];  let index = 0;  function renderBatch(deadline) {    let renderedInFrame = false;    while (index < items.length && (!renderedInFrame || deadline.timeRemaining() > 0)) {      html.push(renderPage(items[index]));      index++;      renderedInFrame = true;    }    if (index < items.length) {      runWhenIdle(renderBatch);      return;    }    onComplete(html.join('\n'));  }  runWhenIdle(renderBatch);}

这里有一个细节:循环条件不是简单的 deadline.timeRemaining() > 0,而是:

!renderedInFrame || deadline.timeRemaining() > 0

这样可以保证每个空闲回调至少渲染一条数据。否则在降级方案里 timeRemaining() 一直是 0,任务就会被不断重新调度,却不会真正处理任何条目。

最后,把三个分类组织成任务队列,按顺序分批渲染并插入到对应分页按钮之前:

function renderTasksInIdle(tasks) {  const renderPage = createBangumiPageRenderer();  let taskIndex = 0;  function runNextTask() {    if (taskIndex >= tasks.length) return;    const task = tasks[taskIndex];    taskIndex++;    renderItemsInIdle(task.items, renderPage, (html) => {      document.querySelectorAll(task.selector)[0].insertAdjacentHTML('beforeBegin', html);      runNextTask();    });  }  runNextTask();}

调用时只需要传入数据和目标选择器:

renderTasksInIdle([  {    items: data.wantWatch.slice(10),    selector: '#bangumi-item1>.bangumi-pagination'  },  {    items: data.watching.slice(10),    selector: '#bangumi-item2>.bangumi-pagination'  },  {    items: data.watched.slice(10),    selector: '#bangumi-item3>.bangumi-pagination'  }]);

效果

优化前,bangumis.json 加载完成后,页面会立刻同步执行三类列表的模板渲染。数据越多,单次任务越长,用户越容易感受到卡顿。

优化后,渲染被拆分到多个空闲帧中执行:

  • 首屏内容和标签切换逻辑不需要等待全部剩余数据渲染完成。
  • 浏览器可以在批次之间处理绘制和输入事件。
  • 单帧 JavaScript 执行时间降低,滚动和点击更不容易被长任务阻塞。
  • 支持 requestIdleCallback 的浏览器可以根据真实空闲时间动态多渲染几条;不支持时也能通过 setTimeout 分批推进。
  • Pug 模板优先编译后复用,减少重复解析模板的成本。

这种优化不会让所有内容“瞬间完成”,但会改善用户感知性能。对用户来说,页面能先响应、逐步补齐内容,通常比等待一个长任务全部执行完更自然。

适用场景

分时渲染适合以下情况:

  • 列表数据较多,但不要求所有内容立即同步展示。
  • 每条数据的渲染逻辑较重,例如模板渲染、格式化、复杂 DOM 字符串拼接。
  • 页面初始交互比完整内容一次性出现更重要。
  • 不方便引入虚拟列表,但希望降低长任务阻塞。

如果是后台管理系统里的超大表格,虚拟列表可能是更彻底的方案;如果是博客、文章页、追番页这类静态内容为主的场景,分时渲染的改动更小,收益也比较直接。

注意事项

分时渲染不是银弹,实现时需要注意几个边界:

  1. 每个批次至少处理一项,避免低空闲时间或降级方案下任务无法推进。
  2. 插入 DOM 的时机要稳定。本文的实现是在某个分类全部渲染完成后再插入,避免一个分类的内容被频繁分段插入造成布局抖动。
  3. 如果用户可能在渲染未完成时切换页面或销毁容器,需要增加取消机制或容器存在性判断。
  4. 如果列表极长,单纯把 HTML 存在数组中最后一次性插入仍可能占用较多内存,可以进一步改成每 N 条插入一次。
  5. requestIdleCallback 适合低优先级任务,不适合用户点击后必须立即完成的关键反馈。

总结

这次优化的关键不是换一个更复杂的框架,而是把“同步做完所有事”的思路改成“浏览器空闲时分批做”。对长列表、模板渲染和博客插件这类场景来说,分时函数是一个成本低、侵入小、效果明确的优化手段。

最终实现保留了原有数据结构和 DOM 插入位置,只替换了渲染调度方式:数据仍然来自 bangumis.json,模板仍然使用 Pug,展示结果保持一致,但渲染过程对主线程更友好。

IP签名图片生成服务

2026-05-21 11:11:19

一个简单的服务,可以生成包含 IP 地址、地理位置、天气、系统信息等数据的签名图片。

功能特点

  • 获取访问者的 IP 地址和地理位置信息
  • 显示当前天气状况
  • 显示访问者的操作系统和浏览器信息
  • 显示一言语句
  • 支持图片缓存
  • 支持跨域访问

示例

IP签名档示例

使用方法

基本用法

直接访问签名图片:

https://你的域名/signature

自定义尺寸

支持通过查询参数自定义图片尺寸:

https://你的域名/signature?width=1000        # 指定宽度,高度会按比例缩放https://你的域名/signature?height=600        # 指定高度,宽度会按比例缩放

默认尺寸为 752x423 像素。建议只指定宽度或高度其中之一,另一个尺寸会自动按原始比例计算,如果同时指定宽度和高度,则根据高度计算。

在网页中使用

<!-- 默认尺寸 --><img src="https://你的域名/signature" alt="IP签名档" /><!-- 自定义尺寸 --><img src="https://你的域名/signature?width=1000" alt="IP签名档" />

缓存说明

  • IP 地理位置数据:长期缓存
  • 天气数据:缓存 30 分钟
  • 一言数据:缓存 5 分钟
  • 生成的图片:客户端缓存 10 分钟

部署方法

本地部署

  1. 克隆仓库:

    git clone https://github.com/HCLonely/ipSignature.gitcd ipSignature
  2. 安装依赖:

    npm install
  3. 配置环境变量:

    # 复制环境变量示例文件cp .env.example .env.production# 然后编辑 .env.production 文件,修改相关配置# 至少需要配置以下变量之一:# - IPINFO_TOKEN(ipinfo.io的API令牌)# - NSMAO_TOKEN(nsmao的API令牌)## 以及:# - OPENWEATHER_API_KEY(OpenWeatherMap的API密钥)
  4. 编译代码:

    npm run build
  5. 启动服务:

    npm start

Vercel 部署

一键部署

部署到 Vercel

手动部署步骤

  1. Fork 本仓库到你的 GitHub 账号

  2. 在 Vercel 中导入项目:

    • 访问 Vercel
    • 点击 “Import Project”
    • 选择你 fork 的仓库
    • 点击 “Import”
  3. 配置环境变量:

    • 在项目设置中找到 “Environment Variables”

    • 添加以下环境变量:

      # IP地理位置服务令牌 (至少需要配置其中一个)IPINFO_TOKEN=your_ipinfo_token_hereNSMAO_TOKEN=your_nsmao_token_here# OpenWeatherMap API密钥OPENWEATHER_API_KEY=your_openweather_api_key_here# 背景图片URL (可选,仅支持 jpg, jpeg, png, gif 格式)BACKGROUND_IMAGE_URL=https://example.com/background.jpg# 生产环境标识NODE_ENV=production
  4. 部署:

    • Vercel 会自动部署你的服务
    • 部署完成后,你会得到一个 .vercel.app 域名
    • 也可以绑定自己的自定义域名

环境变量说明

创建 .env 文件并配置以下环境变量:

变量名 必需 默认值 说明
IPINFO_TOKEN 是* - ipinfo.io 的 API 令牌,用于获取访问者的地理位置信息
NSMAO_TOKEN 是* - nsmao.com 的 API 令牌(备选),用于获取访问者的地理位置信息
OPENWEATHER_API_KEY 是 - OpenWeatherMap 的 API 密钥,用于获取天气信息
BACKGROUND_IMAGE_URL 否 - 背景图片URL,仅支持 jpg, jpeg, png, gif 格式
PORT 否 3000 服务器端口
DEBUG 否 false 调试模式,设置为 true 时显示详细错误信息
NODE_ENV 否 development 运行环境,生产环境请设置为 production

IPINFO_TOKEN 和 NSMAO_TOKEN 至少需要配置其中一个

API 服务说明

本项目使用了以下第三方 API 服务:

IP 地理位置服务

天气服务

一言服务

命令行参数

参数 简写 说明
–public-ip -p 使用公网 IP(当检测到本地 IP 时)