电话咨询
QQ咨询
微信咨询
返回顶部

微信小程序开发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);
      }
    }
  });
}
四、组件间通信的三种姿势

小程序自定义组件开发中,父子组件通信是最常见的需求。只会用propertiesevents,其实还有更灵活的方式。

场景:父页面有一个表单,子组件是一个地址选择器。用户选择地址后,父页面需要拿到选中的地址。

方法一: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'
  });
}

更优雅的做法是使用reLaunchswitchTab来管理页面栈。比如用户从首页进入三级页面后,点击返回按钮时直接回到首页而不是逐级返回:

// 三级页面中
backToHome: function() {
  wx.switchTab({
    url: '/pages/index/index'
  });
}

这样既避免了页面栈过深,又提供了更好的用户体验。记住,页面栈管理是高级开发者和普通开发者的分水岭之一。

最后说一个不知道的细节:setData的key支持数组路径。比如你要修改一个嵌套对象:

this.setData({
  'user.info.name': '李四',
  'user.info.age': 30
});

这比先取出来整个user对象再修改效率高得多。配合上面提到的数组路径用法,可以写出既高效又优雅的小程序代码。

上一篇
玩抖音小程序推广差点踩坑,这些“擦边球”操作到底违不违法?
下一篇
青浦小程序开发,青浦小程序开发费用多少