尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Web-Dev-For-Beginners 银行应用第 4 课:集中式状态管理、localStorage 持久化与数据新鲜度实战指南

Web-Dev-For-Beginners 银行应用第 4 课:集中式状态管理、localStorage 持久化与数据新鲜度实战指南 Web-Dev-For-Beginners 银行应用第 4 课集中式状态管理、localStorage 持久化与数据新鲜度实战指南【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners本篇基于 Web-Dev-For-Beginners 仓库「构建银行应用」系列的第 4 课文档 translations/da/7-bank-project/4-state-management/README.md系统讲解如何将一个登录状态会随页面刷新丢失的简易银行应用改造为具备集中式状态对象、updateState()受控不可变更新、localStorage会话持久化以及「加载即刷新」数据新鲜度机制的成熟 Web 应用。学完后你将掌握与 Redux、Vuex 等状态管理库同源的核心模式单一数据源、受控变更、序列化存储与缓存/服务器一致性平衡并能对照仓库中的参考实现 solution/app.js 逐行验证每个设计决策。一、环境与前置条件本课直接构建在系列前面课程的基础之上动手前需要确认以下环境已就绪已完成「数据获取」一课 7-bank-project/3-data/README.md应用能够正确加载并展示账户数据系统已安装 Node.js用于运行后端 API已本地启动 银行 API 服务处理账户数据操作。API 的启动方式摘自 7-bank-project/api/README.md进入7-bank-project/api目录执行npm install安装依赖再执行npm start即运行node server.js见 7-bank-project/api/package.json 的 scripts 定义服务监听5000端口。验证 API 是否正常运行在终端执行curl http://localhost:5000/api # - 应返回 Bank API v1.0.0这条命令向本地 API 服务器发送 GET 请求、测试连接是否通、并在一切正常时返回 API 版本信息。对照源码 server.js路由GET /直接返回${pkg.description} v${pkg.version}即「Bank API v1.0.0」。需要注意的适用前提源码事实该 API 的数据存储在内存中见 server.js 中的db对象——预置了一个test账户余额 75含 3 笔交易并明确注释「Store data in-memory, not suited for production use」。这意味着服务重启后所有数据都会丢失第 8 节的 curl 实验依赖该test账户存在请勿重启 API 服务器。二、诊断当前实现中的状态问题银行应用从简单的登录表单演进而来典型症状是刷新页面后用户被意外登出、关闭浏览器后一切进度消失、排查问题时要在多个函数里追踪同一个数据被以不同方式修改的痕迹。这些不是代码写错了而是应用从「概念验证」走向「生产就绪」时的自然成长烦恼。诊断实验登录银行应用并进入 dashboard刷新浏览器页面观察登录状态发生了什么。如果被弹回登录页说明你命中了经典的「状态持久化缺失」问题当前实现把用户数据放在 JavaScript 变量里而每次页面加载都会重置这些变量。前一课中那个简单的account变量带来三类问题问题技术原因对用户的影响会话丢失页面刷新会清空 JavaScript 变量用户被迫频繁重新登录更新点分散多个函数直接修改同一状态调试难度越来越高清理不彻底登出时没有清除所有状态引用潜在的安全与隐私隐患逐个修补无法解决根因——需要一套完整的状态管理方案。状态管理本质上在解两个基本问题我的数据在哪里搞清楚手里有哪些信息、它们从哪来界面和数据是否一致确保用户看到的与实际发生的一致。因此本课程的解法是建立集中式状态管理centralized state management由一个统一的组织点负责所有重要数据。三、状态管理架构总览文档将整门课的架构划分为三大板块当前问题会话丢失页面刷新、浏览器关闭、变量重置更新分散多处修改点、难以调试、行为不可预测清理不完整登出状态残留、内存泄漏、安全顾虑。集中式解法统一状态对象单一可信数据源single source of truth、可预测的结构、可扩展的基础受控更新不可变模式、Object.freeze()的使用、函数化变更状态追踪历史管理、调试可见性、变更审计。持久化策略localStorage集成会话连续性、JSON 序列化、自动同步数据新鲜度服务器刷新、过期数据处理、平衡优化存储优化最小化数据、性能导向、安全考量。整个系统的数据流如下与上文示意图对应理解这个数据流的要点把所有应用状态集中在一个位置所有状态变更经过受控函数路由确保界面与当前状态保持同步为数据管理提供清晰、可预测的模式。核心原则专业的状态管理在可预测性、持久性与性能之间取得平衡支撑从简单交互到复杂工作流的可靠体验。两点边界说明继承原文档的提示本课聚焦基础概念复杂应用可用 Redux 等状态库获得更高级能力但这些核心原理是掌握任何状态管理库的前提由状态变更自动驱动 UI 更新涉及响应式编程属于本课之外的进阶主题。四、第一步集中化状态结构步骤 1创建集中式状态对象替换简单的account声明let account null;改为结构化的状态对象let state { account: null };这一步的意义集中所有应用数据于一处预留结构以便日后添加更多状态属性划清状态与其他变量之间的边界建立随应用增长而扩展的模式。步骤 2更新状态的访问方式在register()和login()函数中把account ...替换为state.account ...在updateDashboard()函数顶部添加这一行const account state.account;这些更新做到了保持既有功能不变的同时改善结构、为更精巧的状态管理铺路、形成一致的状态数据访问模式、为集中式状态更新打下基础。注意这次重构本身并不会立刻修复刷新掉登录的问题但它建立了后续一切改进所依赖的基础设施。自检问题原文档的教学检查点能否解释为什么把状态集中在一个对象里比分散变量更好如果忘记更新某个函数改用state.account会发生什么这个模式如何为高级功能做准备集中式模式正是 Redux、Vuex、React Context 等现代框架的架构基础。挑战问题如果要加入用户偏好主题、语言应该加在状态结构的哪个位置如何扩展五、第二步实现受控的状态更新状态集中之后下一步是建立受控的数据修改机制。核心原则类似空中交通管制不让多个函数各自为政地修改状态而是把所有变更汇聚到单一受控函数中从而清晰掌握数据何时、如何变化。不可变immutable状态管理把state对象视为不可变——从不直接修改它每次变更都创建一个带有新数据的状态对象。看似不如直接修改高效却为调试、测试与可预测性带来显著收益收益描述影响可预测性变更只经由受控函数发生更易调试和测试历史追踪每次变更生成新对象可支撑撤销/重做防止副作用不存在意外修改消灭玄学 bug性能优化容易判断状态是否真正变化支持高效的 UI 更新JavaScript 通过Object.freeze()阻止对象被修改const immutableState Object.freeze({ account: userData }); // 任何修改 immutableState 的尝试都会抛出错误逐条拆解其行为阻止直接赋值或删除属性尝试修改时抛出异常确保状态变更必须经过受控函数建立关于「状态如何更新」的明确契约。需要深入的是shallow与deep冻结的区别可查阅 MDN 上Object.freeze的文档Object.freeze()只冻结第一层属性复杂嵌套结构中的子对象仍可变这对理解复杂状态结构至关重要。每次调用updateState()都会派生一个新的冻结版本旧状态被保留形成可追溯的链动手任务创建updateState()函数function updateState(property, newData) { state Object.freeze({ ...state, [property]: newData }); }逐句解析先用展开运算符spread,...把上一版状态的数据拷贝进一个新状态对象再用方括号属性记法[property]把指定属性覆盖为新数据最后用Object.freeze()锁死新对象防止修改。目前状态里只有account一个属性但按这个模式可以往状态里放任意多的属性。同时更新状态初始化确保初始状态也是冻结的let state Object.freeze({ account: null });然后更新register函数把state.account result;替换为updateState(account, result);login函数同理把state.account data;替换为updateState(account, data);趁此机会修复「点击 Logout 后账户数据未被清除」的问题——新建logout()函数function logout() { updateState(account, null); navigate(/login); }在updateDashboard()中把重定向return navigate(/login);换成return logout();。随后注册一个新账户、登出、再登入确认一切正常。提示在updateState()末尾加一行console.log(state)打开浏览器开发者工具的控制台就能观察到每一次状态变更。六、第三步实现数据持久化前面识别的「会话丢失」问题需要一个跨浏览器会话的持久化方案把应用从一次性体验变成可靠的工具。原子钟靠非易失性存储断电后仍保持精准计时Web 应用同样需要持久化机制把关键用户数据保存在浏览器会话与页面刷新之外。持久化决策前的战略问题问题银行应用语境决策影响数据是否敏感账户余额、交易历史选择安全的存储方式需要保存多久登录状态 vs 临时 UI 偏好选择合适的存储时长服务器是否需要认证 token vs UI 设置确定共享需求浏览器存储选项localStorage持久化的键值存储数据跨浏览器会话无限期保存浏览器重启、电脑重启后依然存在作用域限定在特定网站域名内非常适合用户偏好与登录状态。sessionStorage临时会话存储活跃会话期间行为与 localStorage 相同标签页关闭时自动清除适合不应持久化的临时数据。HTTP Cookies与服务器共享的存储每次服务器请求自动附带适合认证 token体积受限可能影响性能。数据序列化要求localStorage与sessionStorage都只存字符串// 存之前把对象转成 JSON 字符串 const accountData { user: john, balance: 150 }; localStorage.setItem(account, JSON.stringify(accountData)); // 取出来时再把 JSON 字符串解析回对象 const savedAccount JSON.parse(localStorage.getItem(account));理解序列化过程用JSON.stringify()把 JavaScript 对象转成JSON 字符串用JSON.parse()从 JSON重建对象自动处理复杂的嵌套对象与数组对函数、undefined值和循环引用会失败。进阶选项复杂离线应用或大数据集可考虑IndexedDBAPI它提供完整的客户端数据库但实现更复杂。动手任务实现 localStorage 持久化目标让用户一直保持登录直到显式登出。用localStorage跨浏览器会话保存账户数据。步骤 1定义存储配置const storageKey savedAccount;这个常量为存储数据提供一致标识、避免存储键拼写错误、方便日后改键、符合可维护代码的最佳实践。步骤 2加入自动持久化在updateState()函数末尾添加这一行localStorage.setItem(storageKey, JSON.stringify(state.account));逐条拆解把账户对象转成 JSON 字符串、用统一的存储键保存、每次状态变化时自动执行、确保所存数据永远与当前状态同步。架构红利正因为所有状态更新都集中到updateState()加入持久化只花了一行代码——这是好架构决策的直接回报。步骤 3应用加载时恢复状态创建初始化函数恢复已存数据function init() { const savedAccount localStorage.getItem(storageKey); if (savedAccount) { updateState(account, JSON.parse(savedAccount)); } // 此前的初始化代码 window.onpopstate () updateRoute(); updateRoute(); } init();初始化过程依次完成从 localStorage 取出之前保存的账户数据、把 JSON 字符串解析回对象、通过受控更新函数更新状态、在页面加载时自动恢复用户会话、且在路由更新之前执行以保证状态可用。步骤 4优化默认路由在updateRoute()中把默认的return navigate(/login);替换为return navigate(/dashboard);为什么合理充分利用新的持久化系统让 dashboard 负责认证检查若无已存会话则自动重定向到登录页用户体验更连贯。测试你的实现登录银行应用刷新浏览器页面确认仍处于登录状态且停在 dashboard关闭并重新打开浏览器回到应用确认仍然登录。至此应用的行为已与专业 Web 应用一致。这个持久化模式也是 PWA、离线优先应用与现代移动 Web 体验的基石。反思问题如果要让同一台设备支持多个用户账户会如何改造这个系统需要考虑哪些隐私与安全影响七、第四步持久化与数据新鲜度的平衡持久化成功维持了会话却引入新挑战数据过期staleness。当其他用户或应用修改了同一份服务器数据时本地缓存的信息就过时了。应用的处境类似维京领航员既需要保存的星图保持一致性也需要实时天象观测来适应变化——既要持久化的用户状态也要当前的服务器数据。发现数据新鲜度问题的实验用test账户登录 dashboard该账户预置在 server.js 的内存数据库里在终端运行以下命令模拟来自其他来源的一笔交易curl --request POST \ --header Content-Type: application/json \ --data { \date\: \2020-07-24\, \object\: \Bought book\, \amount\: -20 } \ http://localhost:5000/api/accounts/test/transactions刷新浏览器中的 dashboard 页面观察是否能看到这笔新交易。该实验展示了本地存储如何变得「过期」、模拟了应用之外数据被修改的真实场景、揭示了持久化与新鲜度之间的张力。对照 server.jsPOST /accounts/:user/transactions会把交易追加进内存账户并累加余额——所以这笔交易确实写进了服务器数据而浏览器里缓存的状态还不知情。数据过期的挑战问题原因用户影响过期数据localStorage 不会自动过期用户看到过时信息服务器变更其他应用/用户修改同一数据跨平台视图不一致缓存与现实不符本地缓存与服务器状态不匹配体验差、令人困惑解决策略实现「加载即刷新refresh on load」模式在持久化收益与新鲜数据之间取得平衡动手任务实现数据刷新系统步骤 1创建账户数据更新器async function updateAccountData() { const account state.account; if (!account) { return logout(); } const data await getAccount(account.user); if (data.error) { return logout(); } updateState(account, data); }逻辑拆解检查是否已登录state.account存在、无有效会话则登出、用既有的getAccount()从服务器拉取最新账户数据、服务器报错时优雅地登出无效会话、通过受控更新系统用新鲜数据更新状态、并经由updateState()自动触发 localStorage 持久化。步骤 2创建 dashboard 刷新处理器async function refresh() { await updateAccountData(); updateDashboard(); }该刷新函数协调数据刷新与 UI 更新、等待新鲜数据落地后再更新显示、确保 dashboard 呈现最新信息、保持数据管理与 UI 更新之间的清晰分离。步骤 3集成进路由系统const routes { /login: { templateId: login }, /dashboard: { templateId: dashboard, init: refresh } };集成方式每次 dashboard 路由加载都会执行刷新函数、保证用户进入 dashboard 时总能拿到新鲜数据、在不破坏既有路由结构的前提下叠加数据新鲜度、为「路由级初始化」提供一致模式。测试数据刷新系统登录银行应用运行上面的 curl 命令创建一笔新交易刷新 dashboard 页面或离开再返回确认新交易立即可见。至此应用同时拥有持久化状态的流畅体验与服务器数据的准确性。你已用与 Redux、Vuex 等专业状态库相同的原理建成了完整状态管理系统后续可自然延伸到 Redux/Zustand/Pinia、WebSocket 实时功能、离线优先 PWA以及状态机、观察者等高级模式。八、参考实现对照仓库源码如何印证这些模式仓库提供了可直接运行的参考实现 7-bank-project/solution/app.js本课的全部模式在其中都能找到一一对应且有多处可学习的工程化细节冻结状态与受控更新app.js 中以注释「Keep a frozen state object to avoid accidental mutations」定义了let state Object.freeze({ account: null })updateState()用{ ...state, [property]: newData }派生新对象并重新冻结末尾一行即localStorage.setItem(storageKey, JSON.stringify(state.account))——与课程步骤 2 完全一致且storageKey savedAccount与课程代码一致登出即清理logout() 先updateState(account, null)再navigate(/login)保证持久化层里也不残留账户数据数据新鲜度updateAccountData() 与 refresh() 与课程代码逐行对应路由表 routes 中/dashboard挂载init: refreshupdateRoute()在渲染模板后统一调用route.init()即课程「路由级初始化」模式的落地形态初始化与恢复init() 先做 schema 迁移migrateSchema()再用safeParse(localStorage.getItem(storageKey), null)安全恢复账户注意这里用带兜底的safeParse替代裸JSON.parse能防止存储数据损坏导致应用白屏——一个值得吸收的健壮性细节若存在已登录账户则把初始路由播种为/dashboard最后调用updateRoute()跨标签页同步app.js 额外监听storage事件——当其他标签页修改了accounts或savedAccount键且当前标签处于登录态时自动触发refresh()。从源码结构看这是「集中式状态」红利的又一个例证因为所有写入都收敛到updateState()只需一个监听器就能实现多标签页一致性。从源码结构看参考实现中的getAccount()app.js以Promise setTimeout模拟延迟、数据源是本地 localStorage 中的accounts键而非直接请求 HTTP APIserverUrl常量被注释为「reserved for future server swap」。也就是说参考实现把「数据获取」抽象成了一个可替换的异步接口——这与课程主线通过http://localhost:5000/api的 Express 服务取数在状态管理层面完全兼容只是数据层实现不同。九、进阶挑战挑战一存储优化。评估当前 localStorage 实现是否最优地平衡了存储效率与功能回答三个战略问题维持用户认证所需的最小信息是什么哪些数据变化足够频繁本地缓存几乎无收益存储优化如何在不牺牲体验的前提下提升性能实施策略识别必须持久化的关键数据很可能只是用户标识、修改 localStorage 实现只存关键会话数据、保证每次进入 dashboard 都从服务器加载新鲜数据、测试优化后的体验不变。进阶思考对比「存完整账户数据」与「只存认证 token」的权衡并把决策与理由文档化供未来团队成员参考。挑战二Agent 模式综合挑战。用支持 Agent 模式的代码编辑器完成一个带撤销/重做的状态管理系统要求1维护一个记录所有历史状态的状态历史数组2实现可回滚到先前状态的撤销/重做函数3在 dashboard 上提供撤销/重做 UI 按钮4历史上限 10 条状态以防内存问题5用户登出时正确清理历史并确保撤销/重做对余额变更生效、且跨页面刷新持久。这道题正是把本课「每次变更生成新对象」的不可变模式推到历史追踪的完整形态。十、课后作业实现「添加交易」对话框本课的正式作业是 实现「添加交易」(Add transaction) 对话框。完成后的效果示例如下十一、小结本课的演进路径可以概括为四步每一步都建立在前一步的架构收益之上集中化let account null→let state { account: null }形成单一数据源受控化updateState()Object.freeze()让每一次状态变更可追踪、可调试、可追溯持久化一行localStorage.setItem让登录状态跨越刷新与浏览器重启init()在路由前恢复会话新鲜度updateAccountData()refresh() 路由init挂载让本地缓存永远向服务器数据看齐。这四个模式——单一数据源、受控不可变更新、序列化持久化、加载即刷新——正是现代状态管理库的通用骨架。仓库内可进一步深入的材料完整参考实现 7-bank-project/solution/app.js、配套 API 服务 7-bank-project/api/server.js 及其接口文档 7-bank-project/api/README.md以及前一课 7-bank-project/3-data/README.md 中的数据获取基础。【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表