# 小程序节流防抖,骨架屏功能、用户体验优化与实践

TIP

从本节内容开始,我们对微信小程序做基础的性能优化和用户体验优化,使用我们之前学习过的节流防抖函数,骨架屏功能与实践。状态展示自定义组件封装,内容标签页吸顶效果与兼容性配置等。

  • 节流防抖函数在小程序中的应用
  • 优化加载提示 - 骨架屏功能实现
  • 状态展示自定义组件封装
  • 吸顶效果 与 兼容性配置

# 一、节流防抖函数在小程序中的应用

TIP

针对上一节内容提到的频繁触发问题,我们会想到前面学过的节流和防抖函数,这也是前端项目开发和面试过程中的高频和必考题,大家一定要重视起来,经常复习和应用。

# 1、节流 和 防抖区别

TIP

防抖和节流的作用:

都是在高频事件中防止函数被多次调用,是一种性能优化的方案。

防抖和节流的相同点:

  • 都可通通过使用 setTimeout 来实现
  • 都是降低真正的事件处理函数的执行频率,达到节省计算资 源,减少性能的消耗

防抖和节流的不同点:

  • 节流:不管事件触发有多频繁,都会保证在规定时间内执行一次真正的事件处理函数
  • 防抖:只有在间隔时间达到规定时间后才会执行一次真正的事件处理函数,如果在规定时间内再次触发事件,则会重新计时。

节流应用场景:

  • 当一个事件触发的时间特别短频繁时,就会频繁的触发事件处理函数,我们需要通过节流函数来限止执行的频率。
  • 如:在小程序频繁的滑动,点击,搜索联想 等 ....

防抖应用场景:

  • 当用户需要连续执行一个操作,只有停下来或完成后才需要返回结果时,就需要使用防抖函数
  • 如:表单输入、验证、提交,文本框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。

# 2、使用节流函数来解决频繁触发调用问题

TIP

在我们调用的函数外包裹一层节流函数,做一些过滤控制。

比如我们频繁触发调用了 100 次,1000 次 我们要做到在某给时间内只会被执行一次,这就是我们最终要实现的效果。

在项目根目录 utils 中新建 utils.js

/**
 * 节流函数
 * @param {Function} callback 需要被节流的函数
 * @param {Number} duration 距离上次执行超过多少毫秒才会执行被节流的函数
 * @desc 涉及知识点:闭包,this指向
 */
function throttle(callback, duration = 500) {
  // 最后执行函数时的时间戳
  let lastTime = 0;
  // 闭包
  return function () {
    // 获取当前时间戳
    const now = new Date().getTime();
    // 判断当前时间距离上一次执行函数的时间是否超过了duration设定的毫秒数
    if (now - lastTime >= duration) {
      // 超过了
      // 调用被节流的方法实现
      callback.call(this, ...arguments);
      // callback(...arguments)
      // 更新最后执行函数时的时间戳
      lastTime = now;
    }
    // 没超过,啥也不干
  };
}

export { throttle };

# 3、点击分类选项时节流

pages/index/index.js 页面逻辑,课程分类选项方法中应用节流函数

// pages/index/index.js

// 导入节流函数
import { throttle } from "../../utils/utils";

Page({
  // 改造函数声明方式,添加节流函数
  handleCategoryChange: throttle(function (e) {
    const id = e.currentTarget.dataset.id;
    if (this.data.categoryId === id) return;
    this.data.categoryId = id;
    this._getCourseList();
  }),
});

注:

应用节流函数后,频繁点击课程分类选项时,在控制台 network 中查看请求频率,明显降低。

# 4、切换 tab 标签页时节流

TIP

由于点击的 tab 切换这个部分是一个自定义组件,因此需要在自定义组件中来使用节流函数

components/tabs/tabs.js在定义组件 handleTabChange() 点击事件处理函数中应用节流函数

// components/tabs/tabs.js

// 导入节流函数
import { throttle } from "../../utils/utils";

Component({
  // 组件的方法列表
  methods: {
    // 点击 tab 切换,添加节流函数
    handleTabChange: throttle(function (e) {
      const index = e.currentTarget.dataset.index;
      if (index === this.data.currentTabIndex) return;
      this.setData({
        currentTabIndex: index,
      });
      this.triggerEvent("change", { index });
    }),
  },
});

注:

应用节流函数后,频繁点击或滑动 tab 选项时,在控制台 network 中查看请求频率,明显降低

# 二、优化加载提示 - 骨架屏功能实现

TIP

本节我们开始继续优化提升项目列表页的优化交互效果,通过前面的学习和实践已经完成了数据的联动加载和必备的性能优化。

但我们知道数据的加载是有时间的,它会受到服务器和带宽等的影响。有时候我们列表页面数据加载是很慢的,这时候就会出现一个白屏的现象。

解决方案如下

以下截图是微博 APP 端 -> 发现页面,在网速慢或断网的情况下的骨架屏效果

20280429162920

# 1、微信开发者工具 - 模拟不同网络环境

TIP

在微信开发工具中可以手动调整网络环境,2G、3G、4G、WiFi、Offline(离线、断网环境) 当切换至 Offline 时演示效果最佳。

image-20230429171301403

将网络环境切换至 - Offline 时,页面就会出现白屏的情况,体验非常的不好 !

image-20230429171635904

# 2、使用 wx.showLoading 实现正在加载

pages/index/index.js 页面逻辑的生命周期函数中添加 loading 提示

// pages/index/index.js
Page({
  // 生命周期函数--监听页面加载
  onLoad: function (options) {
    // 显示 loading 提示框,需主动调用 wx.hideLoading 才能关闭提示框
    wx.showLoading({
      title: "加载中 ...",
    });
    // 初始化课程列表的数据
    this._getCourseList();
    // 初始化课程分类 swiper 数据
    this._getCategoryList();
  },
});

效果如下

image-20230429172349915

注:

虽然这种方式也能实现,但体验并不是最好的。各大厂主流的移动端解决方案是使用骨架屏的方式,开发辅助 - 骨架屏 (opens new window)

# 3、骨架屏的使用

TIP

微信开发者工具可以为当前正在预览的页面生成骨架屏代码,微信开发者工具入口位于模拟器面板右下角三点处。

image-20230429175706715

点击生成骨架屏,将有弹窗提示是否允许插入骨架屏代码。

确定后将在当前页面同级目录下生成 index.skeleton.wxmlindex.skeleton.wxss 两个文件,分别为骨架屏代码的模板和样式。

image-20230429180515308

# 3.1、在页面中引入骨架屏模板和样式

TIP

骨架屏代码通过小程序模板(template (opens new window))的方式引入 以 pages/index/index 页面为例,引入方式如下。

这些代码可在index.skeleton.wxml 模板文件中找到

<!--pages/index/index.wxml-->

<!-- 在页面顶部引入骨架屏 wxml 模板 -->
<import src="index.skeleton.wxml" />
<template is="skeleton" wx:if="{{loading}}" />

注: wx:if 用来控制骨架屏模板的显示隐藏,需要在页面逻辑中定义 loading 字段

在页面样式文件 pages/index/index.wxss 顶部引入骨架屏模板样式文件

/** pages/index/index.wxss **/

/* 在页面样式文件顶部,引入骨架屏模板样式文件 */
@import "./index.skeleton.wxss";

# 3.2、显示与隐藏

TIP

与普通的模板相同,通过 wx:if 控制显示隐藏

  • 在页面逻辑 pages/index/index.js 中定义显示与隐藏字段 loading 用于控制骨架屏的显示与隐藏。
  • loading 的默认值为 true(显示),在初始化页面数据结束后,重新赋值为 false(隐藏)即可
// pages/index/index.js

Page({
  // 页面的初始数据
  data: {
    // 省略部分 ...

    // 控制骨架屏模板显示隐藏,默认显示
    loading: true,
  },

  // 生命周期函数--监听页面加载
  onLoad: async function (options) {
    // 初始化课程列表的数据
    await this._getCourseList();
    // 初始化课程分类 swiper 数据
    await this._getCategoryList();

    // 在初始化页面数据结束后,将 loading 字段重新赋值为 false(更新完成后,发现在离线(Offline)的网络环境情况下骨架屏效果并没有出来)
    // 由于上面的两次请求是异步的,为了保证异步结果返回后,再更新 loading 数据为 false
    // 需要添加 async / await 关键字 保证是按顺序执行(就像同步代码一样)
    this.setData({
      loading: false,
    });
  },
});

在网络离线模式下,效果如下

image-20230429192621663

注:

我们可以看到骨架屏效果已经出来了,但页面的 swiper 位置无法正常显示显示出来。

根据骨架屏的显示原理,它是根据页面可见区域的显示效果,决定骨架屏最终的生成效果。

因此,我们可以使用一个折中的办法,将页面结构 swiper 部分的代码先删除掉,再重新写一个占位元素,再重新生成骨架屏代码。待生成结束后,再将原来的代码还原回来即可。

其他组件没有类似的问题,放心大胆在企业项目中使用即可。

# 4、骨架屏 Bug 优化

TIP

优化 swiper 部分放入插槽后,生成骨架屏无法正常显示的问题

pages/index/index.wxml 页面结构中

<!--pages/index/index.wxml-->

<!-- 在页面顶部引入骨架屏 wxml 模板 -->
<import src="index.skeleton.wxml" />
<template is="skeleton" wx:if="{{loading}}" />

<view class="container">
  <!-- 使用 tabs 自定义组件 -->
  <i-tabs tabs="{{ tabs }}" bind:change="handleChange">
    <view slot="extend">
      <!-- category start -->
      <view class="category">
        <!-- 增加一个 image 占位符标签 -->
        <image style="width: 97%; height: 150rpx;" />

        <!-- 将 swiper 组件先删除 或 注释掉 -->

        <!-- <swiper 
                    class="category-swiper"
                    display-multiple-items="2"
                    next-margin="80rpx"
                    snap-to-edge="true"
                    >
                    <swiper-item 
                        class="category-swiper-item" 
                        wx:for="{{ categoryList }}" 
                        wx:key="id"
                        >
                    <view class="category-name" bind:tap="handleCategoryChange" data-id="{{ item.id }}"> 
                            {{ item.name }}
                        </view>
                    </swiper-item>
                </swiper> -->
      </view>
      <!-- end category -->
    </view>
    <view slot="panel">
      <view wx:for="{{ courseList }}" wx:key="id">
        <i-course-preview course="{{ item }}" />
      </view>
    </view>
  </i-tabs>
</view>

在微信开发者工具模拟器面板右下角三点处,点击重新生成骨架屏模板,将原来生成的文件覆盖掉即可

image-20230429194932566

替换后,重新在离线(Offline)的网络环境下测试,效果如下

image-20230429195248649

注:

骨架屏效果完成后,即可将 pages/index/index.wxml 页面结构中占位符代码删除,恢复原样即可

# 5、骨架屏的定制化

TIP

  • 可在 project.config.json 增加字段 skeletonConfig 进行骨架屏相关配置,页面配置会覆盖掉全局配置。
  • 开发者可根据需要设置文字、图片、按钮的颜色和形状,同时可根据 excludesremovehide等忽视或隐藏部分页面元素,以获取更优的展示效果。
  • 同时,还可以通过自定义属性的方式,修改特定样式或结构等

详细查阅,微信小程序官方文档 - 生成配置、自定义属性模块 (opens new window)

一般情况下,使用我们上面所讲到的生成骨架屏的方式即可 !这个部分作为了解就好。

# 6、骨架屏的应用场景

TIP

  • 骨架屏通常用于商品列表、新闻列表等页面,对于动画/原生组件较多的页面展示效果不佳
  • 该能力除用于展示首屏骨架外,也可作为局部加载的 loading 样式,可灵活使用

# 7、使用骨架屏的注意事项

TIP

  • ①、骨架屏仅包括页面首屏中的可见区域,对于横向滚动的 swiper 等容器,超出屏幕的子元素将被忽略;
  • ②、骨架屏的布局复用开发者的页面布局,需要骨架屏自适应页面尺寸时,页面布局应采用 rpx 等自适应方案;
  • ③、部分组件如 movable-viewmovable-arearich-texteditorpickerpicker-viewpicker-view-columnadofficail-accountopen-data 无法生成理想的骨架效果,可通过添加一个父容器,结合 grayBlock、empty 等配置,将其置灰。
  • ④、请勿修改自动生成的骨架屏的代码,当效果不理想时,建议调整相关配置,这样当页面变更时,仍可自动生成;
  • ⑤、生成的骨架屏代码中会包含预览时的页面数据,将被用来填充页面;
  • ⑥、如果我们业务需求发生变更了,页面结构修改了,只需要重新生成一次骨架屏代码,覆盖即可

# 三、状态展示自定义组件封装

TIP

  • 状态展示:当页面中没有数据时,显示的组件内容。在移动端 或 APP 开发中常见的功能
  • 实现原理:当我们请求接口数据时,如果后端返回的是空数据,我们就会展示一段提示文本(如:暂无课程数据 ...)
  • 具体代码实现思路:通过小程序提供的逻辑渲染功能 wx:if 通过传递一个变量来控制显隐,wx:else 当不存在数据时,显示一段文本。

但,在项目中有好几处这样数据列表加载,如果我们每个页面都这样在加载到空数据时写这样一段判断,就会有很多的重复实现。

根据前面学过的 《自定义组件封装的原则》可知,我们这里是需要通过自定义组件的方式解决。封装一个状态展示的自定义组件,后续在所有需要的地方,我们直接通过一个组件就能展示。

# 1、创建状态展示自定义组件

components 目录中创建 show-status 状态展示自定义组件

icoding-com-course
├─ components
│ ├─ show-status
│ │ ├─ show-status.js
│ │ ├─ show-status.json
│ │ ├─ show-status.wxml
│ │ └─ show-status.wxss

components/show-status/show-status.wxml 中定义组件的结构

<!--components/show-status/show-status.wxml-->
<!-- 
    show 控制显隐 
    top 距顶部的距离
    content 组件内部文本内容
-->
<view wx:if="{{ show }}" class="container" style="margin-top: {{ top }}rpx;">
  {{ content }}
</view>

components/show-status/show-status.js 中接收组件传值

// components/show-status/show-status.js
Component({
  // 组件的属性列表
  properties: {
    show: {
      type: Boolean,
      value: false,
    },
    content: String,
    top: {
      type: String,
      value: "0",
    },
  },

  // 组件的初始数据
  data: {},

  // 组件的方法列表
  methods: {},
});

components/show-status/show-status.wxss 定义样式

/* components/show-status/show-status.wxss */
/* 该样式是为了后续扩展时使用 */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #888;
}

# 2、在页面中使用状态展示自定义组件

状态展示组件在很多页面中都会用到,因此在 app.json 全局配置文件中引入状态展示自定义组件

{
  "pages": [],
  "window": {},
  "tabBar": {},
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "usingComponents": {
    "i-show-status": "/components/show-status/show-status"
  }
}

pages/index/index.wxml中使用状态展示组件

<!--pages/index/index.wxml-->
<view class="container">
  <i-tabs tabs="{{ tabs }}" bind:change="handleChange">
    <view slot="extend">
      <!-- 省略部分 ... -->
    </view>
    <view slot="panel">
      <!-- 标签页面板区域 -->
      <view wx:for="{{ courseList }}" wx:key="index">
        <i-course-preview course="{{ item }}"></i-course-preview>
      </view>

      <!-- 使用状态展示自定义组件 -->
      <i-show-status
        show="{{ courseList.length < 1 }}"
        content="暂时还么有任何课程信息 ..."
        top="300"
      ></i-show-status>
    </view>
  </i-tabs>
</view>

# 3、测试状态展示自定义组件

pages/index/index.js 页面逻辑中,页面数据初始化的方法中数据置为空 [] 数组

// pages/index/index.js

Page({
  // 生命周期函数--监听页面加载
  onLoad: function (options) {
    // 初始化课程列表
    this._getCourseList();
  },

  // 获取课程列表
  async _getCourseList() {
    // 省略部分 ...

    this.setData({
      // 赋值为空数组 []
      courseList: [],
    });
  },
});

实现效果

image-20230505150516113

注:

状态展示组件在我们很多项目中都会看到,可以很好的提升用户体验,也是非常实用的组件。重点掌握自定义组件在实际工程中的应用于实践。

# 四、吸顶效果 与 兼容性配置

TIP

实现内容标签页的吸顶效果,iOS 端的兼容性配置,提升用户体验。

# 1、内容标签页吸顶效果

components/tabs/tabs.wxss tabs 自定义组件中添加样式

/* components/tabs/tabs.wxss */
.tabs {
  display: flex;
  padding: 20rpx 0;
  background-color: #fff;
  align-items: center;
  /* 
        粘性定位,由于定位机制的原因,往下滑动过第一页后,吸附效果就会消失 
        由于它是根据父元素的高度来决定的,height: 100%; 默认就第一屏可视区
        父元素高度修改为:最小高度 min-height: 100%; 即可
    */
  position: sticky;
  top: 0;
  z-index: 99;
}

效果如下:往下滑动过第一页后,吸附效果就会消失

GIF-2023-5-5-15-48-53

注:

粘性定位,由于定位机制的原因,往下滑动过第一页后,吸附效果就会消失。由于它是根据父元素的高度来决定的,height: 100%;默认就第一屏可视区。

将父元素高度修改为:最小高度 min-height: 100%; 即可

/* components/tabs/tabs.wxss */
.container {
  display: flex;
  flex-direction: column;
  /* height: 100%; 修改为最小高度 min-height: 100%; */
  min-height: 100%;
}

修改后的效果

GIF-2023-5-5-15-47-21

# 2、优化 IOS 端橡皮筋效果

TIP

在苹果手机上不停的上拉触底(不开启也会出现)、下拉刷新(没有开启下拉刷新的页面),会拉出一块白色的区域,这是苹果手机独有的看起来很奇怪。

只需要在 app.json 中增加配置项 backgroundColorBottom (opens new window) 颜色值与当前页面的背景色同色即可

{
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "艾编程",
    "navigationBarTextStyle": "black",
    "backgroundColorBottom": "#F5F5F5"
  }
}

注:

需要在苹果手机上演示才能看到效果 !

# 3、优化导航栏背景色和字体颜色

pages/index/index.json

{
  "navigationBarBackgroundColor": "#96e6a1",
  "navigationBarTextStyle": "white"
}

优化后,效果如下

image-20230505171121615

上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X