本地进度规范化的边界:BOM、真值转换与导出假设

举报
黑森鱼 发表于 2026/11/14 09:37:59 2026/11/14
【摘要】 在 Node.js 中对排查清单的纯函数做边界实验:带 BOM 的合法 JSON 会被静默清空;症状勾选用 !! 判断,字符串 false 也会变成已选;步骤状态先经 String() 再比对,数组 ['checked'] 会被收成合法状态。备注按 UTF-16 码元截到 2000,导出函数并不重复做规范化。这些是核心逻辑在 Node 下的实测,不能当成浏览器验收。

离线排查清单把进度恢复和 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,需要改源码并补测;本文只记录现行实现,未修改项目代码。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。