1. 先把两件事拆清楚:tabBar 管“去哪”,个人信息页管“你是谁”
1.1 “tabar”这个命名,几乎是每个小程序开发者都会写错的第一个单词
看到标题里把 tabBar 写成 tabar,我第一反应是太真实了。微信官方文档里这个配置项的真名是 tabBar,注意中间那个大写的 B,但你在代码里、在技术群里、在各类教程评论区看到的,一半以上都是 tabar、tabbr、tabber 这种写法。我自己最早接触小程序时也没少手滑,编译器不报错之前,完全意识不到写错了。
好消息是,大部分情况下这不是致命错误——因为 AppID、页面路由这些才是硬约束,tabBar 写错键名通常只是不生效,不会直接崩掉。但这也导致很多人配完 tabBar 发现底部栏没出来,第一反应是检查 list、检查路径,唯独没意识到 app.json 里写的是 tabar。所以我建议你从今天起就把这个写法刻在肌肉记忆里:app.json 顶层配置键永远是 tabBar,小写 t,大写 B,后面跟一个对象,不是数组。
这个系列走到第三篇,前两篇把项目初始化和通用页面骨架聊得差不多了,这篇要解决的是两个出现频率极高、又经常被放在一起实现的需求:个人信息页面,以及利用 tabBar 把多个页面组织成一套完整的小程序布局。你可能觉得这两个东西不搭边,一个管“用户长什么样”,一个管“应用长什么样”,但它们在实际开发中几乎是绑定的——绝大多数小程序的底部导航里都有一个“我的”页,而这个“我的”页干的最主要的事,就是展示和修改个人信息。
1.2 “多个页面布局”不只有 tabBar 一种解法,但多数项目默认选它
先把概念理清:在小程序里,把多个页面组合成一个应用,你可以选的好几条路,tabBar 只是其中一条。
第一种是最朴素的页面栈跳转。用 navigateTo 从一个页面 push 到另一个页面,靠左上角返回键一级一级退回去。这种模式适合流程型应用,比如下单流程、填写表单、文章详情,它天然带有前后关系,页面之间是“父子”层级,不适合用来做平级入口。
第二种就是 tabBar。它把所有页面当成平级的“工作台”,底部一排入口,用户随时切换,页面切换后各自的状态在一定条件下会被保留。它的核心是“并行”,不是“跳转”。你打开任何一个小程序,最常用的几个核心页面几乎都是挂在 tabBar 下的。
第三种是自己画一套导航,常见的是顶部 tab、侧边抽屉、悬浮按钮。这些方案自由度更高,但代价是要自己处理选中态、页面切换、状态同步,开发成本远比原生 tabBar 高。新人项目我基本不推荐一上来就上自定义导航,除非你有中间凸起按钮这种特殊需求,后面我会专门讲什么时候该换自定义 tabBar。
所以结论很明确:对绝大多数中小型项目,tabBar 就是最优解。它的优势不在于炫技,而在于微信把页面选中逻辑、切换动画、生命周期管理都替你处理了,你要做的只是配置 list 数组,把 pagePath 和图标填对。这也是为什么我把个人信息页和 tabBar 放在同一篇里讲——它们在一套布局里是“骨架”和“门面”的关系。
1.3 个人信息页在 tab 布局中的真实定位
个人信息页通常挂在两个地方:一是作为“我的”tab 页本身,底部导航里就有它;二是作为“我的”tab 下的二级页面,比如点“编辑资料”从右侧推入。我见过不少新人把个人信息页设成 tab 页,又把它的完整编辑表单也放在同一个页面里,导致这个页面又长又乱,交互层级完全糊在一起。
我的建议是分清“展示”和“编辑”。tabBar 里的“我的”页负责展示:头像、昵称、一句话签名、几个功能入口。点击某个字段或“编辑”按钮后,再 navigateTo 到一个独立的编辑页,那里才是表单的主场。这样 tabBar 的页面保持了轻量,编辑流程也符合用户预期——你看信息、点编辑、改完保存、返回,层级是清晰的。下面实现部分我就按这个思路走,和标题里的“个人信息页面”是严格对应的。
2. app.json 里的 tabBar 配置:每个字段都值得抠一抠
2.1 一份能直接跑通的最小配置
先给你一份完整的 app.json 配置,我按常见项目习惯加了两个 tab:一个首页,一个“我的”。你先把它贴到项目里跑通,再往下看字段解释。
{ "pages": [ "pages/index/index", "pages/mine/mine" ], "window": { "navigationBarTitleText": "我的小程序", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f5" }, "tabBar": { "color": "#999999", "selectedColor": "#07c160", "backgroundColor": "#ffffff", "borderStyle": "black", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "images/tab_home.png", "selectedIconPath": "images/tab_home_active.png" }, { "pagePath": "pages/mine/mine", "text": "我的", "iconPath": "images/tab_mine.png", "selectedIconPath": "images/tab_mine_active.png" } ] } }这里最容易被忽略的是 pages 数组。很多新手以为 tabBar.list 里写了 pagePath 就等于注册了页面,其实完全不是。小程序的页面必须先在 pages 数组里声明,tabBar.list 里的 pagePath 只是“从已经注册的页面里挑几个出来放进底部导航”。如果你在 tabBar 里写了一个 pages 中不存在的路径,编译期就会报错,并且明确提示你“未找到这个页面”。所以每次新增 tab 页,我习惯先在 pages 数组里加好路径,再写 tabBar.list,顺序不要反。
再说 list 数组的约束。官方要求至少 2 项、最多 5 项,text 是必填的。iconPath 和 selectedIconPath 可以省略,但省略后底部栏会变成纯文字,视觉效果比较干瘪,绝大多数项目都会配上图标。三个颜色属性里,color 和 selectedColor 控制文字和图标的前景色,backgroundColor 是底栏背景,borderStyle 只有 black 和 white 两种可选,控制顶部的分割线颜色。配置多 tab 时,list 项之间不能因为复制粘贴漏改某个 pagePath,我已经见过无数次两个 tab 指向同一页面的案例,这种错误编译期不报,但用户切换时会发现进入了同一个页面,体验非常莫名其妙。
2.2 图标资源:尺寸、格式、命名那点事
tabBar 图标最容易出的问题不是代码,而是素材本身。官方文档给出的建议尺寸是 81px × 81px,实际开发中我一般直接输出 2 倍图,也就是 162px × 162px,这样在大部分手机上都能保持清晰。格式方面用 PNG 最稳,带透明背景的图标不要转成 JPG,否则四周会出现难看的底色。
文件体积也值得注意。我在之前的项目里遇到过一次“开发工具正常,真机预览底部图标消失”的情况,排查到最后发现是 iconPath 指到了一个超过 1MB 的资源,基础库加载时直接把它丢弃了。保险做法是把每张 tab 图标控制在 40KB 以内,通常一个纯色的小图标压缩后只有几 KB,完全够用。
还有个非常中国的坑:命名。tab_home.png、tab_home_active.png 这种命名是社区通用做法,active 后缀代表选中态。但要留意整个项目的命名大小写规范,有些人新建文件时写成 Tab_Home.png,而 app.json 里写的是 tab_home.png。Windows 和 macOS 的文件系统有时候不区分大小写,开发者工具里也能正常显示,但提交到真机上就会因为路径找不到而白屏或图标消失。我在真机调试时专门因为这翻过车,所以现在凡是涉及图片路径,我都会先把文件名复制到 json 里,而不是凭记忆手打。
2.3 一个低调但关键的属性:position 和 custom
tabBar 配置里还有两个不常用、但面试和项目里经常被问到的字段。一个是 position,默认是 bottom,改成 top 可以把导航栏放到顶部,但官方文档明确说 top 模式不显示图标,只渲染文字,适合少数工具类应用。另一个是 custom,把它设为 true 就表示你准备使用自定义 tabBar,微信会读取一个叫 custom-tab-bar 的组件目录,完全接管底部导航的渲染。custom 字段不是你想开就开的,得先把组件目录建好,不然控制台会提示找不到自定义 tabBar 组件。这两个字段我现在不展开,后面单独一节讲自定义 tabBar 的适用场景。
还有一点是关于“为什么不显示 tabBar”的自查顺序。我列出真实项目里最常见的几类情况,你对照排查会快很多。
| 现象 | 最常见原因 | 处理方法 |
|---|---|---|
| 编译正常,但底部没有导航 | app.json 里写成了 tarBar 等错误键名 | 全局搜索 tabar/tabbr,改成 tabBar |
| tabBar.list 里的路径编译报错 | 页面路径没加进 pages 数组 | 先从 pages 数组注册该页面 |
| 开发者工具有图标,真机没有 | 图标文件名大小写不一致 | 统一命名,建议小写下划线 |
| 图标有但发虚、有白底 | 用了 JPG 或尺寸过小 | 换 PNG 透明底,重新导出 162px |
| 一个 tab 怎么点都指向同一页 | list 里 pagePath 复制后忘改 | 逐个检查 list 项的 pagePath |
这五个点占了 tabBar 问题的大头。你要是配完发现某个 tab 不对,先按这个顺序查一遍再动代码,会比反复清理缓存高效很多。
3. 个人信息页面的四个模块:头像、昵称、表单、缓存
3.1 头像和昵称:新老教程之间隔着一整代能力
如果你翻开三年前的小程序教程,里面大概率会让你用 wx.getUserInfo 拿用户头像和昵称,然后弹窗让用户授权。这套方案现在已经不能当作默认方案了——新版本基础库下,wx.getUserInfo 直接返回的是匿名化的“微信用户”和一张灰色默认头像,不再提供真实的昵称与头像数据。取而代之的,是官方主推的头像昵称填写能力。
这个变化很多人没意识到,尤其是从老教程入门的人,抄来的代码在开发者工具里还看不出问题,一到真机就傻眼。正确的做法是利用微信开放的两个组件能力:头像用 button 的 open-type 属性,昵称用 input 的 type 属性。看代码:
<view class="profile-card"> <button class="avatar-btn" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"> <image src="{{userInfo.avatarUrl}}" mode="aspectFill" class="avatar-img" /> </button> <input type="nickname" class="nickname-input" placeholder="请输入昵称" value="{{userInfo.nickname}}" bind:blur="onNicknameInput" /> </view>对应的逻辑层代码:
Page({ data: { userInfo: { avatarUrl: '/images/default-avatar.png', nickname: '' } }, onChooseAvatar(e) { const { avatarUrl } = e.detail; this.setData({ 'userInfo.avatarUrl': avatarUrl }); }, onNicknameInput(e) { this.setData({ 'userInfo.nickname': e.detail.value }); } });注意 button 组件默认自带边框和背景样式,用作头像入口时需要额外清掉,否则页面上会出现一个很突兀的按钮框。这个细节常有人漏,写完代码发现“我的”页上头像外面多了个方框,然后加各种样式去补救,其实一开始就应该这样写:
.avatar-btn { padding: 0; margin: 0; background: transparent; border: none; line-height: inherit; } .avatar-btn::after { border: none; }3.2 表单字段、校验规则与保存逻辑
个人信息页不用追求字段多,实用就好。我在实际项目里常用的字段组合是:头像、昵称、性别、生日、个性签名。性别用 picker 选择,生日用 picker mode="date",个性签名是一个多行文本输入。把这些字段集中到一个表单里,保存按钮放在底部,由用户主动提交。
保存前要做校验,不能把空值或超长文本写进缓存。以昵称为例,我一般用 2 到 20 个字符的限制,兼容中文、英文、数字和下划线:
saveProfile() { const { nickname, signature, gender, birthday } = this.data.userInfo; if (!nickname) { wx.showToast({ title: '昵称不能为空', icon: 'none' }); return; } if (!/^[\u4e00-\u9fa5a-zA-Z0-9_]{2,20}$/.test(nickname)) { wx.showToast({ title: '昵称需为2-20个字符', icon: 'none' }); return; } if (signature && signature.length > 50) { wx.showToast({ title: '签名最多50个字', icon: 'none' }); return; } const userInfo = { nickname, signature, gender, birthday, avatarUrl: this.data.userInfo.avatarUrl }; wx.setStorageSync('userInfo', userInfo); wx.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => wx.navigateBack(), 600); }校验通过后,这里用的是 wx.setStorageSync 把整个 userInfo 对象同步写进本地缓存。为什么用同步接口?因为这个操作量很小,同步写反而更便于理解代码执行顺序,而且保存完成后马上要跳转返回,用异步回调反而容易产生“页面已返回但数据还没落盘”的边界问题。等以后数据量变大、涉及多端同步,再换成后端接口不迟。
3.3 本地缓存:不是所有数据都该放进去
本地缓存的边界,很多新人容易搞错。我先说结论:像 userInfo 这种纯本地的、用户主动编辑的、体积以 KB 计算的数据,非常适合放在 storage 里。但如果你要做的功能涉及订单、余额、收藏状态这类随时会变的数据,就不能只依赖缓存,必须以服务器数据为准,缓存只能当加速层。
具体到这里,个人信息页面改完数据后,至少要保证两点:一是当前页面在用户返回时看到的是新数据,二是“我的”tab 页和任何需要展示昵称头像的页面,能读到同一份最新数据。这两个需求,靠一个共享的 storage key 就能解决。我习惯把所有与用户信息相关的字段打包成一个对象,key 名叫 userInfo,而不是拆成 user_name、user_avatar 一堆散 key。这样读写都方便,也避免不同页面各写各的 key 导致数据不一致。
考虑到页面展示需要,我还会在 data 里给 userInfo 设置一个初始值:
data: { userInfo: { avatarUrl: '/images/default-avatar.png', nickname: '未设置' } }这样即使缓存里什么都没有,页面渲染也不会出现空头像、空昵称的尴尬。默认头像我放在项目的 images 目录下,而不是用网络图,避免加载失败。
3.4 头像临时路径:一套会过期的地址
头像部分有一个隐蔽的坑:chooseAvatar 返回的 avatarUrl 是临时文件路径,不是持久文件路径。临时路径说白了就是系统临时目录里的一个引用,当前小程序运行期间访问没问题,但经历冷启动、系统清理缓存之后,这个路径大概率会失效,到时候头像就裂了。
正确的处理思路有两层。第一层是至少把临时文件转存到用户目录,使用 wx.getFileSystemManager().saveFile:
onChooseAvatar(e) { const { avatarUrl } = e.detail; const fs = wx.getFileSystemManager(); fs.saveFile({ tempFilePath: avatarUrl, success: (res) => { const savedFilePath = res.savedFilePath; const userInfo = wx.getStorageSync('userInfo') || {}; userInfo.avatarUrl = savedFilePath; wx.setStorageSync('userInfo', userInfo); this.setData({ 'userInfo.avatarUrl': savedFilePath }); }, fail: () => { // 保存失败时至少先用临时路径展示 this.setData({ 'userInfo.avatarUrl': avatarUrl }); } }); }第二层是更严格的方案:直接把头像文件上传到自己的服务器或对象存储,拿到一个稳定的云端 URL 再存进 userInfo。因为即使用 saveFile 转存了,用户目录的文件也可能因为清理缓存或换设备而丢失。对正式上线的项目来说,头像最终都应该走 uploadFile 上传链路,本地缓存只承担“刷新前的一阵子”的兜底作用。这个决策取决于你的项目有没有后端,如果没有,用 saveFile + storage 的组合也能撑住大部分纯前端小工具的场景。
4. switchTab 的跳转规则与 tab 页数据实时刷新
4.1 switchTab 的特殊脾气:不能带参数
tab 页面和平凡页面最大的不同,是跳转规则。navigateTo 是万金油,但它唯独不能跳 tab 页面。你要从一个二级页面切回底部导航页,只能用 wx.switchTab:
// 错误示范:navigateTo 跳 tab 页,控制台直接报错 wx.navigateTo({ url: '/pages/index/index' }); // 正确方式 wx.switchTab({ url: '/pages/index/index' });更关键的是 switchTab 不能带 query 参数。很多人在编辑页里改完数据,想跳回“我的”页时顺手在 url 后面拼一个 ?refresh=true,结果发现目标页收到的 options 是空的。这不是 bug,是规则不允许。
那跨页传参怎么办?我常用的办法是先把数据写进 storage 或 globalData,再 switchTab 跳转,目标页在 onShow 里自行读取。记住这个顺序:先写共享数据,再跳页面。如果顺序反了,目标页的 onShow 会先执行,读到的是旧数据,然后你的新值才落地,页面就卡在旧状态上。
4.2 onLoad、onShow、onHide 在 tab 切换时的真实顺序
这是 tab 布局里最值得理解的一组生命周期。先记住一个事实:tab 页面的 onLoad 在整个小程序生命周期里只触发一次,第一次进入该页时调用;之后每次从别的 tab 切回来,都只会触发 onShow,不会再走 onLoad。
举个例子。用户首次启动,进入“我的”tab,执行顺序是:
- onLoad:可以读缓存、做初始化
- onShow:每次页面展示都会执行,紧跟 onLoad 之后
然后用户切到首页,“我的”页触发 onHide。接着用户又从首页切回“我的”页,此时 onLoad 不再执行,只有 onShow 执行。
这个机制带来的实际影响非常大。如果你把“读取用户信息”的逻辑只写在 onLoad 里,那么用户切到别的 tab、再切回来时,读到的是第一次进入时的旧数据。用户明明已经在别处修改了昵称,回来一看还是旧的,体验极差。反过来,如果你把读取逻辑只写在 onShow 里,第一次进入时虽然也能用,但某些耗时初始化(比如统计上报、复杂计算)会在每次切换 tab 时反复执行,白白浪费性能。
我的建议是拆分:低频的一次性初始化放进 onLoad,高频的、需要反映最新状态的数据读取放进 onShow。个人信息展示页的代码可以写成这样:
onLoad() { // 只在首次进入时初始化:读一次缓存 const userInfo = wx.getStorageSync('userInfo'); if (userInfo) { this.setData({ userInfo }); } }, onShow() { // 每次切回本页时都重新读取,保证数据最新 const userInfo = wx.getStorageSync('userInfo'); if (userInfo) { this.setData({ userInfo }); } }这样第一次进入时有数据可显示,之后每次从编辑页返回、从别的 tab 切回来,也都能拿到最新缓存。
4.3 个人信息修改后,其他 tab 页怎么联动
跨 tab 数据同步,是这类布局里真正考验功底的地方。个人信息在“我的”tab 里被改了,但首页可能也展示着同一份用户昵称,怎么让首页也更新?我按复杂度从小到大列三种方案。
方案一:纯 storage 主动读取。目标页面在 onShow 时重新 getStorageSync,拿到什么就渲染什么。这个方案最简单,适合数据量小、页面少的项目,也是这一篇推荐你优先用的。
方案二:全局事件通知。用 getApp().globalData 存一份用户信息,再定义一个简单的订阅发布器,个人信息页修改后 emit 一个更新事件,首页监听后重新读取并 setData。这个方案适合数据量开始变大、且多个页面需要几乎同步刷新的场景,但要自己管理监听器的注册和销毁,一个不留神就会出现重复触发或内存泄漏。
方案三:走服务端。所有数据以服务器为准,页面通过轮询或长连接获取最新状态。这个方案成本最高,但也是多端同步唯一靠谱的方式。对个人项目和早期产品,我不建议一上来就上这个。
三选一的时候,判断标准很简单:你有后端吗?没有,就选方案一。有后端但用户信息改动频率很低,也可以用方案一,只是把“读 storage”换成“拉接口”。只有当用户信息的改动会实时影响核心业务逻辑(比如会员等级、权限标识、余额)时,才需要考虑方案二或三。
4.4 保存成功后的“一眼反馈”细节
改完个人信息的最后一个环节,是给用户明确的反馈。wx.showToast 是最轻量的方案:
wx.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => wx.navigateBack(), 600);这里有个小细节:navigateBack 前先等 toast 至少展示一会儿,不然页面刚弹出来就消失,用户根本看不清保存成功提示。600 毫秒是我试过比较舒服的间隔,太短看不到,太长又显得拖沓。
如果页面结构允许,也可以不返回,就地更新展示区的数据并加一个轻提示。整体原则是:保存动作必须有反馈,反馈要出现在用户视线焦点附近,且页面状态要跟保存结果保持一致。
5. 实测过程中值得记下的坑和几个长期受用的设计习惯
5.1 高频报错与解决办法
这部分是我做这类需求时踩过的坑集合。做成表格可以收藏起来,下次遇到直接对照。
| 报错或现象 | 原因 | 解决办法 |
|---|---|---|
| app.json 中 tabBar 未生效 | 键名写错,比如写成了 tarBar | 改成 tabBar,大小写敏感 |
| Invalid tabBar list | list 为空或 pagePath 有误 | 检查 list 项数量,确保 2 到 5 项 |
| 页面路径未找到 | tabBar pagePath 未在 pages 中注册 | 先在 pages 数组添加页面路径 |
| 无法调用 wx.switchTab | 目标页不是 tab 页面 | 确认目标页已在 tabBar.list 中 |
| switchTab 后拿不到参数 | tab 跳转不支持 query | 改用 storage 或 globalData 传参 |
| 真机头像裂图 | 直接保存了临时路径 | 调用 saveFile 转存,或上传服务器 |
| 修改后切回 tab 无变化 | 读取逻辑写在了 onLoad | 同步在 onShow 中重新读取 |
这里面最值得培养的意识是:看到错误先区分“编译期错误”和“运行期错误”。路径缺失、键名错误大多在编译期就会报出来,给你明确的行号;但数据不刷新、头像裂图这类运行期问题,编译器帮不了任何忙,只能靠生命周期理解和数据流向自查。
5.2 什么时候才需要上自定义 tabBar
原生 tabBar 在很多场景下够用了,但它有两个硬伤:一是中间不能凸起,二是每个 tab 的 UI 表现高度统一。如果你的需求包含以下任一特征,考虑换成自定义:
- 中间需要一个大号的“发布”按钮,突出主操作
- 某个 tab 需要展示动态红点、角标数量
- 需要切换动画,或者不同 tab 有不同底栏背景色
- 需要根据用户身份动态隐藏某个 tab
自定义 tabBar 的官方做法是在项目根目录下创建 custom-tab-bar 目录,里面放一个自定义组件,并在 app.json 的 tabBar 配置中加上 "custom": true。之后你就在这个组件里完全控制底部导航的渲染和点击逻辑。代价也很明显:选中态、页面切换、icon 资源加载都需要你手动处理,而且每次给 tab 页面 setData 时,要记得一并通知自定义 tabBar 组件更新 selected 状态,否则会出现“页面已经切走,底部高亮还停在旧位置”的经典 bug。
对这篇面向多数读者的小项目来说,我真心建议先不要碰自定义。原生 tabBar 的稳定性和性能都够好,把精力放在页面本身的体验上更划算。
5.3 隐私合规方面值得留意的两件事
头像昵称相关能力这些年调整了很多轮,只要你的项目涉及个人信息收集,就必须在微信公众平台后台配置对应的用户隐私保护指引,说明你收集了什么数据、用来做什么。这是一个上线前必须完成的配置项,缺了它,真机上调用 chooseAvatar 时可能会被拦截或弹授权失败。
另一个容易被忽视的点是,不要试图绕过官方能力去获取不该获取的信息。用 open-type="chooseAvatar" 拿头像、用 type="nickname" 收昵称,都是微信认可的闭环流程。不要在控制台里偷偷调用或存储任何未申报的字段,这是底线问题,也是长期维护的硬约束。个人开发者和正在学习阶段的同学,尤其要养成一开始就按规范写的习惯,不然等审核被拒再回来改,返工成本非常高。
5.4 给这套组合再加一点扩展空间
做完基础的个人信息页和 tabBar 布局后,你完全可以顺着这个架构继续加功能。“我的”tab 里常见扩展包括:设置入口、收货地址、订单列表、清缓存按钮、关于与版本号。tabBar 数量有限,后加的功能页面基本都是点进“我的”之后 navigateTo 进去的二级页,所以你在设计个人信息页时,就应该把列表式的功能入口结构预留好,后续每加一个功能,只是往列表里加一行的问题。
关于 tab 页的布局,如果业务真的超过五个核心入口,我不建议硬塞,而是把次要入口收敛到“我的”页面的功能列表里。底部五个 tab 是用户注意力最稀缺的资源,只放真正高频的功能,其余的藏在二级页里,这既是交互常识,也符合小程序的设计规范。
最后说点我自己的体感。tabBar 属于那种配置一次就不再翻看的代码,但恰恰是项目中期最容易被改出 bug 的地方;个人信息页看起来只是一堆输入框和一张头像,实际上涉及权限能力、临时路径、缓存同步、生命周期,难度密度比很多看起来复杂的业务页面都要高。我每次处理这类需求,都会先把“用户信息存在哪、什么时候读、谁负责刷新”这十几个字写在文件注释的第一行,然后再去写实现代码。这套思路在后续维护里帮我省了非常多时间,也推荐你试试。