本地进度规范化的边界:BOM、真值转换与导出假设
离线排查清单把进度恢复和 Markdown 导出放在 checklist-core.js 的纯函数里。仓库已有 7 项 Node 单测,覆盖分流约束、非法状态丢弃和损坏 JSON 等基本情况。本次在 Node.js v24 下只读引用这份模块,追加边界输入,记录实际输出。这不是浏览器测试:没有打开 index.html,也没有读写真实的 localStorage。源码见 dev-startup-checklist。
一、损坏文本会被清空,带 BOM 的合法对象也不例外
parseStorageJson 对空串、残缺文本、null、数组、数字、布尔和普通字符串都会回到空进度:没有已选症状,备注为空,步骤全是「待验证」。它用 try/catch 包住 JSON.parse,解析失败时不抛给调用方。
这在大多数坏数据上是稳妥的,但有一个容易忽略的例子。把合法对象写成 \uFEFF{"symptoms":{"blank":true}},也就是前面多了一个 BOM 时,JSON.parse 会抛出 SyntaxError。核心函数把它当成损坏输入,已选症状被清空。对象本身是可识别的,只是多了文件头标记,结果却与空串相同。若有工具按「带 BOM 的 UTF-8」写出本地进度,恢复时会表现为进度丢失,而不是报出解析错误。
非对象值交给 normalizeState 时,同样得到空进度,包括 null、数字、数组和布尔。
二、症状按 !! 判断,步骤状态会先做成字符串
勾选症状的实现是:
state.symptoms[s.id] = !!(raw.symptoms && raw.symptoms[s.id]);
因此 true 仍为已选,false、0、"" 为未选;但 "false"、"0"、1、[]、{} 都会变成已选。实验里,一份合法 JSON 写着 "blank": "false",经过 parseStorageJson 后,blank 变为 true。这不是解析失败,而是真值转换与字面含义不一致。
未知症状名、未知步骤 id,以及顶层额外字段(实验里用了占位字段名,不是真实凭据)都不会进入规范化后的对象。JSON 里即便出现 __proto__ 或 constructor,已知症状键仍只有三类,Object.prototype 也未被写入新字段。
步骤状态的处理是另一条路径:先 String(value),再与 pending、checked、na 比较,其余改回「待验证」。于是 "done"、"CHECKED"、"Checked"、带空格的 " na "、数字 1、普通对象都会回到 pending。可是 String(["checked"]) 的结果恰好是 "checked",因此数组 ["checked"] 会被收成合法的「已检查」。这是白名单之前多了一次字符串化带来的缝隙,仓库原有单测用的是字符串 "done",没有覆盖到这一点。
未选中症状上的步骤状态会留在 state 里,但不计入 countProgress,导出时也不形成对应分区。需要判断某步是否出现在导出中时,应查找 **[已检查]** 这样的步骤标记;模板声明里本来就有「已检查」字样,不能只做全文搜索。
三、备注截断和导出,并不在同一层完成
normalizeState 只在备注为字符串时保留,并用 slice(0, 2000) 截断;数字、对象、数组和布尔都会变成空串。这个 2000 按 JavaScript 的 UTF-16 码元计算。纯汉字超长时,第 2001 个字会被切掉。全部由表情符号组成、长度为 2002 码元的字符串,截到 2000 时仍落在完整字符边界,因为每个表情占两个码元,且从下标 0 开始。但在「一个汉字 + 1000 个表情」(共 2001 码元)时,截断末尾留下孤立高代理项 U+D83D。所以不能笼统说「超长表情一定被劈开」或「一定不会被劈开」,要看切点是否落在代理对中间。
normalizeState 不会去掉备注两端空白;exportMarkdown 在导出时才 trim。因此只含空格和换行的备注,存储后仍有长度,导出会写成「(无)」。导出也不转义备注中的 Markdown:实验把「## 声明」写进备注后,成品里会出现两个同名标题。占位文案 EXAMPLE_NOT_A_REAL_TOKEN 也会原样出现。工具不会扫描或改写备注,分享前仍需人工检查。
更重要的是,导出函数并不会先调用 normalizeState。直接把 2500 个「测」字交给 exportMarkdown,超长备注会完整进入正文,2000 的上限不会在这一层生效。若传入的对象缺少 symptoms,会在读取 state.symptoms.blank 时抛出 TypeError,还走不到备注处理。若对象已有症状表、但 notes 是数字,则会在 trim 处报「不是函数」。updatedAt 若是普通对象,则会被拼成 [object Object]。页面写入路径会先规范化,再导出,通常不会碰到这些输入;它们说明的是纯函数自身的假设,而不是已经在浏览器里复现的故障。
四、测试范围
本次额外实验 50 项,在 Node.js v24 下全部按上述观察成立;仓库原有 7 项单测同时重新执行,全部通过。没有做界面点击、也没有验证不同浏览器对 localStorage 或 textarea.maxLength 的处理,不能把这些结论写成「打开页面后一定如此」。若要收紧行为,例如拒绝带 BOM 的文本、用严格布尔判断症状、在字符串化之前拒绝非字符串状态,或让导出前统一走 normalizeState,需要改源码并补测;本文只记录现行实现,未修改项目代码。
- 点赞
- 收藏
- 关注作者
评论(0)