MoreRSS

site iconHCLonely修改

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

Inoreader Feedly Follow Feedbin Local Reader

HCLonely的 RSS 预览

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

2026-08-05 11:12:36

先说结论

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

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

事件查看器只能看到 BEX64StackHash 和异常码。完整转储则保留了动态内存、寄存器和线程上下文,最终正是内存里的 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 的 appIdappKey,而是通过一个由用户自行部署的 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,并尽量保留原有的 objectIdcreatedAtupdatedAt 和其他业务字段。这样不仅可以保留历史发布时间,也可以继续维持评论与说说之间的关联。

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

升级需要做什么

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

  1. 从 LeanCloud 导出 shuoshuoatComment 数据;
  2. 将 Artitalk 服务端部署到 Vercel;
  3. 为 Vercel 项目连接 Neon Postgres;
  4. 配置管理员账号、数据库和允许访问的站点域名;
  5. 在服务端初始化页面导入 LeanCloud JSONL 文件;
  6. 初始化新的管理员账户;
  7. 将博客中的旧版 appIdappKey 配置替换为 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 时)

树莓派搭建私有在线PS网站

2025-06-05 14:57:09

厌倦了大型图像编辑软件对电脑性能的压榨?担心在线工具泄露敏感设计?让你的树莓派不在吃灰!只需一台树莓派,你就能拥有完全私有的、功能媲美 Photoshop 的在线编辑利器——Photopea!通过部署Docker项目 photopea,我们将把强大的图像处理能力装进小小的树莓派里。

为什么选择树莓派 + Photopea?

  • 隐私至上: 所有图片处理都在你的本地网络或树莓派上进行,敏感设计稿绝不外流。
  • 离线可用: 无需连接官方服务器,断网也能畅快编辑。
  • 轻量低耗: 树莓派功耗极低,7x24 小时运行也毫无压力,静音环保。
  • 成本低廉: 利用闲置树莓派,无需额外投入高性能电脑或订阅费用。
  • 跨平台访问: 家里任何设备(电脑、平板、手机)的浏览器都能随时使用。
  • 开源自由: 基于开源项目部署,掌控自己的工具。

所需设备:

  1. 树莓派: 推荐树莓派 4B (2GB/4GB/8GB 内存均可) 或树莓派 5,性能更佳。树莓派 3B+ 也可尝试(性能稍弱)。
  2. 操作系统: ubuntu 22.04 (理论上能运行Docker的系统均可)。
  3. 网络: 树莓派接入你的家庭/办公室局域网。
  4. 存储: 一张容量足够的 microSD 卡 (建议 16GB 或以上)。
  5. 电源: 合适的电源适配器。

手把手搭建指南 (基于 Docker)

Docker 让部署变得异常简单。如果你的树莓派还没安装 Docker,请先安装:https://docs.docker.com/engine/install/

核心步骤:一键部署 Photopea

# 运行 Photopea 容器 (将内部 8887 端口映射到树莓派的 8080 端口)sudo docker run -d --name ps-online --restart always -p 8080:8887 ramuses/photopea:latest

访问你的私有 Photopea

  1. 本地测试: 在树莓派本身的浏览器中访问:http://localhost:8080
  2. 局域网访问: 在同一局域网内的其他设备浏览器中,输入你的树莓派 IP 地址 + :8080,例如:http://192.168.1.100:8080

重要提示

  • 性能考虑: Photopea 在浏览器中运行,主要依赖访问设备的 CPU/内存。树莓派本身主要负责提供服务。树莓派 4/5 能流畅服务多个客户端。复杂操作(如超大图、大量滤镜)在客户端设备上可能变慢。
  • 数据存储: 默认部署下,用户上传或处理的图片仅保存在浏览器缓存中。关闭浏览器标签页或清理缓存可能导致未保存的更改丢失!务必养成使用 File > Save As PSD (或导出其他格式) 保存到本地设备的习惯。
  • 首次加载: 镜像较大,首次拉取和启动可能需要几分钟,请耐心等待。