1. 页面传值这件事,为什么值得单独拎出来讲
做小程序开发的人,几乎都遇到过这样的场景:列表页点进详情页,详情页要拿到那个商品的 id;表单页填完一堆数据,提交前要跳到一个确认页展示;购物车结算时,选中的商品要带到订单页。这些需求背后都是同一件事——页面之间怎么把数据传过去。
很多人刚上手小程序时,觉得传值无非就是url后面拼个参数,能跑通就行。但真到了项目里,你会发现事情没那么简单。参数里带中文和特殊字符会乱码,对象类型传不过去,页面返回时数据要回传,多个页面共享一份状态,页面刷新后数据丢了……这些问题一个接一个冒出来。我见过不少项目,传值逻辑写得乱七八糟,globalData到处塞,缓存键名随手起,最后维护起来极其痛苦。
小程序官方给了好几套传值机制,它们不是随便设计的,每一套都有自己最适合的场景和明确的边界。路由传值适合简单的、一次性的参数传递;页面栈适合页面返回时的数据回传;globalData适合全局共享的、生命周期跟随应用的状态;缓存适合需要持久化、跨会话保留的数据。用错了地方,代码能跑但会埋雷;用对了地方,逻辑清晰还省事。
这篇内容我会把这四种方式逐个拆开,讲清楚它们各自的原理、适用场景、具体怎么写,以及我在实际项目里踩过的坑。不管你是刚接触小程序的新手,还是写过几个项目但传值逻辑一直靠感觉来的开发者,应该都能从中找到能直接用的东西。下面从最常用的路由传值开始。
2. 路由传值:最直接但也最容易踩坑的方式
2.1 路由传值的两种写法与底层逻辑
路由传值是小程序里用得最多的方式,因为它最直观——跳转的时候顺手把参数带上。它有两种写法,对应两种跳转 API。
第一种是wx.navigateTo,保留当前页面,跳转到新页面。参数直接拼在 url 后面:
wx.navigateTo({ url: '/pages/detail/detail?id=123&name=test' })目标页面在onLoad的 options 里接收:
Page({ onLoad(options) { console.log(options.id) // '123' console.log(options.name) // 'test' } })第二种是wx.switchTab,用于跳转到 tabBar 页面。这里有个关键限制:switchTab 不支持在 url 后面带参数。很多人第一次遇到这个会懵,明明写了参数却收不到。原因是 tabBar 页面通常是常驻的,它的生命周期和普通页面不一样,官方从设计上就不允许通过 url 传参。那 tabBar 页面之间怎么传值?后面讲 globalData 和缓存时会说。
路由传值的底层其实很简单,参数就是 url 的 query string,和网页开发里的?a=1&b=2是一个道理。小程序框架在跳转时解析这段字符串,塞进目标页面的onLoad参数里。理解这一点,后面很多坑就都能解释了。
2.2 参数类型限制:为什么对象传不过去
路由传值最大的限制是只能传字符串。你写id=123,收到的options.id是字符串'123'而不是数字123。如果你直接拿它去做数值比较或者计算,就可能出问题:
// 列表页 wx.navigateTo({ url: `/pages/detail/detail?id=${item.id}` }) // 详情页 onLoad(options) { // 如果这里直接 options.id === 123,永远是 false // 因为 options.id 是字符串 '123' const id = Number(options.id) // 必须手动转换 }那对象怎么办?比如你想把一个商品对象整个传过去。答案是:传不了。你只能传它的标识,比如 id,然后在目标页面重新请求或者从缓存里取。有些人会把对象JSON.stringify之后传:
const obj = { id: 1, name: 'test' } wx.navigateTo({ url: `/pages/detail/detail?data=${JSON.stringify(obj)}` })这样确实能传,但我不推荐。原因有两个:一是 url 长度有限制,对象大了可能超限;二是JSON.stringify后的字符串里有大量特殊字符,必须配合encodeURIComponent使用,否则遇到&、=、#这些字符会直接把参数结构搞乱。如果非要传复杂数据,用后面讲的缓存或 globalData 更稳妥。
2.3 中文和特殊字符的编码处理
这是路由传值里最经典的坑。假设你要传一个中文名字:
wx.navigateTo({ url: `/pages/detail/detail?name=张三` })在开发者工具里可能看着正常,但真机上或者某些环境下就会乱码。正确做法是用encodeURIComponent编码:
const name = '张三' wx.navigateTo({ url: `/pages/detail/detail?name=${encodeURIComponent(name)}` }) // 接收端 onLoad(options) { const name = decodeURIComponent(options.name) // '张三' }注意,encodeURIComponent和decodeURIComponent要成对使用。我见过有人编码了但忘了解码,结果页面上显示一堆%E5%BC%A0%E4%B8%89,排查半天才发现是这个问题。
还有一个容易忽略的点:如果参数值本身包含&或=,不编码的话会被解析成多个参数。比如传一个搜索关键词a&b,不编码的话接收端会收到a和b两个参数。所以只要参数值不是纯数字或纯英文,一律编码,这是最省心的做法。
2.4 路由传值的适用边界
总结一下,路由传值适合这些场景:传简单的标识符(id、type、来源标记)、传少量字符串参数、一次性传递不需要回传的数据。不适合的场景:传对象或数组、传大量数据、需要跨多个页面共享、需要持久化保留。
判断标准很简单:如果这个数据只是为了让目标页面知道"我是谁、我要展示什么",用路由传值;如果这个数据需要在多个页面之间流转或者长期存在,换别的方式。
3. 页面栈:返回时怎么把数据带回去
3.1 页面栈是什么,为什么返回传值要靠它
小程序维护着一个页面栈,每打开一个新页面就入栈,返回就出栈。栈里最多放 10 个页面,超过就用redirectTo或者navigateBack处理。这个栈不只是个概念,它直接决定了你能用哪些 API 来操作页面。
返回传值的需求很常见:A 页面跳到 B 页面做选择,选完返回 A,A 要拿到 B 选的结果。比如选择收货地址、选择优惠券、选择联系人,都是这个模式。这时候数据是从"后面的页面"往"前面的页面"传,方向反了,路由传值那套用不上。
页面栈提供了两个关键能力来解决这个问题:一是getCurrentPages()能拿到当前页面栈的所有页面实例,二是navigateBack可以指定返回几层。结合起来,就能实现返回传值。
3.2 用 getCurrentPages 拿到上一页实例
getCurrentPages()返回一个数组,数组里每个元素是页面实例,最后一个就是当前页面,倒数第二个就是上一页。拿到上一页实例后,可以直接调用它上面定义的方法:
// B 页面(选择页) Page({ selectItem(item) { const pages = getCurrentPages() const prevPage = pages[pages.length - 2] // 上一页实例 prevPage.setSelectedItem(item) // 调用上一页的方法 wx.navigateBack() // 返回 } }) // A 页面(来源页) Page({ data: { selected: null }, setSelectedItem(item) { this.setData({ selected: item }) } })这个写法的核心是:B 页面主动调用 A 页面暴露出来的方法,把数据塞回去。A 页面需要提前定义好接收数据的方法,相当于开了一个口子。
这里有个细节要注意:getCurrentPages()拿到的页面实例,它的setData是可以正常调用的,但前提是那个页面还在栈里。如果 A 页面已经被销毁了(比如用了redirectTo),那pages[pages.length - 2]拿到的就不是你以为的那个页面,调用方法会报错。所以用之前最好判断一下页面实例是否存在。
3.3 返回传值的几种变体与选择
除了上面那种直接调方法的方式,还有几种变体。
一种是通过页面实例直接改 data:
const prevPage = pages[pages.length - 2] prevPage.setData({ selected: item })这种方式更直接,但耦合度更高,因为 B 页面需要知道 A 页面的 data 结构。如果 A 页面的 data 字段改了名,B 页面也得跟着改。相比之下,调方法的方式把数据结构封装在 A 页面内部,B 页面只负责传值,耦合更低。
另一种是配合 EventChannel。navigateTo的 success 回调里可以拿到一个 eventChannel 对象,用它来跨页面通信:
// A 页面跳转时 wx.navigateTo({ url: '/pages/select/select', events: { // 监听 B 页面发来的数据 selectResult(data) { this.setData({ selected: data }) } } }) // B 页面返回时 const eventChannel = this.getOpenerEventChannel() eventChannel.emit('selectResult', item) wx.navigateBack()EventChannel 的好处是解耦更彻底,A 页面不需要暴露方法,B 页面也不需要知道 A 页面的存在,双方通过事件名通信。缺点是写法稍微绕一点,而且只在navigateTo打开的页面之间有效。我个人在页面层级比较深、传值关系比较复杂的场景下会用 EventChannel,简单的两层返回还是直接调方法更省事。
3.4 页面栈传值的注意事项
用页面栈传值有几个坑要避开。
第一,页面栈最多 10 层。如果你的应用层级很深,比如 A→B→C→D→E,到第 10 层就没法再navigateTo了。这时候要么用redirectTo替换当前页,要么重新设计页面结构。返回传值时如果跨了多层,pages[pages.length - 2]拿到的可能不是你想要的那个页面,需要根据实际层级调整索引。
第二,不要在 onLoad 里调用 getCurrentPages 去操作上一页。因为这时候当前页面刚入栈,上一页的状态可能还没准备好。返回传值的操作应该放在用户触发的事件里,比如点击选择按钮的时候。
第三,页面实例的方法名要避免和生命周期函数重名。如果你在 A 页面定义了一个叫onLoad的方法,然后 B 页面去调用它,会出问题。给接收数据的方法起个明确的名字,比如onSelectResult、receiveData之类的。
4. globalData:全局共享数据的正确打开方式
4.1 globalData 的本质与初始化
globalData是定义在app.js里的一个全局对象,所有页面都能通过getApp()拿到它。它的本质就是一个挂在应用实例上的普通 JavaScript 对象,生命周期跟随整个应用,应用不销毁它就不销毁。
// app.js App({ globalData: { userInfo: null, token: '', cartCount: 0 } })页面里这样用:
const app = getApp() Page({ onLoad() { console.log(app.globalData.userInfo) }, updateCart() { app.globalData.cartCount = 5 } })看起来很简单,但正因为简单,很多人把它当成了万能垃圾桶,什么都往里塞,最后globalData变成一个巨大的、谁都能改的、没人说得清里面有什么的对象。这是典型的反模式。
4.2 globalData 适合放什么,不适合放什么
globalData 最适合放的是全局唯一、多个页面都需要读、生命周期跟随应用的数据。典型的有:用户登录信息、token、全局配置、当前选中的门店或城市、购物车数量这种需要全局同步的计数。
不适合放的有几类。一是只在两个页面之间传的数据,这种用路由或页面栈更合适,放 globalData 属于杀鸡用牛刀,还污染了全局空间。二是大量数据,globalData 常驻内存,塞太多东西会占内存。三是需要持久化的数据,globalData 在应用关闭后就没了,下次打开是初始值,需要持久化的得配合缓存。
我见过一个项目,把列表页的数据整个塞进 globalData,详情页再从里面取。结果用户从详情页返回列表页时,列表数据被改了,页面显示错乱。这种问题的根源就是把 globalData 当成了数据中转站,而不是全局状态容器。
4.3 globalData 的响应式问题与解决思路
globalData 有个很关键的局限:它本身不是响应式的。你改了app.globalData.cartCount,页面上绑定的{{cartCount}}不会自动更新,必须手动setData。
// 这样改,页面不会更新 app.globalData.cartCount = 5 // 必须这样 app.globalData.cartCount = 5 this.setData({ cartCount: 5 })这就带来一个问题:如果多个页面都依赖同一个 globalData 字段,每个页面都得在合适的时机手动同步。比如购物车数量,首页、分类页、个人中心页都显示,任何一个页面改了数量,其他页面返回时都得重新读一次。
常见的解决思路有两种。一种是在 onShow 里同步,因为页面每次显示都会触发 onShow,这时候从 globalData 读最新值 setData 到页面:
onShow() { this.setData({ cartCount: getApp().globalData.cartCount }) }另一种是封装一个简单的订阅机制,在 globalData 变化时通知所有订阅的页面。这个稍微复杂一点,但对于状态多的项目能省很多事。不过对于大多数中小项目,onShow 同步已经够用了,没必要过度设计。
4.4 globalData 与 tabBar 页面传值的配合
回到前面提到的 tabBar 页面传值问题。因为switchTab不能带参数,tabBar 页面之间的传值基本就靠 globalData 和缓存。
典型场景:在普通页面点击一个按钮,要跳到 tabBar 的某个页面并让它展示特定内容。做法是先把数据写进 globalData,再switchTab:
// 普通页面 const app = getApp() app.globalData.targetTab = 'orders' wx.switchTab({ url: '/pages/user/user' }) // tabBar 页面 onShow() { const app = getApp() if (app.globalData.targetTab) { this.setData({ activeTab: app.globalData.targetTab }) app.globalData.targetTab = null // 用完清掉,避免下次误读 } }这里有个经验:用完的临时 globalData 字段要及时清空。否则下次进入这个页面时,onShow 又读到旧值,出现"莫名其妙跳到了上次的 tab"这种问题。这个坑我踩过,排查的时候完全想不到是 globalData 没清导致的。
5. 缓存:需要持久化时绕不开的选择
5.1 小程序缓存的 API 与容量限制
小程序提供了同步和异步两套缓存 API,同步的是wx.setStorageSync/wx.getStorageSync,异步的是wx.setStorage/wx.getStorage。日常开发里同步的更常用,因为写起来简单,而且缓存操作通常很快,不会明显阻塞。
// 存 wx.setStorageSync('userInfo', { name: 'test', age: 18 }) // 取 const userInfo = wx.getStorageSync('userInfo') // 删 wx.removeStorageSync('userInfo') // 清空 wx.clearStorageSync()缓存有几个关键特性要记住。第一,单个 key 最大 1MB,所有缓存总共最大 10MB。超过会报错。所以别往缓存里塞大图片的 base64 或者大列表数据。第二,缓存是持久化的,应用关闭再打开还在,除非用户手动清除或者调了清除 API。第三,缓存可以存对象,小程序会自动做序列化和反序列化,不需要你手动JSON.stringify。这一点比路由传值方便很多。
5.2 缓存做页面传值的典型用法
缓存做传值,核心思路是:A 页面把数据写进缓存,B 页面从缓存读,读完删掉。
// A 页面 wx.setStorageSync('tempData', { id: 1, list: [...] }) wx.navigateTo({ url: '/pages/detail/detail' }) // B 页面 onLoad() { const data = wx.getStorageSync('tempData') if (data) { this.setData({ detail: data }) wx.removeStorageSync('tempData') // 用完即删 } }这个模式适合传对象、传数组、传大量数据。因为缓存没有 url 长度限制,也不会有编码问题,对象直接存直接取。
但这里有个必须注意的点:用完一定要删。如果不删,下次进入 B 页面时getStorageSync又读到旧数据,页面显示的就是上次的内容。这个 bug 很隐蔽,因为第一次用是正常的,只有第二次进入才出问题。我建议凡是这种临时中转的缓存,key 名统一加个前缀比如temp_,方便识别和清理。
5.3 缓存键名管理与数据过期策略
缓存用多了,键名管理就成了问题。项目里如果到处setStorageSync('data', ...)、setStorageSync('info', ...),很快就会冲突或者搞不清哪个 key 是干什么的。
我的做法是统一在一个文件里管理缓存键名:
// storageKeys.js export const STORAGE_KEYS = { USER_INFO: 'app_user_info', TOKEN: 'app_token', TEMP_DETAIL: 'temp_detail_data', SEARCH_HISTORY: 'app_search_history' }用的时候引用常量,而不是直接写字符串。这样改键名的时候只改一个地方,也不会出现拼写错误导致的"存了取不到"。
关于过期策略,小程序缓存本身不支持设置过期时间,需要自己实现。简单做法是存的时候带上时间戳,取的时候判断:
function setWithExpire(key, value, expireMs) { wx.setStorageSync(key, { value, expireAt: Date.now() + expireMs }) } function getWithExpire(key) { const data = wx.getStorageSync(key) if (!data) return null if (Date.now() > data.expireAt) { wx.removeStorageSync(key) return null } return data.value }对于登录 token 这种有有效期的数据,这个机制很有用。不然 token 过期了页面还在用,请求一直失败,用户体验很差。
5.4 缓存传值 vs globalData 传值:怎么选
这两个都能传对象,都能跨页面,很多人纠结用哪个。我的判断标准是看数据需不需要在应用重启后还在。
如果数据只是当前这次使用流程里的中转,应用关了就不需要了,用 globalData,读写更快,也不占缓存空间。如果数据需要持久化,比如用户偏好、搜索历史、草稿内容,用缓存。还有一点,globalData 在应用被杀掉后重新打开是初始值,而缓存还在,所以涉及"下次打开还要用"的场景必须用缓存。
另外,缓存读写是同步的,频繁读写大量数据可能影响性能。globalData 是内存操作,快得多。所以高频读写的全局状态用 globalData,低频的持久化数据用缓存。
6. 四种传值方式的横向对比与选型决策
6.1 一张表看清四种方式的差异
| 维度 | 路由传值 | 页面栈 | globalData | 缓存 |
|---|---|---|---|---|
| 数据方向 | 单向,前往后 | 可反向,后往前 | 任意方向 | 任意方向 |
| 支持类型 | 仅字符串 | 任意类型 | 任意类型 | 任意类型(自动序列化) |
| 数据量限制 | url 长度限制 | 无 | 受内存限制 | 单 key 1MB,总 10MB |
| 生命周期 | 一次性 | 一次性 | 跟随应用 | 持久化 |
| 响应式 | 否 | 否 | 否 | 否 |
| 编码问题 | 有,需手动处理 | 无 | 无 | 无 |
| 适用场景 | 简单标识传递 | 返回传值 | 全局共享状态 | 持久化数据 |
这张表不是让你背的,而是帮你建立判断直觉。遇到传值需求时,先问自己三个问题:数据往哪个方向走?数据是什么类型、多大?数据要活多久?三个问题的答案基本就能定位到该用哪种方式。
6.2 组合使用的真实场景拆解
实际项目里,这四种方式经常是组合使用的。举一个电商下单流程的例子。
用户在商品列表页点击商品,跳详情页。这一步用路由传值传商品 id,因为只需要一个标识。
详情页点"加入购物车",购物车数量要更新。这个数量是全局的,首页、分类页、购物车页都要显示,所以用globalData存,各页面在 onShow 里同步。
详情页点"立即购买",跳到订单确认页。订单确认页需要商品详情、收货地址、优惠券等信息,数据量大且是对象,用缓存传,key 用temp_order_data,订单页读完即删。
订单确认页点"选择收货地址",跳到地址列表页。选完返回订单页,要把选中的地址带回去,用页面栈的返回传值。
你看,一个流程里四种方式全用上了,各司其职。关键不是记住用哪个,而是理解每种方式的特性,然后在具体场景里做匹配。
6.3 选型时最容易犯的三个错误
第一个错误是什么都用路由传值。因为路由传值最直观,很多人就习惯性地把所有数据都往 url 上拼。结果遇到对象就JSON.stringify,遇到中文就乱码,url 长得离谱。这是没有理解路由传值的边界。
第二个错误是什么都往 globalData 塞。globalData 方便,随手就能读写,于是变成了全局变量污染。项目大了之后,没人知道 globalData 里有哪些字段、谁在用、什么时候改的。调试的时候改一个字段,另一个页面莫名其妙出问题。
第三个错误是缓存只存不删。临时数据用完不清理,导致下次读到脏数据。或者缓存键名随意起,时间长了分不清哪些还有用。缓存是需要管理的资源,不是免费的。
7. 那些文档里不会写的踩坑经验
7.1 路由参数里的布尔值和数字陷阱
前面提过路由传值只能传字符串,但实际踩坑的时候,问题往往更隐蔽。比如你传一个布尔值:
wx.navigateTo({ url: `/pages/detail/detail?isVip=${true}` }) // 接收端 options.isVip 是字符串 'true',不是布尔 true如果你写if (options.isVip),字符串'false'也是真值,判断就错了。正确做法是显式转换:
const isVip = options.isVip === 'true'数字同理,options.id是字符串,做数值运算前必须Number()转换。我建议在onLoad里接收参数后,第一件事就是把所有参数转换成正确的类型,后面用起来才不会有隐患。
7.2 页面返回时 onLoad 不会重新执行
这是个新手很容易困惑的点。A 页面跳到 B 页面,B 页面返回 A 页面时,A 页面的onLoad不会再次执行,只有onShow会执行。所以如果你在 A 页面的onLoad里请求数据,返回时数据不会刷新。
Page({ onLoad() { // 只在页面第一次加载时执行 this.fetchData() }, onShow() { // 每次页面显示都执行,包括从其他页面返回 // 需要刷新的逻辑放这里 } })这个特性直接影响返回传值的实现。如果你用页面栈传值,在 B 页面调用了 A 页面的方法改了 data,A 页面返回后显示的是新数据,没问题。但如果你是把数据写进缓存,指望 A 页面返回时自动读,那得在onShow里读,放onLoad里读不到。
7.3 globalData 在页面 onLoad 时可能还没准备好
如果 globalData 里的数据是在app.js的onLaunch里异步获取的,比如登录后拿用户信息,那么页面onLoad的时候可能还没拿到。这时候读 globalData 会读到 null。
// app.js App({ onLaunch() { // 异步请求,需要时间 wx.request({ url: '...', success: (res) => { this.globalData.userInfo = res.data } }) } })页面onLoad里getApp().globalData.userInfo很可能是 null。解决办法是要么用回调/事件通知页面,要么在页面里做判空和重试。我一般会在 app.js 里维护一个userInfoReadyCallback,页面 onLoad 时如果 userInfo 还没好,就把回调挂上去,等数据到了再执行。
7.4 缓存数据被用户手动清除后的兜底
用户可以在小程序设置里清除缓存,这时候所有缓存数据都没了。如果你的应用逻辑强依赖缓存里的某个数据,比如 token,清除后就会出问题。所以读缓存一定要判空,并且想好缓存不存在时的降级逻辑。
const token = wx.getStorageSync('app_token') if (!token) { // 重新登录或者跳转到登录页 this.redirectToLogin() return }这个判空看起来是常识,但实际项目里经常被忽略,因为开发阶段缓存一直在,测试不出问题,上线后用户清了缓存才暴露。
7.5 页面栈超过 10 层的处理
前面提过页面栈最多 10 层。如果你的应用层级很深,比如从首页一路点进去,A→B→C→D→E→F→G→H→I→J,到第 10 层就跳不动了。用户点一个按钮没反应,也不知道为什么。
解决办法是在跳转前判断当前页面栈层数:
const pages = getCurrentPages() if (pages.length >= 10) { // 用 redirectTo 替换当前页,或者提示用户 wx.redirectTo({ url: '...' }) } else { wx.navigateTo({ url: '...' }) }更好的做法是从产品设计上避免过深的层级,比如用 tab 切换代替层层深入。但技术上这个兜底还是要有的。
8. 把传值逻辑写得更可维护的一些实践
8.1 封装统一的传值工具函数
项目里传值逻辑散落各处,维护起来很累。我的习惯是封装一个工具模块,把常用的传值操作收口。
// utils/navigate.js export function navigateWithParams(url, params) { const query = Object.keys(params) .map(key => `${key}=${encodeURIComponent(params[key])}`) .join('&') wx.navigateTo({ url: `${url}?${query}` }) } export function parseParams(options) { const result = {} Object.keys(options).forEach(key => { result[key] = decodeURIComponent(options[key]) }) return result }这样跳转的时候不用每次手动拼字符串和编码,接收的时候也不用每个参数手动解码。统一收口之后,编码遗漏这类问题基本就杜绝了。
8.2 给临时缓存加命名规范和清理机制
临时传值用的缓存,我建议统一加temp_前缀,并且写一个清理函数,在应用启动或者合适的时机清理掉残留的临时数据。
function clearTempStorage() { const info = wx.getStorageInfoSync() info.keys.forEach(key => { if (key.startsWith('temp_')) { wx.removeStorageSync(key) } }) }这个清理可以放在app.js的onLaunch里,防止上次异常退出留下的临时数据影响这次使用。
8.3 用 TypeScript 约束传值类型
如果项目用 TypeScript,可以给传值定义类型,编译期就能发现类型不匹配的问题。
interface DetailParams { id: number from: string } // 跳转时 navigateWithParams('/pages/detail/detail', { id: 123, from: 'list' } as DetailParams) // 接收时 onLoad(options: Record<string, string>) { const params = parseParams(options) as unknown as DetailParams }虽然小程序的路由参数本质上都是字符串,但通过类型定义可以约束调用方传什么、接收方期望什么,减少沟通成本和低级错误。
8.4 传值逻辑的调试技巧
传值出问题时,最快的排查方式是打日志。在跳转前打印要传的数据,在接收端打印收到的数据,对比一下就知道是传丢了还是解析错了。
// 跳转前 console.log('navigate params:', params) // 接收端 onLoad(options) { console.log('received options:', options) }另外,开发者工具的 Storage 面板可以查看当前缓存的所有数据,调试缓存传值问题时很有用。AppData 面板可以看 globalData 的实时值。这两个面板配合日志,基本能定位大部分传值问题。
9. 写在最后的一点个人体会
传值这件事,技术含量不算高,但它是小程序开发里绕不开的基础设施。我见过太多项目,功能都能跑,但传值逻辑一团乱麻,加个需求就要改好几个地方,还容易改出 bug。根源往往不是技术难,而是一开始没有想清楚每种传值方式的定位,随手就用,用着用着就失控了。
我的建议是,在项目初期就定好规矩:什么场景用路由,什么场景用 globalData,什么场景用缓存,临时缓存的 key 怎么命名,用完怎么清理。这些规矩定下来,后面写代码的时候就不用每次纠结,团队协作也顺畅。传值本身不难,难的是保持一致性。把这件事做好,项目的可维护性能上一个台阶。