Repository Wiki
jason5ng32/MyIP

个人安全检查清单与本地进度

个人安全检查清单通过分类建议、优先级和三态标记,帮助用户记录个人网络安全实践的落实情况。核心组件 SecurityChecklist 将进度按检查项 slug 保存在浏览器 localStorage 中,再从当前语言的数据集重建展示列表。

目的与范围

本页覆盖清单数据的按需加载、分类导航、完成与忽略操作、进度计算、本地恢复和重置,以及与应用事件的连接点。它是一份人工自查记录,不能把勾选结果当作自动安全检测或安全认证。

IP、DNS 泄漏、浏览器指纹等检测机制不属于本页范围;成就规则、公共国际化回退器和通用抽屉组件只讨论清单侧已核实的调用方式。本次证据集中于 SecurityChecklist.vue 的模板与主要业务逻辑;共享加载器、事件消费者及独立测试的内部实现未在本次阅读中核实。

概述

界面分为三个相互联动的区域:

  • 总览面板:显示总数、已完成、已忽略、未完成数量,以及分类堆叠进度条和四种优先级圆环。
  • 分类导航:选择当前安全领域,显示该领域的完成比例;达到计算意义上的 100% 时显示勾选图标。
  • 分类详情:显示标题、描述、可展开介绍、条目优先级,以及完成按钮和忽略开关。用户可以筛选全部、未完成、已完成或已忽略条目。

默认分类是 authentication,优先级的显示顺序为 Basic、Optional、Essential、Advanced。这些值是程序数据标识,而不是可自由翻译的状态值。页面统计来自实际加载的数据,不在统计函数中写死条目数量。

来源:SecurityChecklist.vue、状态与优先级定义。

架构与职责

Loading diagram...

Source: SecurityChecklist.vue

这里的状态分为两层:fullList 是按分类组织、携带布尔状态的展示模型;slugs 是按检查项标识组织的持久化模型。持久化只保存状态映射,不保存翻译文案,因此切换语言时可以重新加载内容,同时通过相同 slug 恢复进度。

代码中没有为进度保存调用后端接口。不过,组件确实调用 trackEvent 和 emitAppEvent;“进度存于本地”不等于整个应用完全没有遥测或事件处理。相关函数是否最终联网,需要另行核实其实现。

数据模型与稳定标识

对象已核实字段或形态职责
分类输入slug、title、description、intro、checklist提供分类内容
条目输入slug、point、priority,以及保留的其他字段提供建议内容和优先级
fullList以分类 slug 为键的对象供导航、筛选、统计使用
条目运行时状态checked: boolean、ignored: boolean由持久化字符串转换得到
slugs检查项 slug → 状态字符串写入浏览器的完整映射
categories分类 slug 数组控制导航和总览遍历顺序

持久化状态只有三个正常值:空字符串表示未完成,checked 表示已完成,ignored 表示忽略。初始化对已有键做合并,对新条目补空字符串,再写回整个映射;没有在这条路径上删除旧键或增加数据版本号。

因此,条目 slug 必须跨语言稳定且全局唯一。分类自身虽有命名空间,持久化键却没有附加分类前缀:同名条目会共享同一个保存位置。修改条目 slug 会使其按新条目处理,旧记录仍可能留在存储中。

来源:SecurityChecklist.vue。

加载与恢复流程

按需加载当前语言

组件通过 import.meta.glob 收集清单语言包加载入口,并创建 Map 缓存传给 loadLocaleDataset。源码注释说明每种语言的数据集约为 30 KB gzip,因此选择按需加载,避免将所有清单内容放入初始国际化包。共享加载器的注释说明它会沿语言回退链尝试加载,但具体回退顺序、缓存命中与异常处理细节未在本次源代码阅读中核实。

实际加载入口如下:

javascript
1const loadSecurityChecklist = async () => { 2 fullList.value = null; 3 const loc = locale.value; 4 const pack = await loadLocaleDataset(securityDataLoaders, loc, securityDataCache); 5 if (loc !== locale.value) return; // stale load — the locale changed mid-flight 6 if (!pack) { 7 console.error('Security checklist dataset unavailable in every locale'); 8 return; 9 } 10 securityChecklist.value = pack.data; 11 fullList.value = initSecurityList(pack.data); 12};

Source: SecurityChecklist.vue

首先清空 fullList,模板据此显示 Spinner;加载完成后,只有请求开始时的语言仍等于当前语言才接受结果。随后保存原始数据到 securityChecklist,并调用 initSecurityList 生成展示模型。

初始化和语言切换

Loading diagram...

Source: SecurityChecklist.vue、SecurityChecklist.vue

挂载时等待加载结束,再通过 300 ms 定时器调用 changeList('authentication', false);语言改变时等待重新加载,然后调用 changeList(currentList.value, false)。后一条路径保留当前分类,但会清除筛选条件。两条路径均关闭分类切换滚动。

注意:加载函数没有通过返回值告诉调用者“成功”“过时”或“失败”。即使内部提前返回,外层仍继续执行 changeList。这对加载失败和快速切换语言具有实际影响,详见后面的边界说明。

从本地恢复状态

下面的源码展示了读取与单项更新的全部逻辑:

javascript
1const loadLocalSlugs = () => { 2 const storedSlugs = localStorage.getItem('securityChecklistSlugs'); 3 if (storedSlugs) { 4 const parsedSlugs = JSON.parse(storedSlugs); 5 Object.keys(parsedSlugs).forEach(key => { slugs.value[key] = parsedSlugs[key]; }); 6 } 7}; 8 9const updateSlugs = (slug, value) => { 10 slugs.value[slug] = value; 11 setLocalSlugs(); 12};

Source: SecurityChecklist.vue

恢复不是替换整个响应式对象,而是将已保存键逐个合并进去。初始化只将严格等于 checked 或 ignored 的值识别为对应状态;其他字符串不会产生这两个布尔标记。每次单项更新调用的 setLocalSlugs 都用 JSON.stringify(slugs.value) 覆盖同一个存储键。

条目操作、筛选与重置

三态转换规则

Loading diagram...

Source: SecurityChecklist.vue

忽略状态优先于完成状态:忽略一个已完成条目会清除完成标记,取消忽略后也不会恢复之前的完成状态。模板禁用忽略条目的完成按钮,函数内部再做一次判断,形成 UI 与业务逻辑两层约束。

javascript
1const ignoreItem = (item) => { 2 item.ignored = !item.ignored; 3 item.checked = false; 4 updateSlugs(item.slug, item.ignored ? 'ignored' : ''); 5}; 6 7const checkItem = (item) => { 8 if (item.ignored) return; 9 item.checked = !item.checked; 10 updateSlugs(item.slug, item.checked ? 'checked' : ''); 11 // Achievement rules (SurfaceCheck / HalfwayThere / FullySecured) live in 12 // data/achievement-rules.js. 13 emitAppEvent('securitychecklist:progress', { 14 checked: checkedItems.value, 15 total: totalItems.value, 16 }); 17};

Source: SecurityChecklist.vue

checkItem 在保存成功后发布 securitychecklist:progress,载荷包含全清单的完成数和总数。取消完成也会发布该事件;ignoreItem 不发布它。事件中的 total 没有扣除忽略项,不能直接用它替代 UI 的有效分母。源码注释提及三个成就名称,但具体解锁阈值不能从本组件推断。

筛选和分类导航

filterChecklist(filter) 仅针对当前分类处理四种筛选:

筛选值条件
all直接使用当前分类的原始数组
ignoreditem.ignored
checkeditem.checked
unchecked!item.checked && !item.ignored

非 all 筛选调用数组 filter 生成新数组,但条目仍引用原对象。因此状态修改会更新条目显示;这几个操作函数本身没有重新运行筛选,不能假定条目修改后会立即从当前筛选结果中移除。

changeList(listName, shouldScroll = true) 先设置展示数组和当前分类,再重置筛选为 all。只有允许滚动且分类发生变化才记录 ChangeList 事件。滚动通过 nextTick 寻找 #checklist 最近的 overflowY 为 auto 或 scroll 的祖先;已完整可见时不滚动,否则只对该祖先调用 scrollBy,保留 8 px 偏移。

该实现的注释明确指出:避免 scrollIntoView 向页面根滚动器传播,防止 iOS Safari 下与抽屉的 body 滚动锁发生冲突。

来源:SecurityChecklist.vue。

重置的真实语义

重置按钮直接调用 resetAllslugs(),所读模板没有确认步骤。函数记录 Reset 事件,将所有已知 slugs 键设为空字符串,保存并重新初始化数据,保留当前分类,最后恢复 all 筛选。

这不是删除存储键,也不是只重置当前分类;旧数据遗留的键也会被清空。重置函数没有发送 securitychecklist:progress。

来源:SecurityChecklist.vue、重置实现。

进度统计:三个不同口径

数量与有效完成率

countItems({ action, category, priority }) 根据分类选出条目;若传入 priority,先按优先级精确过滤,再执行对应统计。数量和百分比的计算分支如下:

javascript
1const actionMap = { 2 total: (items) => items.length, 3 ignored: (items) => items.filter(item => item.ignored).length, 4 checked: (items) => items.filter(item => item.checked).length, 5 percentage: (items) => { 6 const total = items.length; 7 const ignored = items.filter(item => item.ignored).length; 8 const checked = items.filter(item => item.checked).length; 9 const denominator = total - ignored; 10 return denominator === 0 ? 100 : Math.round((checked / denominator) * 100); 11 }, 12};

Source: SecurityChecklist.vue

  • 总览未完成数为:totalItems - checkedItems - ignoredItems。
  • 单分类有效完成率为:已完成数 ÷(总数 − 忽略数),再四舍五入到整数。
  • 有效分母为零时返回 100,包括分类中所有条目都被忽略的情况。

因此,导航上的勾选图标表示该分类计算所得百分比等于 100,不一定意味着每个原始条目都已完成。四舍五入也意味着比例接近 100% 时可能显示勾选。

总览堆叠条与优先级圆环

展示位置分母忽略项的表现
顶部分类堆叠条分类原始总数单独显示忽略段,完成段与忽略段分别计算宽度
当前分类详情进度条分类总数减忽略数不进入有效分母
优先级圆环全分类同优先级总数减忽略数分母为零时显示替代圆环和“已忽略”标签

总览堆叠条由 getProgressStyle(category)(action) 计算宽度,不使用 percentage 分支。优先级圆环则分别累计数量后做比值,其分母由 priorityDenom(priority) 提供。

一个容易误用的接口细节:countItems 对各分类的结果执行求和。category: 'all' 配合 percentage 会得到分类百分比之和,不是全局完成率。现有模板只对单分类调用百分比分支;全局圆环使用完成数量和有效总数,绕开了这一问题。

来源:SecurityChecklist.vue、统计归约逻辑。

配置与内部调用参考

本组件没有在所读实现中声明服务端配置、环境变量或 HTTP API。下面列的是内部约定,不是可直接从外部传入的配置接口。

约定类型初始值或固定值影响
本地存储键字符串securityChecklistSlugs保存完整条目状态映射
currentList响应式字符串authentication初始分类,挂载后也显式选中该分类
filterTag响应式字符串all切换分类或重置后恢复此值
priorities字符串数组Basic、Optional、Essential、Advanced圆环顺序及对应的标签样式
securityDataCacheMap空 Map传入共享语言包加载器
挂载后导航延迟毫秒300数据加载返回后的分类初始化延迟
进度颜色十六进制颜色完成 #22c55e,信息 #0ea5e9圆环使用的颜色
圆环背景十六进制颜色深色 #262626,浅色 #e5e5e5随 store.isDarkMode 切换

来源:SecurityChecklist.vue、生命周期。

内部函数契约

这些是 JavaScript 组件内部函数,并非经过类型声明的公共 SDK。表中返回行为依据实现归纳,不补造 TypeScript 签名。

实际函数形式输入与返回行为前置条件或副作用
loadSecurityChecklist = async () =>无参数;异步完成,没有显式业务返回值清空列表、加载语言包、初始化;过时或缺包时提前返回
initSecurityList = (securityChecklist) =>分类数组;返回按分类 slug 建立的对象读取并写回存储;输入需具备 checklist.map 所需结构
updateSlugs = (slug, value) =>条目标识与状态值;无显式返回值修改内存并同步写入本地存储
checkItem = (item) =>带状态和 slug 的条目;无显式返回值切换完成状态,保存并发布进度事件;忽略项直接返回
ignoreItem = (item) =>条目;无显式返回值切换忽略状态,清除完成标记并保存
countItems = ({ action, category, priority }) =>action 为统计动作,category 为 slug 或 all,priority 可省略;返回数字依赖非空 fullList;未知动作按零处理,缺少分类清单时跳过
filterChecklist = (filter) =>四种筛选字符串;无显式返回值更新筛选标签与数组;未知值只改标签,不更新数组
changeList = (listName, shouldScroll = true) =>分类 slug 与可选滚动开关;无显式返回值分类必须存在;重置筛选,可记录行为事件及滚动
resetAllslugs = () =>无参数;无显式返回值清空所有已知状态,重建当前分类视图

相关函数没有显式声明错误类型,也没有在本组件这些路径上捕获存储、解析或数据结构错误;不要据此认为调用永远成功。

来源:SecurityChecklist.vue。

失败模式、边界与并发

存储损坏或不可写

JSON.parse 没有 try/catch,解析结果也没有做对象结构校验。损坏的 JSON 会中断初始化;即使是合法 JSON,也不代表其形态满足 Object.keys 和状态映射要求。初始化结束时的保存、用户操作时的保存都可能因浏览器存储访问或容量限制而失败,组件没有降级到纯内存模式的分支。

用户操作先改内存状态,再调用存储写入。如果保存抛错,可能出现“当前界面已变化,但刷新后没有保留”的不一致;checkItem 后面的进度事件也不会执行。排查时应区分列表加载失败、状态解析失败和保存失败,而不是将三者都归因于网络。

语言切换竞态和加载失败

  • 过时结果防护有限:函数通过比较 loc 与当前语言拒绝不同语言的旧结果,但不是请求序号或取消机制。快速 A → B → A 切换时,旧 A 请求仍可能通过检查。
  • 后续导航没有成功守卫:旧请求提前返回后,语言监听器仍调用 changeList;若新请求尚未重建 fullList,可能访问空值。
  • 全部语言数据不可用:加载函数记录控制台错误并保持 fullList 为空;没有独立错误状态或重试按钮。外层挂载定时器和语言监听器仍可能继续执行导航。
  • 数据集需要保持分类一致:changeList 直接访问分类的 checklist;默认分类不存在或语言包缺少当前分类时没有回退处理。初始化只向 categories 追加尚未出现的分类,也没有清除旧分类的步骤。

这些结论来自控制流审阅,不是本次执行了故障注入测试。来源:加载实现、分类初始化与导航、生命周期调用方。

多标签页与隐私边界

写入策略是整份 slugs JSON 覆盖,而不是逐项原子合并。所读路径没有跨标签页冲突协调逻辑:不同页面实例各自持有旧映射并写回时,有覆盖其他实例更新的风险。不能将“刷新后恢复”描述成“多标签实时同步”。

保存值也没有加密步骤,并且没有账户、用户 ID 或清单版本参与存储键。它应被理解为当前站点浏览器存储中的自查记录,而不是账户级、可跨设备恢复的安全档案。清除站点数据后,不应期望组件能从服务器恢复进度。

来源:SecurityChecklist.vue。

统计和展示边界

  • 单分类百分比对零有效分母做了处理,但 getProgressStyle 直接除以分类总数,没有处理空分类的零分母。
  • 已忽略项不计入未完成数;“全部忽略”和“全部完成”在单分类百分比上都可以是 100%,但顶部数量与优先级替代圆环仍可区分它们。
  • 条目说明展开状态使用 checklistInfoOpen[index],而不是条目 slug。筛选或分类切换改变索引含义后,展开位置可能对应到其他条目,不能把它当成按条目保存的阅读状态。

来源:模板索引与展开行为、展开状态、统计实现。

性能、扩展与验证建议

性能与运行注意事项

按语言懒加载降低初始包负担,但首次打开或切换语言要等待数据加载。Map 被提供给共享加载器,不能在未阅读该加载器的情况下承诺缓存有效期或并发请求去重。

统计函数没有预先维护分组索引:每次按所选分类遍历条目,并通过 filter 计算状态数量。总览模板会按分类、优先级重复调用它。当前实现简单直观;若数据规模显著扩大,可考虑在保持统计口径不变的前提下预计算聚合结果。

每次勾选和忽略都会同步序列化、写回完整映射,没有防抖或批量保存。这使保存时机明确,但写入成本随记录规模增长。挂载后的 300 ms 是导航定时器,不是网络超时或重试间隔。

来源:按需加载说明、保存实现、统计和挂载。

安全扩展的约束

  1. 新增条目:使用稳定、全局唯一的 slug;不要通过翻译改变状态键。
  2. 新增分类:保持所有语言包的分类结构兼容;图标映射未命中时 getChecklistIcon 回退到 Info。
  3. 新增优先级:同时考虑 priorities、标签文案和 priorityBadgeClass。未出现在优先级数组中的值不会自动增加圆环;未命中样式映射时返回空字符串。
  4. 新增持久化状态:同步修改初始化转换、状态切换、筛选和分母规则;当前只有三种状态约定,没有通用状态机接口。
  5. 扩展事件消费:不要假定忽略、重置、恢复本地进度都会发布 securitychecklist:progress。若消费者需要完整快照通知,应显式设计额外触发点。

来源:图标与优先级、状态转换和事件。

测试证据与建议

本次没有读取到清单专用测试文件,不能声称下列场景已有自动化覆盖。基于实现,建议验证:

  • 勾选、取消勾选、忽略、取消忽略、刷新恢复,以及忽略已完成项时是否清除完成状态。
  • 全部忽略、空分类、百分比四舍五入、优先级无有效条目时的展示。
  • 保存内容损坏、存储不可访问、写入失败时 UI 与持久化的一致性。
  • 快速语言切换、缺失默认分类、所有数据包加载失败的后续导航。
  • 在已完成或未完成筛选中改变状态后,列表成员是否符合产品预期。
  • 多标签页交替操作导致的整份映射覆盖,以及 iOS 抽屉内切换分类时的滚动稳定性。

相关链接

公共国际化回退、成就解锁和检测工具的实现应分别查阅其对应专题;本页不将这些外部模块未核实的行为纳入清单自身的保证。

Sources

(1 files)