从作者原版 0915 出发,到当前这个「能给家里人用」的版本。每一次功能增加、bug 修复、安全加固都记在这里,方便回头看清「改了什么、为什么改、改完什么样」。
两个阶段加起来的结果。字节数里含 231 KB 的内联图标字体。
每次改完另存为新文件,原文件保持不动 —— 出问题能拿原文件重新改。
| 阶段 | 日期 | 产出文件 | 体量 | 路由 | 主要内容 |
|---|---|---|---|---|---|
| 起点 | 2026-09-15 | worker原版0915.js作者 GitHub 发布的版本 |
325,550 B 7,814 行 |
35 | 只读参考,未改动 |
| 一 | 2026-09-26 | worker家人-0926.js |
344,678 B 7,975 行 |
35 +5 PWA |
去掉作者痕迹、加 PWA 支持 |
| 二 | 2026-09-27 | worker家人-0927.js |
639,596 B 9,266 行 |
43 +5 PWA |
回收站、版本历史、打包下载等 7 项 |
| 三 | 2026-09-28 | worker家人-0928.js当前版本 |
640,621 B 9,269 行 |
43 +5 PWA |
壁纸空文件兜底、Office 文案、窄屏横滑、回收站简化、修壁纸遮挡、改回收站文案 |
按「影响面」列排序,可以一眼看出哪几项是必须的。
| # | 事项 | 类型 | 服务端 | 网页端 | 客户端 | 规模 |
|---|---|---|---|---|---|---|
| 1 | 回收站 删掉的东西能捞回来 |
新增 | ✅ | ✅ | ✅ | ~205 行 |
| 2 | 版本历史 文件被改坏了能退回旧版 |
新增 | ✅ | — | ✅ | ~255 行 |
| 3 | 整目录打包下载 多选一次下成一个 zip |
新增 | ✅ | — | ✅ | ~285 行 |
| 4 | 断点续传 大文件断了不用从头传 |
新增 | ✅ | — | ✅ | 75 行 |
| 5 | 安全响应头 9 处页面出口统一加防护头 |
加固 | ✅ | ✅ | — | ~60 行 |
| 6 | 图标字体本地化 不再依赖外部 CDN |
加固 | ✅ | ✅ | — | ~25 行 |
| 7 | 目录改名批量实现 文件多了改名不再出错 |
修复 | ✅ | ✅ | ✅ | ~145 行 |
| 8 | 去作者痕迹 + PWA 去掉作者署名,加可安装到桌面 |
改造 | ✅ | ✅ | — | +363 / −202 |
| 9 | 安全补丁验证 4 组反向测试,证明补丁不是白加的 |
验证 | — | — | — | 41 项断言 |
| 10 | 修 CSP 图片白名单 页头 logo 不再是破图 |
修复 | ✅ | ✅ | — | ~25 行 |
| 11 | 修浏览器端调服务端函数 分享「有效期」一显示就报错(作者原版就有) |
修复 | — | ✅ | — | ~14 行 + 1 检查脚本 |
| 12 | 壁纸空文件兜底 选到 0 字节图时如实报错,不再「提示已更新、壁纸却没变」 |
修复 | — | ✅ | — | 1 行 |
| 13 | Office 文案指向客户端 网页端不做 Office 预览,把人引到客户端去打开 |
调整 | — | ✅ | — | 1 行 |
| 14 | 文件列表窄屏横滑 手机上「修改时间」不再被压扁,且仍能点表头按时间排序 |
新增 | — | ✅ | — | 2 行 CSS |
| 15 | 回收站简化 去掉「清理 N 天前」按钮 + JS(蔡老师觉得多余,只留「恢复所选 / 彻底删除 / 清空回收站」) |
调整 | — | ✅ | — | -18 行 |
| 16 | 修回收站被壁纸盖住 壁纸层 body::before 是定位元素,会盖在没定位的内容之上;作者的白名单漏了 .trash-page → 设了壁纸后标题和按钮被洗白。补上并加 body > * 兜底 |
修复 | — | ✅ | — | +5 行 |
| 17 | 回收站副标题去掉空头承诺 原文案「30 天后可手动清理」已不成立(「清理 N 天前」按钮上一轮删了,也没有任何定时任务)→ 改成「暂未启用自动清理」,只说实话 |
修复 | — | ✅ | — | 改 1 行文案 |
按时间顺序。每条都写了「为什么改」「怎么改的」「改完什么样」。 「怎么改的」和「技术要点」默认收起 —— 想学细节,点标题就展开。
比我们原来用的 0630 多了几样东西。这是我们的出发点 —— 一个字都不改,只当参考。
0915 是作者自己更新的版本,里面有几样我们后来一直想要的:纯文本在线编辑、上传速度显示、自定义分享后缀、复制下载直链、主题切换 + 云盘背景图。从它出发,就不用把这些再自己写一遍。
/api/text-file、/api/ui-background、/api/ui-settings把作者的署名、版本检查、页脚链接都清掉,同时让它能「安装」到手机/电脑桌面上像 App 一样用。
作者在页面里留了署名(页脚的 GitHub 链接)、版本徽章,以及一个会去访问作者仓库检查更新的脚本。自己家里人用,这些既没必要,又会让页面多一次外部请求。
先拿 0630 那次的改动当「配方」(7 处改动),再逐条套到 0915 上。PWA 的代码块直接从既有文件里抽出来插入,保证逐字节一致。
By HandsomeMJZCURRENT_VERSION / checkVersionUpdate)—— 那个地址指向作者仓库/pwa-manifest.json、/pwa-icon.png、/pwa-icon-192.png、/favicon.png、/pwa-sw.jsviewport-fit=cover 那一行:0915 的作者自己已经加过了,我们不用再加。说明配方套得准。
客户端本身已经有回收站、版本历史的界面了,但 Worker 这边没有对应的接口 —— 点了就报错。这次把缺的补上,顺带把两个「会卡住人」的地方加固掉。
把客户端源码里调用的所有接口提取出来,跟 Worker 的路由求差集 —— 客户端要的 31 个接口里,有 8 条路由在 Worker 里不存在:回收站 3 条、版本历史 3 条、打包下载 1 条、断点续传 1 条。对应的就是「点回收站报错」「点版本历史报错」「多选下载只能下一个」。
删掉的东西先放进回收站,能恢复回原来的位置;确认不要了才真正释放空间。
家里人共用一份空间,谁手滑删了东西都可能。原来的删除是直接抹掉,没有后悔药。而「回收站管『没了』、版本历史管『变了』」是两件不同的事 —— 文件被改坏时文件还在,回收站里什么都没有,那要靠版本历史;文件被删了才归回收站。
关键顺序:删除时先把条目快照进回收站,再摘掉当前条目。R2 里的文件本体因为还被回收站这条记录引用着,就不会被清理掉。
每次覆盖上传前,自动把旧的那一份存起来。文件被改坏了,能退回任意一个旧版本。
家人共用 + 自动同步,是最容易「内容被改坏」的组合:文件被覆盖上传之后,文件本身还在、回收站里什么都没有 —— 这时候只有版本历史能救。所以它更必要,不是更不必要。
在「覆盖写入之前」先把旧对象复制一份。难点是「哪里算覆盖」要一处不落地找出来 —— 一共挂了 6 个钩子:
/api/upload/api/multipart/complete/api/distributed/complete多选一批文件,一次下载成一个 zip 压缩包 —— 而不是「选了 10 个只下来 1 个」。
原来多选下载是逐个文件触发下载。但浏览器只放行第一个,后面的全部静默拦掉 —— 用户看到的现象是「我选了 10 个,只下来 1 个」,很难理解是为什么。
选 1 个:还是普通下载。选多个:走新接口,服务端边读边打包、流式吐出来(用不压缩的 store 模式,省 CPU),前端边收边显示进度。
files.zip 好认大文件传到一半断了,下次接着传没传完的部分 —— 不用整个文件从头再来。
教学资料动辄几百 MB。原来一旦中途失败(网络抖一下、关了个窗口),整个文件白传。客户端的同步任务尤其怕这个 —— 每次跑都得从头来。
客户端把「哪个文件、传到第几个分片」记在本地。重传时先问服务端要一份「还没传完的那些分片」的新上传地址,只补缺的。
这个接口就是 /api/distributed/reallocate —— 顺带把旧节点上的残留对象清掉。
给每一个页面出口统一加上安全头,并且用一处函数收口 —— 以后新加页面不会漏。
全站 一个安全响应头都没有。这意味着:别人可以用别的网站把我们的页面嵌进 iframe 里(钓鱼/伪装),浏览器也不知道该怎么防 XSS。
写了一个 securityHeaders(),再把所有返回 HTML 的地方统一改走 htmlResponse()。改完实测:裸写 text/html 的地方 = 0 处,全部走统一出口。
onclick,
script-src 必须带 'unsafe-inline' 才能正常跑。
它真正有用的地方是:frame-ancestors(别人不能嵌我们的页面)、
object-src、base-uri,以及限制外部域。别把它当万能药。
text/html 的地方,计数必须是 0。
全站 164 个图标原来靠一个外部 CDN 提供字体 —— 那个 CDN 一旦连不上,整个界面全是「文字」而不是图标。
页面里只有 1 个外部链接,但全站 164 处图标、48 种不同图标全都靠它。这个 CDN 在国内访问不稳 —— 一旦失败,所有图标位置会显示成「home」「upload」这样的文字名,界面直接不能用。
把那 1 个外链换掉,改成把字体文件直接内嵌进页面(base64)。代价是文件大了 231 KB,换来的是零外部依赖。
给文件夹改名,背后其实是「把里面所有文件一个个搬到新位置」。文件一多就中途超时,留下半成品 —— 看起来就是「莫名其妙多了副本」。
给文件夹改名,服务端走的不是「改个名字」,而是通用的「搬移」逻辑:逐条复制 + 逐条删除。
算下来,一个 200 个文件的目录,要来回问数据库约 800~1000 次,而且全是排队一个个来、没有事务。
问到一半超时了 —— 这时候的状态是:一部分文件在新位置、一部分还在旧位置,旧目录的索引删了一半。用户看到的就是「改个名,怎么多出一堆重复文件」。
核心思路:别再一条条问了,把要写的操作先攒起来,再一次提交。
用浏览器打开云盘时,工具栏多了一个回收站图标 —— 否则「从网页删掉的东西」会静默进回收站,看不见也捞不回来。
回收站接口做好之后,网页端删除就变成「软删除」了 —— 但网页上没有回收站页面。结果就是:从浏览器删一个文件,它消失了,你也没法确认它到底删没删、更没法恢复。这比没有回收站还让人不安。
在顶部工具栏「分享管理」和「界面设置」之间加了一个回收站图标,点进去是一个完整页面:全选、恢复所选、彻底删除所选、清理 30 天前、清空回收站。
实现上做了简化:参考实现里有一套「窄屏点表头切换显示大小/时间列」的响应式机关,要连带搬一大堆样式。我们改成窄屏直接把「大小」列藏掉 —— 够用,也不容易出错。
回收站和版本历史能跑起来,靠的不是那几个新页面,而是四处「顺手补上的判断」。这一轮专门给它们做证据 —— 而且是能证明「旧代码确实会坏」的那种证据。
新增了一类数据(回收站条目、历史版本),就等于多了一个「谁引用着 R2 对象」的答案。而清理程序判断「这个对象没人要了」时,只认它知道的那几个地方。少登记一处,后果不是"少个功能",而是: 一次「清理空间」把用户的回收站和历史版本整批永久删掉 —— 没有二次确认,也没有回收站兜底(回收站自己就是被删的那个)。
这种补丁的特点是:不补也能跑、测试也不会红、只有真出事了才知道。所以必须主动造证据。
写 4 个验证脚本,每个都配一组反向测试:把同一套流程换成改动前的真实旧代码再跑一遍,断言它确实会失败。 只有「旧代码确实坏 + 新代码确实好」同时成立,才说明这个补丁不是多余的。
node tools/run-all-tests.mjs 能跑完全部 6 组 92 项。
We'll、don't 那个撇号会被当成「字符串开始了」,后面整个函数都被当字符串,配对直接错位。这个坑是跑测试时才发现的 —— 说明测试本身也在被测。蔡老师报:「图标显示有问题」—— 页头那个小图标变成了浏览器的「图片加载失败」占位符。查下来是加安全响应头时自己踩的坑。
安全响应头里的 CSP 写了 img-src 'self' data: blob:,意思是「只允许加载本站自己的图片」。
本意是好的:别人分享的页面里如果有外链图片,会被浏览器直接拦掉,不会「看一眼就把你的 IP 暴露给图片服务器」。
但站点自己的 logo 恰恰是站主自己填的外链(CLOUD_ICON_URL 指向另一个域名)。
规则不认这个区别,一视同仁地拦掉 → 页头当场变破图。图片地址本身没问题,是浏览器按 CSP 拒绝加载它。
让 CSP 的 img-src 自动放行「站主自己填过的那几个图片域名」:
从 CLOUD_ICON_URL / CLOUD_PWA_ICON_URL / CLOUD_FAVICON_URL / LOGIN_BACKGROUND_URL
四个环境变量里各取一次域名,加进白名单。
其余外域仍然一律拦掉 —— 安全和可用两头都占住。一个都没配时,白名单为空,CSP 回到最严。
frame-src 'none' / frame-ancestors 'none',
这会把 PDF 预览用的 iframe 一起拦掉(预览是把下载地址塞进 iframe)。
本地文件已经改成 'self' 了,部署新版本时会一并解决。
javascript: 这种危险协议」的情况。另一个 IDE 那边报了个「版本历史弹窗永远打不开」的 bug,蔡老师问我们这边有没有同样的问题。查下来:我们也中了一个,而且是作者原版就带来的。
他们前端函数里直接调了 jsAttr() —— 而那函数只定义在 Worker(服务端)。点开弹窗就报 jsAttr is not defined,而且藏了 21 天:没历史版本时函数会提前 return,永远走不到那一行;语法检查、部署自检也全都抓不到(语法完全合法)。
根子在于:单文件 Worker 里「服务端」和「浏览器端」的代码住在同一个文件里,肉眼看着是一家人,运行时却是两个世界。
写了个静态检查脚本,把 <script> 块(浏览器执行)和内联事件属性里的代码抠出来,
看它们调用了哪些函数、其中哪些在浏览器端根本没定义。结果抓到 1 个真的:
formatDate —— 分享管理面板里的 shareMetaText() 调它,而它只在服务端定义。
和那边一模一样:只有分享设了有效期才会触发(没设就短路跳过),所以一直没被发现。
影响比那边还大一点:报错发生在渲染循环里,一抛就把整个循环打断 —— 从那条分享起,后面所有分享都不再渲染(而列表开头已经被清空), 表现为「分享管理面板一片空白」。分享管理是常用功能,不像版本历史那样偶尔才点一次。
formatDate(和服务端格式保持一致)。
?path=... 查询参数传,服务端用 searchParams.get() 取,会自动解码。已逐个接口确认过。escapeHtml / formatDate / formatSize
在服务端和浏览器端各有一份。这是正常的,但改动时必须两边一起改,只改一边就会出现「有的地方显示对了、有的地方不对」。circle(、translate( 这种 CSS 字符串里的词。
换成「服务端有定义 + 浏览器端没有 + 浏览器端在调用」三条同时成立,噪声立刻归零 —— 因为杂词根本不在服务端名单里。formatDate —— 这才证明脚本有效、而且这个坑是原版带来的。
前面几项是「加功能」,这一段反过来 —— 全是蔡老师在手机和电脑上真用出来的问题。 改动都很小(1 行、2 行 CSS、改个文案),但每条都对着一个真实的不顺手。
回收站页面的标题和按钮点开时正常,不到一秒就变白,只剩表格。根因是壁纸层:
body.has-custom-bg::before 是 position:fixed; z-index:0 的伪元素,
而按 CSS 绘制顺序,定位元素画在所有「没设 position」的普通内容之后 —— 它天然压在页面内容上面。
作者早知道这事,写了一条白名单把内容抬到 z-index:1:
.app-bar, .layout, body > main, .login-wrap, .foot-bar —— 名单里漏了 .trash-page。
补上,并加 body > * 兜底,以后新增页面不会再踩。
时间线也对得上:壁纸是异步加载的(loadUiSettings() 去 fetch,回来才加 class),
所以「点开那一瞬间没有壁纸层、一切正常;fetch 一回来就被洗白」。
${TRASH_KEEP_DAYS} 往回查 →
发现这个常量无人引用 → 全项目 grep「自动清理 / 30 天后」→ 又挖出 3 处(客户端界面、客户端开发文档、常量注释),一起改掉。
这一节是专门给「学习参考」用的 —— 把这次改动里反复出现的做法抽出来。 它们不限于这个项目,换个场景也成立。整节默认收起,点标题展开。
要搬一段别人的代码过来,先列出它用到的所有东西(函数、常量、工具), 逐个确认目标文件里有没有。有一个缺的就不能搬 —— 否则搬过去是一片报错。
这次用在哪:搬回收站和版本历史前,先逐项核对依赖清单, 结果发现「版本历史」背后整套机制在原版里根本不存在(不是缺几条路由,是整个底座都没有)。 如果没核这一步,会以为搬两条路由就完事。
「备份一份,再删原件」这类操作,顺序反了会静默失效 —— 不报错、不崩溃, 只是备份出来的东西永远恢复不回来。这类 bug 最难发现,因为一切看起来都正常。
这次用在哪:回收站必须先「快照」再「摘条目」; 彻底清除必须先「删记录」再「清理数据」。两处顺序反了都会导致数据找不回来。
系统里往往有「清理没人用的东西」的逻辑。你新增一种新的数据种类时, 必须让所有这类清理逻辑都认识它 —— 漏一处,你的数据就会被当垃圾清掉。
这次用在哪:回收站和版本历史都是「新的数据种类」, 各自要登记到两处扫描函数里。这是本次最大的坑,也是最有价值的一条经验。
逻辑上已经保护好了,但再加一道不依赖逻辑的保险: 用命名前缀做白名单,这类数据永远不允许被自动删除。将来逻辑改错了,也删不掉。
这次用在哪:历史版本的对象名有固定前缀,加进「永不自动删除」的白名单, 并且在删除前再反查一遍引用关系。两道锁。
处理大批量数据时,N 次单独请求换成「先把要做的操作攒在内存里,再分批提交」。 速度快几十倍,而且单批内是原子的 —— 顺带解决了「做一半失败了」的问题。
这次用在哪:文件夹改名。200 个文件从约 1000 次排队往返,降到 5 次提交。
给老逻辑加一条新快路时,让新路只在条件完全满足时启用, 只要有一点不确定(依赖不可用、目标有冲突),就退回原来的实现。 这样新增的东西不可能破坏既有行为。
这次用在哪:批量搬移只在「数据库可用 + 目标位置是空的」时生效, 否则原样走老的逐条逻辑。
要保证「每处都做同一件事」(加安全头、记日志、统一格式), 别去每处各写一遍 —— 做个统一函数让所有地方都从它过。 这样「漏掉」在结构上就不可能发生。
这次用在哪:安全响应头。9 个页面出口全部改走同一个函数, 以后新加页面自动继承。
不说「应该没问题了」,而是给出一个能数出来的数字: 缺几条、剩几处、多少项通过。结论可复现、可复查,也能在下一次改动后重跑一遍。
这次用在哪:写脚本比对两边的接口清单求差集(缺口 8 → 0); 搜「裸写页面类型的地方」计数必须为 0;把函数抽出来单独跑,逐项断言。
想优化体积/性能之前,先测一下能优化多少。 如果收益很小,就果断放弃、改用最简单可靠的方案 —— 别硬啃一个不划算的优化。
这次用在哪:想让图标字体只保留用到的部分来省体积,实测只压到 97.6%, 于是直接放弃,改成内嵌完整字体(代价 231 KB,换来零外部依赖)。
每次改动不覆盖原文件,而是另存成一个名字里带「这次加了什么」的新文件。 出问题能拿原文件重新改,也能一眼看出每个文件的来历。
这次用在哪:贯穿全程。
从 worker原版0915.js → …改版-去作者+PWA.js → …加回收站版本历史打包下载.js,
每一版都在,都能回退。
| 当前文件 | worker家人-0928.js |
| 体量 | 640,621 字节 / 9,269 行(其中 231 KB 是内联字体) |
| API 路由 | 43 条 (+ 5 条 PWA) |
| 客户端兼容性 | 客户端调用的 31 个接口 —— 一条不缺 ✓ |
| 语法校验 | 通过 ✓ |
| 页面出口收口 | 10 处全走统一函数,裸写 text/html 的 0 处 ✓ |
| 原文件备份 | worker家人-0927.js 保持不动 |
node tools/run-all-tests.mjs 一键跑完。tools/api-diff.mjs,缺口 0)也已通过。frame-src 'none',会把预览用的 iframe 直接拦掉
(本地已改成 'self',部署新版本时会一并解决)。
另一层疑点是:预览用的是「下载接口」,而响应头写的是「附件」,按浏览器的规矩可能变成下载而不是直接看。
(无头浏览器没有 PDF 插件,测不出差别,需要实机点一下)STORAGE_NODE_TOKEN。script-src 必须带 'unsafe-inline',因为全站用的是内联 onclick。
要收紧就得把 164 处图标和大量内联事件重写成监听器,工作量大、收益有限,往后放。