题干与适用场景
一个多租户项目管理 Web 应用同时支持服务端渲染与离线编辑。它需要处理四类状态:登录会话要能被服务端识别并跨标签页工作;主题和语言偏好要在下次访问时保留;多步骤表单草稿只属于当前标签页,关闭标签页后可以丢失;最多 10000 条结构化任务记录和图片附件要支持离线查询、修改并在恢复网络后同步。
请在 Cookie、localStorage、sessionStorage 和 IndexedDB 之间分配这些数据,并说明 XSS 与 CSRF、同源隔离、SSR 首屏一致性、多标签页并发、配额与驱逐、数据库版本升级、退出登录与本地清理,以及离线同步的处理方式。
10000 条记录是用于迫使候选人讨论结构化查询、异步访问和同步协议的场景约束,不代表浏览器保证的容量。题目的核心能力是依据 Web 平台语义做前端状态持久化决策,因此归为 frontend。
面试官考察点
第一,看候选人是否先问“谁要读取、要活多久、数据是什么形态、泄漏后果是什么、谁是事实来源”,再选择 API。只背容量表无法解释 SSR、认证和离线一致性。
第二,看能否区分持久化与安全。localStorage 持久并不等于适合保存会话标识;同源脚本可以读写它,XSS 既能窃取也能篡改其中的数据。HttpOnly Cookie 可以阻止 JavaScript 读取会话标识,但不能阻止已运行的恶意脚本借用户身份发请求。
第三,看是否理解 Cookie 的双向风险。浏览器会按匹配规则随请求发送 Cookie,服务端渲染与认证因此方便;同样的自动发送也要求 CSRF 防护。SameSite 是防线之一,不能替代来源校验、CSRF token 和敏感操作再认证等措施。
第四,看能否把 IndexedDB 当作会失败的本地副本。它适合异步保存结构化对象、索引和 Blob,但不会自动与服务器同步;配额、驱逐、清理、升级阻塞和并发冲突都要由应用处理。
第五,看是否能给出可执行验证矩阵,而不是以“刷新后还在”作为唯一测试。
回答前需要澄清的问题
- 服务端首屏需要哪些值? 若 SSR 必须按登录态、语言或主题输出一致 HTML,这些值需要服务端可读来源,或需要接受客户端挂载后的切换。
- 认证形态是什么? 本题默认服务端会话。若改为纯 API token,还要明确 token 的签发、轮换、吊销和跨站请求模型。
- 草稿真的应按标签页隔离吗? 若用户期望关闭后恢复或跨设备继续,
sessionStorage就不满足需求,应改用服务端或 IndexedDB。 - 离线数据是否含敏感内容? 共享设备、XSS、浏览器配置文件和本地备份会改变允许落盘的数据范围。
- 10000 条记录如何查询和更新? 需要按项目、更新时间或同步状态检索时,索引与事务比简单键值更重要。
- 服务器是否为最终事实来源? 本题假设服务器权威,本地副本可以重建;若离线创建的数据不可重建,就要提高持久化、导出和冲突处理等级。
- 冲突如何定义? 同一记录多设备修改可以采用版本拒绝、字段合并或业务优先级,存储 API 本身不会替应用决定。
- 退出登录是否需要清除全部租户数据? 多账号共用浏览器时,应按用户和租户命名空间清理,避免下一位用户看到上一位用户的副本。
30 秒回答框架
“我按读取方、生命周期、数据形态、敏感度和事实来源分配。认证使用服务端会话和带 Secure、HttpOnly、合适 SameSite 的 __Host- Cookie,并继续做 CSRF 防护。小型非敏感偏好放 localStorage,SSR 需要时提供服务端可读初始值;可丢弃的单标签页草稿放 sessionStorage;结构化记录与图片放 IndexedDB,使用索引、事务、版本化 schema 和 outbox。服务器保持权威,同步用版本与幂等键并显式解决冲突。所有本地存储都按可清除、可配额失败处理,并测试 XSS、CSRF、多标签页、驱逐、阻塞升级、重连和退出清理。”
分步骤深入解答
第一步:用五个问题建立选择矩阵
先为每类数据回答五个问题:读取方是服务端、当前标签页还是同源所有页面;生命周期是一次渲染、标签页、浏览器会话还是跨会话;数据是小字符串、结构化对象还是 Blob;泄漏或篡改后果多大;最终事实在服务器还是本地。
由此得到本题的初始分配:
登录会话 -> 服务端会话 + HttpOnly Cookie 中的随机会话标识
主题与语言 -> localStorage;SSR 必需时另有服务端可读初始值
单标签页草稿 -> sessionStorage
离线记录与图片 -> IndexedDB + 应用层同步协议这不是按“哪个容量大”排序。Cookie 的关键能力是随请求到达服务端;sessionStorage 的关键边界是顶层浏览上下文;IndexedDB 的关键能力是异步事务、结构化对象和索引。
第二步:让认证状态由服务器权威管理
浏览器只保存高熵、短生命周期的会话标识,真实权限、过期时间和吊销状态在服务端。Cookie 使用 Secure、HttpOnly、适当 SameSite,并尽量采用不带 Domain、路径为根的 __Host- 形式。退出、密码修改和风险事件都在服务端使会话失效,不能只删除浏览器值。
HttpOnly 降低会话标识被脚本直接读取并带走的风险,但同源 XSS 仍可能在当前页面发起已认证操作。因此仍要做输出编码、CSP 等 XSS 防护,并对高风险操作再认证。Cookie 会自动随匹配请求发送,所以状态变更请求还应验证来源并使用 CSRF token;SameSite 不能作为唯一控制。
把会话标识放进 localStorage 会让任何成功执行的同源脚本直接读取它。客户端加密也不能自动解决问题:若解密密钥同样可被页面 JavaScript 取得,同源 XSS 仍能拿到明文或调用解密流程。
第三步:用 localStorage 保存小型非敏感偏好
localStorage 按来源隔离,键和值是字符串,跨浏览器会话保留;它的读写是同步的。主题、语言、表格密度等小型偏好适合放在这里,但不能假设永久存在,用户清理站点数据、隐私模式结束或浏览器策略都可能移除它。
服务端不能直接读取 localStorage。若 SSR 必须在第一帧采用正确语言或主题,可以把经校验的偏好同步到服务端可读 Cookie 或用户资料,并规定一个权威来源;若只在客户端读取,则先输出稳定默认值,挂载后再切换,避免服务端 HTML 与首次客户端渲染不一致。
不要用 localStorage 存放大量记录后反复 JSON.parse 整个数组。同步序列化和主线程 I/O 会随数据增长影响交互,而且无法获得 IndexedDB 的事务与索引。
第四步:只把可丢弃的标签页状态放进 sessionStorage
sessionStorage 同时按来源和顶层浏览上下文分区,在同一标签页刷新后仍保留,关闭标签页或窗口后清除。因此它适合当前标签页的多步骤表单位置和可丢弃草稿,不适合跨标签页购物车、长期恢复或跨设备状态。
一个容易漏掉的边界是 opener:从已有页面打开的新页面可能先得到 opener 的 sessionStorage 副本,此后两者分别变化。若草稿绝不能被复制,应移除 opener 关系或在草稿中加入每标签页生成的实例 ID,并在恢复时校验。
sessionStorage 也能被同源脚本读写,不能因为“关闭即清除”就存放长期凭证。保存草稿前还应判断字段是否包含不应落盘的密码、支付或医疗信息。
第五步:用 IndexedDB 建立可重建的离线副本
IndexedDB 提供异步请求、事务、对象存储、键和索引,也能保存 Blob,适合本题的大量结构化记录和图片。可以按 tenantId + userId 隔离数据,建立任务、附件和 outbox 三类对象存储,并按项目、更新时间、同步状态建立实际查询需要的索引。
数据库升级要使用明确版本和逐版本迁移。打开新版本时,旧标签页的连接可能阻塞升级;应用应监听 versionchange 并关闭旧连接,向旧页面提示刷新,同时处理 blocked,不能让发布后的页面永久卡在启动阶段。
IndexedDB 是本地数据库,不是同步服务。写入成功只说明本地事务提交。应用仍要记录本地版本、服务器版本、操作 ID 和同步状态,并保证重试幂等。
第六步:显式设计离线同步和多标签页冲突
每次离线变更与业务记录放在同一 IndexedDB 事务中,同时追加 outbox。联网后 worker 按操作 ID 上传;服务器以幂等键去重,并比较记录版本。成功确认后再在事务中更新服务器版本并删除 outbox 项,避免“记录已改但待同步操作丢失”。
冲突策略应来自业务:低风险偏好可最后写入者生效,任务状态可使用版本不匹配后提示用户合并,资金或权限变更则应由服务器拒绝离线提交。storage 事件或 BroadcastChannel 可以通知其他标签页刷新数据,但通知不等于锁,也不能替代 IndexedDB 事务和服务器版本检查。storage 事件不会在执行写入的同一文档触发。
第七步:把配额、驱逐和清理当成正常失败
浏览器存储配额因浏览器、设备和模式而异。IndexedDB 通常属于来源的 best-effort 存储,可能被用户清除或在存储压力下被驱逐;写入也可能抛出配额错误。应用可用存储估算观察使用量,对不可重建数据谨慎请求持久化,并始终捕获事务与配额失败。
策略应包含附件大小上限、最近最少使用清理、服务器确认后压缩或删除旧版本,以及“本地空间不足”的可恢复提示。退出登录时先在服务端吊销会话,再关闭数据库连接并删除该用户与租户的 IndexedDB、偏好和草稿;同时通知其他标签页退出。删除本地数据本身不能替代服务端吊销。
第八步:用失败矩阵验证边界
验证至少覆盖:SSR 首屏与水合一致;刷新、复制标签页、新开标签页和关闭标签页;另一个标签页退出;XSS 场景下脚本可读范围;跨站状态变更请求;隐私模式;站点数据被清理;配额超限;旧标签页阻塞数据库升级;两个标签页同时编辑;离线重试、重复响应、乱序响应和冲突;切换租户后旧数据不可见。
通过标准不只是“数据还在”。认证必须可吊销,敏感值不能暴露给 JavaScript,草稿遵守标签页边界,离线写入不会丢失 outbox,重复同步不产生重复业务效果,升级可恢复,空间不足有降级路径,服务器可以重建本地副本。
高质量示范回答
“我不会先按容量选,而是先看读取方、生命周期、数据模型、信任边界和事实来源。登录会话需要服务端和所有标签页识别,所以真实会话保存在服务端,浏览器只持有 Secure、HttpOnly、合适 SameSite 的 __Host- Cookie。它减少 token 被 JavaScript 读取的机会,但 XSS 仍能代用户操作,Cookie 自动发送也需要 CSRF token、来源校验和敏感操作再认证。
主题与语言是小型非敏感偏好,放 localStorage;如果 SSR 第一帧必须使用它们,我会同步一份经过校验的服务端可读偏好并规定权威来源。当前标签页的可丢弃表单草稿放 sessionStorage,刷新可恢复,关闭即清除;同时处理新页面从 opener 获得初始副本的边界。
10000 条任务和图片放 IndexedDB。数据库按用户与租户隔离,使用版本化 schema、索引和事务。每次业务修改与 outbox 在同一事务提交,恢复网络后用操作 ID 幂等上传,服务器比较记录版本并返回确认或冲突。跨标签页通知只触发重新读取,最终一致性由本地事务与服务器版本控制保证。
我会把所有本地存储视为可清除、可配额失败且可被同源脚本访问。实现配额捕获、附件清理、升级时关闭旧连接,以及先吊销服务端会话再清理用户命名空间的退出流程。最后测试 SSR、水合、标签页复制、XSS、CSRF、配额、驱逐、阻塞升级、并发编辑、离线重试和租户切换。”
常见错误
- 按容量表机械选择 → 忽略服务器读取、标签页边界和数据权威 → 先使用五维选择矩阵。
- 把会话标识放进
localStorage→ 同源 XSS 可直接读取并带走 → 使用服务端会话和受保护 Cookie,并继续防 XSS。 - 认为
HttpOnly消灭 XSS → 恶意脚本仍可发起已认证操作 → 把防窃取与防代操作分开。 - 只设置
SameSite就不做 CSRF → 浏览器策略、请求类型和业务流程仍有边界 → 组合 token、来源校验和再认证。 - 服务端直接依赖
localStorage→ SSR 根本无法读取浏览器存储 → 提供服务端可读初始值或接受挂载后切换。 - 认为
sessionStorage新标签页必定为空 → opener 可能带来初始副本 → 移除 opener 或加入标签页实例标识。 - 把大数组塞进
localStorage→ 同步解析、整包重写且无索引事务 → 改用 IndexedDB。 - 认为 IndexedDB 会自动同步 → 本地提交与服务器确认是两件事 → 实现 outbox、幂等、版本和冲突策略。
- 用跨标签页通知当分布式锁 → 消息可能延迟,且不能决定服务器冲突 → 通知后重读,用事务和版本做正确性控制。
- 假设本地数据永久存在 → 清理、驱逐、隐私模式和配额都可能让它消失 → 允许重建并处理写入失败。
- 退出时只清 Cookie → IndexedDB 和偏好仍可能泄漏给下一账号 → 服务端吊销后按用户与租户清理全部本地副本。
追问及应对
追问一:把 token 加密后放 localStorage 可以吗?
如果页面 JavaScript 能取得解密密钥或调用解密流程,成功运行的同源恶意脚本通常也能做到,所以这不能解决本题的 XSS 会话窃取问题。更稳妥的边界是服务端会话加 HttpOnly Cookie,并结合 XSS、CSRF、轮换与吊销控制。客户端加密只有在密钥不暴露给同一攻击面时才解决对应威胁。
追问二:用户要求草稿关闭标签页后还能恢复怎么办?
生命周期约束已经变化,sessionStorage 不再适用。非敏感且仅本机恢复的草稿可放 IndexedDB;需要跨设备或不可丢失时应同步到服务器。无论选择哪种,都要定义保留期、账号与租户隔离,以及敏感字段不落盘规则。
追问三:两个标签页同时修改同一离线任务怎么办?
每条记录保存服务器版本和本地修订号,写入 IndexedDB 时使用事务。标签页通过 BroadcastChannel 或数据变更通知其他页面重读,但服务器在同步时仍以版本做条件更新。版本冲突后按业务选择字段合并、提示用户或拒绝;不能把“最后收到通知的标签页”当作事实来源。
追问四:IndexedDB 升级被旧标签页阻塞怎么办?
旧连接监听 versionchange 后主动关闭并提示刷新;新页面监听 blocked,展示可恢复状态而不是无限等待。迁移保持逐版本、幂等且能处理部分历史数据。发布前用一个旧版本标签页保持连接,再打开新版本验证关闭、提示和迁移路径。
追问五:浏览器空间不足时如何降级?
捕获配额与事务错误,先停止缓存新附件,再清理已由服务器确认且可重建的旧附件和历史版本。保留未同步 outbox 的优先级高于可下载缓存。仍不足时明确提示用户联网同步或释放空间,不能静默宣称保存成功。
追问六:退出登录的正确顺序是什么?
先请求服务端吊销当前会话,使已复制的 Cookie 或仍打开的标签页也失效;随后通知其他标签页,停止同步任务,关闭 IndexedDB 连接,按用户和租户删除数据库、草稿与偏好,最后跳转到未登录状态。网络失败时不能只做本地“假退出”,应标明会话吊销尚未确认并限制继续操作。