微信小程序开发JS:3步实现用户登录状态持久化与Token自动刷新
在微信小程序开发中遇到js逻辑处理时,总感觉代码写出来像拼积木,能跑但总觉得别扭。问题出在大家对小程序js的理解还停留在“照着文档写”的阶段,没有真正建立起“数据驱动视图”的思维模式。今天我们就从实际场景出发,把小程序js里那些容易踩坑、又特别实用的点掰开揉碎了讲清楚。
一、数据绑定不是简单的赋值游戏
刚接触小程序的人最容易犯的错,就是把this.data当成普通变量直接修改。比如你写this.data.userName = '张三',页面不会有任何变化。这是因为小程序的视图层和逻辑层是分离的,必须通过this.setData()这个官方指定的“信使”来传递数据。
举个例子,你做一个购物车页面,用户点击加号按钮增加商品数量。错误的写法是:
// ❌ 错误示范
addCount: function(e) {
let index = e.currentTarget.dataset.index;
this.data.cartList[index].count++;
}
正确做法是先把数据取出来修改,再用setData整个传回去:
// ✅ 正确做法
addCount: function(e) {
let index = e.currentTarget.dataset.index;
let key = 'cartList[' + index + '].count';
let newCount = this.data.cartList[index].count + 1;
this.setData({
[key]: newCount
});
}
这里有个小技巧——动态路径写法。用字符串拼接的方式指定要修改的数组元素,比直接修改整个数组性能好得多。特别是当你的购物车有几十件商品时,这种增量更新的优势非常明显。
二、页面跳转时数据传递的那些坑很多开发者习惯用全局变量或storage来传参,其实小程序本身提供了非常优雅的解决方案。比如从商品列表页跳转到详情页,最正规的做法是利用url传参:
// 列表页
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=商品A'
});
// 详情页 onLoad 中接收
onLoad: function(options) {
console.log(options.id); // 输出 "123"
console.log(options.name); // 输出 "商品A"
}
但这里有个容易被忽略的问题——参数值中的特殊字符。如果商品名称包含&或?,直接拼接url会导致参数解析错误。稳妥的做法是用encodeURIComponent编码:
let name = '商品&A';
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=' + encodeURIComponent(name)
});
// 接收时用 decodeURIComponent 解码
onLoad: function(options) {
let realName = decodeURIComponent(options.name);
}
更复杂的场景是传递对象或数组。比如你要把整个商品信息传到详情页,可以先用JSON.stringify序列化,但要注意url长度限制。超过2048字符的建议改用页面栈数据共享:
// 列表页
let pages = getCurrentPages();
let prevPage = pages[pages.length - 2];
prevPage.setData({
sharedData: { /* 你的数据 */ }
});
// 详情页返回后就能拿到
wx.navigateBack();
三、异步请求的并发控制与防抖
小程序里最常见的场景是搜索框。用户每输入一个字就发一次请求,如果不加限制,服务器会被刷爆。知道用防抖,但具体怎么在小程序里优雅实现?看这个例子:
// 在Page外部定义timer变量
let searchTimer = null;
Page({
data: {
keyword: '',
searchResult: []
},
onInput: function(e) {
let value = e.detail.value;
this.setData({ keyword: value });
// 清除上一次的定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
// 设置新的定时器,300ms后执行
searchTimer = setTimeout(() => {
this.search(value);
}, 300);
},
search: function(keyword) {
wx.request({
url: 'https://api.example.com/search',
data: { q: keyword },
success: (res) => {
this.setData({ searchResult: res.data });
}
});
}
});
注意这里把timer定义在Page外部,而不是data里。因为data里的数据变化会触发页面重绘,而timer不需要参与渲染。另外,请求成功的回调里用了箭头函数,这样this指向Page实例,不用额外保存that变量。
还有一个容易被忽略的细节——请求取消。当用户快速输入时,可能同时有多个请求在飞。如果先发的请求比后发的慢,页面会显示错误的结果。虽然防抖已经减少了请求次数,但极端情况下仍可能发生。可以用abort方法:
let currentRequest = null;
search: function(keyword) {
// 如果有未完成的请求,先取消
if (currentRequest) {
currentRequest.abort();
}
currentRequest = wx.request({
url: 'https://api.example.com/search',
data: { q: keyword },
success: (res) => {
this.setData({ searchResult: res.data });
currentRequest = null;
},
fail: (err) => {
// 注意:abort触发的fail要忽略
if (err.errMsg.indexOf('abort') === -1) {
console.error(err);
}
}
});
}
四、组件间通信的三种姿势
小程序自定义组件开发中,父子组件通信是最常见的需求。只会用properties和events,其实还有更灵活的方式。
场景:父页面有一个表单,子组件是一个地址选择器。用户选择地址后,父页面需要拿到选中的地址。
方法一:triggerEvent(推荐)
// 子组件中
methods: {
onSelect: function(e) {
let address = e.currentTarget.dataset.address;
this.triggerEvent('addresschange', { address: address });
}
}
// 父页面中
<address-picker bind:addresschange="onAddressChange" />
onAddressChange: function(e) {
let selectedAddress = e.detail.address;
this.setData({ address: selectedAddress });
}
方法二:selectComponent(适合直接调用子组件方法)
// 父页面中
let child = this.selectComponent('#addressPicker');
child.setAddress('北京市朝阳区'); // 直接调用子组件方法
// 子组件中
methods: {
setAddress: function(addr) {
this.setData({ address: addr });
}
}
方法三:observers监听(适合被动响应)
// 子组件中
observers: {
'selectedId': function(newVal) {
// 当父组件修改selectedId时自动触发
this.loadAddressDetail(newVal);
}
}
这三种方式各有适用场景。triggerEvent适合事件通知,selectComponent适合主动调用,observers适合数据联动。实际开发中经常混合使用,比如用triggerEvent通知父组件“用户选择了地址”,父组件再用selectComponent告诉子组件“需要高亮某个选项”。
五、性能优化的两个隐藏点觉得小程序性能优化就是少用setData,其实有两个更隐蔽的坑。
第一个是频繁的setData导致渲染卡顿。比如做一个实时显示鼠标位置的页面,每移动1像素就setData一次,页面会非常卡。解决办法是使用虚拟列表或节流:
// 节流函数
function throttle(fn, delay) {
let last = 0;
return function(...args) {
let now = Date.now();
if (now - last > delay) {
last = now;
fn.apply(this, args);
}
};
}
// 页面中使用
onTouchMove: throttle(function(e) {
this.setData({
x: e.touches[0].clientX,
y: e.touches[0].clientY
});
}, 50), // 每50ms更新一次
第二个是页面栈泄漏。当你用wx.navigateTo反复跳转页面,没有用wx.navigateBack返回时,页面栈会越来越深。小程序限制页面栈最多10层,超过后无法再跳转。可以在跳转前检查:
// 获取当前页面栈
let pages = getCurrentPages();
if (pages.length >= 8) { // 留点余量
// 使用redirectTo替代navigateTo
wx.redirectTo({
url: '/pages/new/new'
});
} else {
wx.navigateTo({
url: '/pages/new/new'
});
}
更优雅的做法是使用reLaunch或switchTab来管理页面栈。比如用户从首页进入三级页面后,点击返回按钮时直接回到首页而不是逐级返回:
// 三级页面中
backToHome: function() {
wx.switchTab({
url: '/pages/index/index'
});
}
这样既避免了页面栈过深,又提供了更好的用户体验。记住,页面栈管理是高级开发者和普通开发者的分水岭之一。
最后说一个不知道的细节:setData的key支持数组路径。比如你要修改一个嵌套对象:
this.setData({
'user.info.name': '李四',
'user.info.age': 30
});
这比先取出来整个user对象再修改效率高得多。配合上面提到的数组路径用法,可以写出既高效又优雅的小程序代码。

