微信小程序


微信小程序开发

准备工作

使用浏览器打开本网址,注册小程序开发账号。然后获取一个小程序 APPID 即可(“开发”一栏中即可找到)。最后下载微信小程序开发者工具。

小程序的项目结构

  • pages 用来存放所有小程序的页面。
  • utils 用来存放工具性质的模块(例如:格式化时间的自定义模块)。
  • app.js 是小程序项目的入口文件。
  • app.json 是小程序项目的全局配置文件。
  • app.wxss 是小程序项目的全局样式文件。
  • project.config.json 是项目的配置文件。
  • sitemap.json 用来配置小程序及其页面是否允许被微信索引。

小程序的页面结构

小程序官方建议把所有小程序的页面,都存放在 pages 目录中,以单独的文件夹存在。其中,每个页面由 4 个基本文件组成,它们分别是:

  • .js 文件(页面的脚本文件,存放页面数据,事件处理函数等)
  • .json 文件(当前页面的配置文件,配置窗口的外观、表现等)
  • .wxml 文件(页面的模板结构文件)
  • .wxss 文件(当前页面的样式表文件)

新建小程序页面

只需要在 app.json 中的 pages 属性中新增路径即可,小程序开发者工具就会帮助我们创建对应的页面文件。通过调整 pages 数组中页面路径的前后顺序,即可修改项目的首页。小程序会把排在第一位的页面,当作项目首页进行渲染。

js 文件

小程序项目中一共有 3 中 js 文件,分别是:

  1. app.js:是整个小程序项目的入口文件,通过调用 App() 函数来启动整个小程序。
  2. 页面的 .js 文件:是页面的入口文件,通过调用 Page() 函数来创建并运行页面。
  3. 普通的 .js 文件:是普通的功能模块文件,用来封装公共的函数或属性供页面使用。

json 配置文件

小程序项目中一共有 4 种 json 配置文件,分别是:

  1. 项目根目录中的 app.json 配置文件。

    app.json 是当前小程序的全局配置,包括了小程序的所有页面路径、窗口外观、界面表现、底部 tab 等。

    {
      // pages用来记录当前小程序所有页面的路径
      "pages": [
        "pages/index/index",
        "pages/logs/logs"
      ],
      // window用来全局定义小程序所有页面的背景色、文字颜色等
      "window": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "Weixin",
        "navigationBarBackgroundColor": "#ffffff"
      },
      // style用于全局定义小程序组件所使用的样式版本
      "style": "v2",
      "componentFramework": "glass-easel",
      // 用于指明sitemap.json的存放位置
      "sitemapLocation": "sitemap.json",
      "lazyCodeLoading": "requiredComponents"
    }
  2. 项目根目录中的 project.config.json 配置文件。

    project.config.json 是项目配置文件,用来记录我们对小程序开发工具所做的个性化配置。例如:

    • setting 保存了编译相关的配置。
    • projectname 保存了项目名称。
    • appid 保存的是小程序的账号 ID。(要运行别的小程序需要更改这个 appid 为自己的开发者 id)
  3. 项目根目录中的 sitemap.json 配置文件。

    微信现已开放小程序内搜索,效果类似于 PC 网页的 SEO。sitemap.json 文件用来配置小程序页面是否允许微信索引。当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索关键字和页面的索引匹配成功的时候,小程序的页面将可能展示在搜索结果中。

  4. 每个页面文件夹中的 .json 配置文件。

    小程序中的每一个页面,可以使用 json文件来对本页面的窗口外观进行配置,页面中的配置项会覆盖 app.json的 window 中相同的配置项。

WXML

WXML(WeiXin Markup Language)是小程序框架设计的一套标签语言,用来构建小程序页面的结构,其作用类似于网页开发中的 HTML。

WXML 与 HTML 的区别:

  1. 标签名称不同:
    • HTML(div,span,img,a)
    • WXML(view,text,image,navigator)
  2. 属性节点不同:
    • <a href="#">超链接</a>
    • <navigator url="/pages/home/home"></navigator>
  3. 提供了类似于 Vue 中的模板语法。
    • 数据绑定。
    • 列表渲染。
    • 条件渲染。

WXSS

WXSS(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式,类似于网页开发中的 CSS。

WXSS 与 CSS 的区别:

  1. 新增了 rpx 尺寸单位。
    • CSS 中需要手动进行像素单位换算,例如 rem。
    • WXSS 在底层支持新的尺寸单位 rpx,在不同大小的屏幕上小程序会自动进行换算。
  2. 提供了全局的样式和局部样式。
    • 项目根目录中的 app.wxss 会作用于所有小程序页面。
    • 局部页面的 .wxss 样式仅对当前页面有效。
  3. WXSS 仅支持部分 CSS 选择器。

小程序的宿主环境

宿主环境(host environment)指的是程序运行所必须的依赖环境。例如:Android 系统和 IOS 系统是两个不同的宿主环境。安卓版的微信 App 是不能在 IOS 环境下运行的。所以,Android 是安卓软件的宿主环境,脱离了宿主环境的软件是没有任何意义的!

而小程序的宿主环境是手机微信。小程序借助宿主环境提供的能力,可以完成许多普通网页无法完成的功能,例如:微信扫码、微信支付、微信登录、地理定位、etc……

微信为小程序提供了以下环境内容支持:

  1. 通信模型。
  2. 运行机制。
  3. 组件。
  4. API。

通信模型

小程序中的主体是渲染层和逻辑层,其中:WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。小程序中的通信模型分为两部分:

  1. 渲染层和逻辑层之间的通信:由微信客户端进行转发。
  2. 逻辑层和第三方服务器之间的通信:由微信客户端进行转发。

运行机制

小程序启动的过程主要分成五个步骤:

  1. 把小程序的代码包下载到本地。
  2. 解析 app.json 全局配置文件。
  3. 执行 app.js 小程序入口文件,调用 App() 创建小程序实例。
  4. 渲染小程序首页。
  5. 小程序启动完成。

小程序页面的渲染过程:

  1. 加载解析页面的 .json 配置文件。
  2. 加载页面的 .wxml 模板和 .wxss 样式。
  3. 执行页面的 .js 文件,调用 Page() 创建页面实例。
  4. 页面渲染完成。

组件

组件内容见下一节。

API

小程序中的 API 是由宿主环境提供的,通过这些丰富的小程序 API,开发者可以方便的调用微信提供的能力,例如:获取用户信息、本地存储、支付功能等。

小程序官方把 API 分为三类:

  1. 事件监听 API:以 on 开头,用来监听某些事件的触发。
  2. 同步 API:以 sync 结尾的 API,同步 API 的执行结果,可以通过函数返回值直接获取,如果执行出错会抛出异常。
  3. 异步 API:类似 ajax,需要通过 success、fail、complete 接收调用的结果。

组件

小程序中的组件也是由宿主环境提供的,开发者可以基于组件快速搭建出漂亮的页面结构。官方把小程序的组件分为了 9 大类,常用的有:视图容器、基础内容、表单组件、导航组件等。

常用的视图容器类组件

view

  • 普通视图区域。
  • 类似于 HTML 中的 div,是一个块级元素。
  • 常用来实现页面的布局效果。
<view class="container1">
  <view>A</view>
  <view>B</view>
  <view>C</view>
</view>

scroll-view

  • 可滚动的视图区域。
  • 一般用来实现可滚动的列表效果。
<!-- 
  scroll-y 属性:允许纵向滚动
  scroll-x 属性:允许横向滚动
  注意:在使用纵向滚动时,需要在样式中添加一个固定高度
-->
<scroll-view class="container1" scroll-y>
  <view>A</view>
  <view>B</view>
  <view>C</view>
</scroll-view>
.container1 {
  border: 1px solid red;
  /* 给scroll-view固定高度 */
  height: 120px;
  width: 100px;
}

swiper 和 swiper-item

  • 轮播图的容器组件和轮播图项组件。
<!-- 
  indicator-dots 属性:显示面板指示点
  indicator-color 属性:未选中的指示点颜色
  indicator-active-color 属性:被选中的指示点颜色
  autoplay:是否自动切换
  interval:自动切换时间间隔
  circular:是否采用衔接滑动(最后一张和第一张连到一起循环播放)
-->
<swiper class="swiper-container" autoplay="true" interval="3000" indicator-dots="true">
  <!-- 第一个轮播图 -->
  <swiper-item>
    <view class="item">A</view>
  </swiper-item>

  <!-- 第二个轮播图 -->
  <swiper-item>
    <view class="item">B</view>
  </swiper-item>

  <!-- 第三个轮播图 -->
  <swiper-item>
    <view class="item">C</view>
  </swiper-item>
</swiper>

常用的基础内容组件

text

  • 文本组件。
  • 类似于 HTML 中的 span 标签,是一个行内元素。

通过 text 组件中的 selectable 属性,实现长按选中文本内容的效果:

<view>
  手机号支持长按选中效果:
  <text selectable="true">13729486775</text>
</view>

rich-text

  • 富文本组件。
  • 支持把 HTML 字符串渲染为 WXML 结构。

通过该组件,可以把 HTML 字符串渲染为对应的 WXML 结构:

<rich-text nodes="<h1 style='color: red'>标题</h1>"></rich-text>

其他常用组件

button

  • 按钮组件。
  • 功能比 HTML 中的 button 按钮丰富。
  • 通过 open-type 属性可以调用微信提供的各种功能(客服、转发、获取用户授权、获取用户信息等)
<button>普通按钮</button>
<button type="primary">主色调按钮</button>
<button type="warn">警告按钮</button>

<button size="mini">普通小按钮</button>
<button type="primary" size="mini">主色小调按钮</button>
<button type="warn" size="mini">警告小按钮</button>

<button plain>普通镂空按钮</button>
<button type="primary" plain>主色调镂空按钮</button>
<button type="warn" plain>警告镂空按钮</button>

image

  • 图片组件。
  • 该组件默认宽度 300px、高度 240px。

注意默认情况下 image 组件也会占用空间!

<image class="image-container"></image> <!-- 这个image也会占据空间 -->
<image class="image-container" src="/image/Elieen2.jpg"></image>

image 组件中的 mode 属性用来指定图片的裁剪和缩放模式,常用的 mode 如下:

mode 值 说明
scaleToFill (默认值)缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFit 缩放模式,保持纵横比缩放图片,可以完整地将图片显示出来
aspectFill 缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来
widthFix 缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
heightFix 缩放模式,高度不变,宽度自动变化,保持原图宽高比不变
  • 页面导航组件。
  • 类似于 HTML 中的 a 链接。

详情见 “页面导航” 一节。

WXML 模板语法

数据绑定

在 页面 js 文件的 data 中定义数据:

Page({
  /**
   * 页面的初始数据
   */
  data: {
    info: 'hello world',
    imgSrc: '/image/Elieen2.jpg',
    randomNum: Math.random() * 10 // 生成10以内的随机数
  }
})

在 WXML 中利用插值表达式渲染数据:

<view>{{ info }}</view>
<image src="{{ imgSrc }}" mode="widthFix"></image>
<view>{{ randomNum >= 5 ? '大于等于5': '小于5' }}</view>

setData 对数据的修改(拓展)

修改对象类型数据

新增和修改如下:

data: {
  userInfo: {}
},

// 新增、修改单个
updateUserInfo() {
  // 新增属性(修改属性也是同样的)
  this.setData({
    // 可以写成数据路径的方式
    'userInfo.name': 'tom',
    'userInfo.id': '1001'
  })
}

// 新增、修改多个 
updateUserInfo() {
  // 使用es6语法
  const userInfo = {
    ...this.data.userInfo,
    name: 'tom',
    id: '1001'
  }
  // 下述写法也可以
  // const userInfo = Object.assign(this.data.userInfo, { name:'tom' }, { id: '1001' })
  
  // 用新值进行覆盖
  this.setData({
    userInfo: userInfo
  })
}

删除数据如下:

// 删除单个
updateUserInfo() {
  // 删除属性
  delete this.data.userInfo.id
  // 同样也是新值覆盖旧值
  this.setData({
    userInfo: this.data.userInfo
  })
}

// 删除多个
updateUserInfo() {
  // 使用解构表达式和剩余参数进行处理
  const { age, id, ...rest } = this.data.userInfo
  // age和id将被删除
  this.setData({
    userInfo: rest
  })
}

修改数组类型数据

新增数组元素:

data: {
  list: [1, 2, 3]
},

updateList() {
  // 新增数组元素(先修改,再覆盖)
  this.data.list.push(4)
  this.setData({
    list: this.data.list
  })

  // 新增数组元素(利用concat进行先修改再赋值)
  const newList1 = this.data.list.concat(4)
  this.setData({
    list: newList1
  })

  // 使用解构表达式
  const newList2 = [...this.data.list, 4]
  this.setData({
    list: newList2
  })
}

修改数组元素:

updateList() {
  // 修改数组元素
  this.setData({
    'list[2]': 6
  })
}

删除数组元素:

updateList() {
  // 删除数组元素
  this.data.list.splice(1, 1)
  this.setData({
    list: this.data.list
  })

  const newList = this.data.list.filter(item => item !== 2)
  this.setData({
    list: newList
  })
}

事件绑定

小程序中常用的事件:

类型 绑定方式 事件描述
tap bindtap 或 bind:tap 类似于 click 事件
input bindinput 或 bind:input 文本框的输入事件
change bindchange 或 bind:change 状态改变时触发

当事件回调触发的时候,会收到一个事件对象 event,它的详细属性如下表所示:

属性 类型 说明
type String 事件类型
timeStamp Integer 页面打开到触发事件所经过的毫秒数
target Object 触发事件的组件的一些属性值集合
currentTarget Object 当前组件的一些属性值集合
detail Object 额外的信息
touches Array 触摸事件,当前停留在屏幕中的触摸点信息的数组
changedTouches Array 触摸事件,当前变化的触摸点信息的数组

target 是触发该事件的源头组件,而 currentTarget 是当前事件所绑定的源头组件。举例如下:

<view bindtap="outerHandler">
  <button>普通按钮</button>
</view>

绑定事件是绑在 view 的,而 view 中有一个按钮,当我们点击内部的按钮时,点击事件以冒泡的方式向外扩散,也会触发外层 view 的 tap 事件处理函数。此时,对于 view 来说:

  • e.target 指向的是触发事件的源头组件,是内部的按钮组件。
  • e.currentTarget 指向的是当前正在触发事件的那个组件,是 view 组件。

tap 事件

示例如下:

<button bindtap="btnTapHandler">普通按钮</button>
Page({
  btnTapHandler(e) {  // 这里的e就是事件event
    console.log(e)
  }
})

在触发事件中,可以利用 this.setData(dataObject) 方法,给 data 属性重新赋值:

Page({
  data: {
    info: 'hello world',
  },

  /**
   * 定义按钮的事件处理函数
   */
  btnTapHandler() {
    this.setData({
      // 注意这里取info是this.data.info
      info: this.data.info + " on click"
    })
  }
})

input 事件

通过 input 事件完成文本框和数据的绑定:

<input bindinput="inputHandler"></input>
inputHandler(e) {
    // e.detail.value是变化过后,文本框的最新值
    // console.log(e.detail.value)
    this.setData({
        msg: e.detail.value
    })
}

事件传参

小程序的事件传参比较特殊,我们不能在绑定事件的同时为事件处理函数传递参数。例如,下述代码不能正常工作:

<button bindtap="btnHandler(123)">事件传参</button>

可以为组件提供 data-* 来传递参数,其中 * 是参数名字:

<button bindtap="btnHandler" data-info="{{ 2 }}">事件传参</button>

而在事件处理函数中,通过 event.target.dataset.参数名 可以获取到具体参数的值:

btnHandler(event) {
    // dataset包含了所有通过data-*传递过来的参数项
    console.log(event.target.dataset)
    // 通过dataset可以访问到具体参数值
    console.log(event.target.dataset.info)
}

除了使用 data-* 传参之外,还可以使用 mark 标记传递参数。mark 是一种自定义属性,可以在组件上添加,用于来识别具体触发事件的 target 节点。同时 mark 还可以用于承载一些自定义数据。

<!-- 语法为mark:自定义属性="值" -->
<button bind:tap="btnHandler" mark:id="1" mark:name="tom">按钮</button>
// 使用事件对象接受参数
btnHandler(event) {
  console.log(event)
  var id = event.mark.id
  var name = event.mark.name
  console.log(id + ' ' + name)
}

通过事件对象获取到的是触发事件的节点以及其父节点身上所有的 mark 数据。

一般情况下,数据量少的时候,推荐使用 data-*,如果需要使用大量自定义数据,这个时候再用 mark。

条件渲染

在小程序中,使用 wx:if={{condition}} 来判断是否需要渲染该代码块:

<view wx:if="{{ type == 0 }}">男</view>
<view wx:elif="{{ type == 1 }}">女</view>
<view wx:else>其他</view>

如果要一次性控制多个组件的展示和隐藏,可以使用一个 block 标签将多个组件包装起来,并使用 wx:if 控制属性:

<block wx:if="{{ true }}">
  <view>A</view>
  <view>B</view>
</block>

在小程序中,也可以直接使用 hidden 来控制组件的显示与隐藏:

<view hidden="{{ condition }}">使用hidden控制显示和隐藏</view>

hidden 与 wx:if 的区别:

  • wx:if 以动态创建和移除元素的方式,控制元素的展示与隐藏,当控制条件复杂时,使用 wx:if。
  • hidden 以切换样式的方式控制元素的显示和隐藏,当频繁切换时,使用 hidden。

列表渲染

使用 wx:for 可以进行列表渲染:

<view wx:for="{{ array }}">
  索引是:{{ index }},item项是:{{ item }}
</view>

类似于 Vue 列表渲染中的 :key,小程序在实现列表渲染时,也建议为渲染出来的列表项指定唯一的 key 值,从而提高渲染的效率,示例代码如下:

data: {
    userList: [
        {id:1, name:"A"},
        {id:2, name:"B"},
        {id:3, name:"C"}
    ] 
}
<view wx:for="{{ userList }}" wx:key="id">{{ item.name }}</view>

WXSS 模板样式

相比 CSS,WXSS 扩展的特性有:rpx 尺寸单位、@import 样式导入。

rpx 尺寸单位

rpx(responsive pixel)是微信小程序独有的,用来解决屏适配的尺寸单位。

rpx 的实现原理非常简单:鉴于不同设备屏幕的大小不同,为了实现屏幕的自动适配,rpx 把所有设备的屏幕,在宽度上等分为 750 份(即:当前屏幕的总宽度为 750 rpx)。这就导致在较大的设备上,1 rpx 所代表的宽度较大,反之较小。

一般认为 1 rpx = 0.5 px = 1 个物理像素。

样式导入

使用 WXSS 提供的 @import 语法,可以导入外联的样式表。

/* common.wxss */
.phone {
    color: red;
}

/* list.wxss */
@import '/common/common.wxss'
/* 注意common文件夹要在page里面 */

全局样式和局部样式

定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在页面的 .wxss 文件中定义的样式为局部样式,只作用于当前页面。

当局部样式和全局样式冲突时,根据就近原则,局部样式会覆盖全局样式。当局部样式的权重大于或等于全局样式的权重时,才会覆盖全局的样式。

全局配置

小程序根目录下的 app.json 文件是小程序的全局配置文件。常用的配置项如下:

  1. pages:记录当前小程序所有页面的存放路径。
  2. window:全局设置小程序窗口的外观。
  3. tabBar:设置小程序底部的 tabBar 效果。
  4. style:是否启用新版的组件样式。

window 节点

其中,window 节点常用配置如下:

属性名 类型 默认值 说明
navigationBarTitleText String 字符串 导航栏标题文字内容
navigationBarBackedColor HexColor #000000 导航栏背景颜色
navigationBarTextStyle String white 导航栏标题颜色,只支持 white 和 balck
backgroundColor HexColor #ffffff 窗口背景色
backgroundTextStyle String dark 下拉 loading 样式,只支持 dark 和 light
enablePullDownRefresh Boolean false 是否全局开启下拉刷新
onReachBottomDistance Number 50 页面上拉触底事件触发时距页面底部距离,单位为 px

tabBar 节点

tabBar 是移动端应用常见的页面效果,用于实现多页面的快速切换。小程序中通常将其分为:底部 tabBar、顶部 tabBar。其中,tabBar 只能配置最少 2 个、最多 5 个的 tab 页签。当渲染顶部 tabBar 时,不显示 icon,只显示文本。

tabBar 有 6 个组成部分:

  1. backgroundColor:tabBar 的背景色。
  2. selectedIconPath:选中时图片路径。
  3. borderStyle:tabBar 上边框的颜色。
  4. iconPath:未选中时的图片路径。
  5. selectedColor:tab 上的文字选中时的颜色。
  6. color:tab 上文字未选中(默认)的颜色。

tabBar 的配置项:

属性 类型 必填 默认值 说明
position String 否 bottom tabBar 的位置,仅支持 bottom、top
borderStyle String 否 black tabBar 上边框的颜色,仅支持 black、white
color HexColor 否 tab 上文字的默认颜色
selectedColor HexColor 否 tab 上文字的选中时颜色
backgroundColor HexColor 否 tabBar 的背景颜色
list Array 是 tab 页签列表,最少 2 个,最多 5 个 tab

list 中存放的是 tab 页签,每个 tab 页签的配置项如下:

属性 类型 必填 说明
pagePath String 是 页面路径,页面必须要在 pages 中先预定
text String 是 tab 上显示的文字
iconPath String 否 未选中的图标路径
selectedPath String 否 选中时的图标路径

页面配置

在小程序中,app.json 文件中的 window 节点,可以全局配置小程序中每个页面的窗口表现。如果某些小程序页面想要拥有特殊的窗口表现,此时,“页面级别的 .json 配置文件” 就可以实现这种需求。

页面配置的配置选项详情请见 “全觉配置” 一节,二者一致。

注意,对于下拉刷新这个选项,一般不要在全局中开启,而是把这一项放在具体的页面中进行单独配置。

网络数据请求

出于安全性方面的考虑,小程序官方对数据接口的请求做出了如下两个限制:

  1. 只能请求 HTTPS 类型的接口。
  2. 必须将接口的域名添加到信任列表中。

配置合法的 request 域名需要在小程序开发页面中,左侧一栏选择 “开发” 选项,进入 “开发设置”,在 “服务器域名” 一栏中进行域名的配置。

注意事项:

  1. 域名只支持 https 协议。
  2. 域名不能使用 IP 地址或 localhost。
  3. 域名必须经过 ICP 备案。
  4. 服务器域名一个月内最多可以申请 5 次修改。

发起 GET 请求

调用微信小程序提供的 wx.request() 方法,可以发起 GET 数据请求:

getInfo() {
  wx.request({
    url: 'https://www.escook.cn/api/get', // 指定url
    // url: `https://www.escook.cn/${this.data.query.id}`,	利用模板字符串填入动态参数
    method: 'GET',
    data: {	// 携带data
      name: 'zhangsan',
      age: 20
    },
    success: (res) => {
      console.log(res.data)
    }
  })
}

发起 POST 请求

getInfo() {
  wx.request({
    url: 'https://www.escook.cn/api/post', // 指定url
    method: 'POST',
    data: {	// 携带data
      name: 'zhangsan',
      age: 20
    },
    success: (res) => {
      console.log(res.data)
    }
  })
}

在页面刚加载的时候请求数据

在很多情况下,我们需要在页面刚加载的时候,自动请求一些初始化的数据。此时需要在页面的 onLoad 事件中调用获取数据的函数,示例代码如下:

onLoad(options) {
    this.getInfo()
}

跳过 request 合法域名校验

如果后端程序员仅仅提供了 http 协议的接口、暂时没有提供 https 协议的接口,此时为了不耽误开发的进度,我们可以在微信开发者工具中,临时开启 “开发环境不校验请求域名、TLS 版本及 HTTPS 证书” 选项,跳过 request 合法域名的校验。

但是在上线的时候依旧要关掉这个选项并且配置合法域名!!!

关于跨域和 Ajax 说明

跨域问题只存在于基于浏览器的 Web 开发中。由于小程序的宿主环境不是浏览器,而是微信客户端,所以小程序中不存在跨域的问题。

Ajax 技术的核心是依赖于浏览器中的 XMLHttpRequest 这个对象。由于小程序的宿主环境是微信客户端,所以小程序中不能叫做 “发起Ajax请求”,而是叫做 “发起网络数据请求”。

页面导航

页面导航指的是页面之间的相互跳转,小程序中有两种方式进行页面导航:

  1. 声明式导航
    • 在页面上声明一个 navigator 导航组件。
    • 通过点击 navigator 组件实现页面跳转。
  2. 编程式导航
    • 调用小程序的 API,实现页面的跳转。

声明式导航

导航到 tabBar 页面

tabBar 页面指的是被配置为 tabBar 的页面。

在使用 navigator 组件跳转到指定的 tabBar 页面时,需要指定 url 属性和 open-type 属性,其中:

  • url 表示要跳转的页面的地址,必须以 / 开头。
  • open-type 表示跳转的方式,必须为 switchTab。
<navigator url="/pages/message/message" open-type="switchTab">导航到消息页面</navigator>

导航到非 tabBar 页面

非 tabBar 页面指的是没有被配置为 tabBar 的页面。

在使用 navigator 组件跳转到普通的非 tabBar 页面时,需要指定 url 属性和 open-type 属性,其中:

  • url 表示要跳转的页面的地址,必须以 / 开头。
  • open-type 表示跳转的方式,必须为 navigate。(该属性在导航非 tabBar 页面时可以省略不写)
<navigator url="/pages/info/info" open-type="navigate">导航到info页面</navigator>

该种方式还可以进行导航传参:

<navigator url="/pages/info/info?username=zhangsan&age=20">导航到info页面</navigator>

后退导航

如果要后退到上一页面或多级页面,则需要指定 open-type 属性和 delta 属性,其中:

  • open-type 的值必须是 navigateBack,表示要进行后退导航。
  • delta 的值必须是数字,表示要后退的层级。(delta 的值默认为 1,如果只是返回上一级的话可以省略不写)
<navigator open-type="navigateBack" delta="1">返回上一页</navigator>

编程式导航

导航到 tabBar 页面

调用 wx.switchTab(Object object) 方法,可以跳转到 tabBar 页面。其中 Object 参数对象的属性列表如下:

属性 类型 是否必选 说明
url String 是 需要跳转的 tabBar 页面的路径,路径后不能带参数
success function 否 接口调用成功的回调函数
fail function 否 接口调用失败的回调函数
complete function 否 接口调用结束的回调函数(调用成功、失败都会执行)

示例:

<button bindtap="toIndex">导航到index页面</button>
toIndex() {
  wx.switchTab({
    url: '/pages/index/index'
  })
}

导航到非 tabBar 页面

调用 wx.navigateTo(Object object) 方法,可以跳转到非 tabBar 页面。其中 Object 参数对象的属性列表同上。

示例:

<button bindtap="toInfo">导航到info页面</button>
toInfo() {
  wx.navigateTo({
    url: '/pages/info/info'
  })
}

该种方式也可以进行传参:

toInfo() {
  wx.navigateTo({
    url: '/pages/info/info?name=zhagnsan&age=20'
  })
}

后退导航

调用 wx.navigateBack(Object object) 方法,可以进行后退导航。其中 Object 参数对象的属性列表如下:

属性 类型 是否必选 说明
delta Integer 是 返回的页面数,如果 delta 大于现有页面数,则返回到首页,默认值为 1
success function 否 接口调用成功的回调函数
fail function 否 接口调用失败的回调函数
complete function 否 接口调用结束的回调函数(调用成功、失败都会执行)
<button bindtap="goBack">返回上一级</button>
goBack() {
  wx.navigateBack({
    delta: 1
  })
}

在 onLoad 中接收导航参数

通过声明式导航传参或编程式盗汗传参所携带的参数,可以直接在 onLoad 事件中直接获取到,示例代码如下:

data: {
  // 导航参数空对象
  query: {}  
},

onLoad(options) {
  // options就是导航传递过来的参数对象
  console.log(options)
  this.setData({
    query: options
  })
}

自定义导航栏

小程序默认的导航栏与 APP 一样都位于顶部固定位置。但是默认导航栏可能会影响小程序整体风格,且无法满足特定的设计需求,这时候,就需要进行自定义导航栏。

在 app.json 或者 page.json 中,配置 navigationStyle 属性为 custom,即可自定义导航栏。在设置之后,会移除默认的导航栏,只保留右上角胶囊按钮。

页面事件

下拉刷新

下拉刷新是移动端的专有名词,指的是通过手指在屏幕上的下拉滑动操作,从而重新加载页面数据的行为。

开启下拉刷新需要在 .json 配置文件中更改 enablePullDownRefresh 的值,建议针对指定页面进行下拉刷新的操作,而不是直接在全局上进行统一配置。

通过 backgroundColor 配置下拉刷新窗口背景颜色,通过 backgroundTextStyle 配置下拉刷新 loading 样式。

在页面的 .js 文件中,使用 onPullDownRefresh() 方法可以监听当前页面的下拉刷新事件:

/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
  console.log('触发下拉刷新')
}

停止下拉刷新的效果(虽然在模拟器上下拉刷新可以自动回弹,但是在移动端的下拉刷新是没办法自动回弹的):

// 调用wx.stopPullDownRefresh()可以停止下拉刷新操作
onPullDownRefresh() {
  this.setData({
    randomNum: 0
  })
  // 做好上述的操作后,调用stopPullDownRefresh方法就可以关闭下拉刷新效果了
  wx.stopPullDownRefresh()
}

上拉触底

上拉触底是移动端的专有名词,通过手指在屏幕上的上拉滑动操作,从而加载更多数据的行为。实际开发中,上拉触底更多用来实现分页操作。

在页面 .js 文件中,通过 onReachBottom() 方法来触发上拉触底事件:

onReachBottom() {
  console.log('触发上拉触底事件')
}

每一次上拉触底都会触发该方法的执行,如果前一次的事件还没有彻底执行完毕就再一次触发上拉触底,就会导致请求频繁更新,需要进行节流。

节流的步骤如下:

  1. 在 data 中定义 isloading 节流阀:
    • false 表示当前没有任何数据请求。
    • true 表示当前正在进行数据请求。
  2. 在上拉触底方法中修改 isloading 的值:
    • 刚调用的时候立刻将节流阀设置为 ture。
    • 在网络请求完毕后(complete 函数),将节流阀重置为 false。
  3. 在 onReachBottom 中判断节流阀的值,从而对数据请求进行节流控制:
    • 如果节流阀的值为 ture,则可以进行请求。
    • 如果节流阀的值为 false,则不能进行请求。
data: {
  colorList: [],
  isloading: false
}

getColors() {
  // 控制节流阀
  this.setData({
    isloading: true
  })

  // 展示加载框
  wx.showLoading({title: '数据加载中...'})

  wx.request({
    url: 'https://applet-base-api-t.itheima.net/api/color',
    method: 'GET',
    success: ({data: res}) => {
      this.setData({
        colorList: [...this.data.colorList, ...res.data]
      })
    },
    // 方法调用完成之后,重置节流阀并关闭加载框
    complete: () => {
      this.setData({
        isloading: false
      })
      wx.hideLoading()
    }
  }) 
}

/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
  // 判断节流阀是否为真,为真则表示不能进行请求
  if (this.data.isloading) {
    return
  }
  this.getColors()
}

可以在全局或者页面的 .json 配置文件中,通过 onReachBottomDistance 属性来配置上拉触底的距离。

页面间通信

如果一个页面通过 wx.navigateTo 打开一个新页面,这两个页面间将建立一条数据通道。

  1. 在 wx.navigateTo 的 success 回调中通过 EventChannel 对象发射事件。
  2. 被打开的页面通过 this.getOpenerEventChannel 方法来获得一个 EventChannel 对象,进行监听、发射事件。
  3. wx.navigateTo 方法中可以定义 events 配置项接收被打开页面发射的事件。

list 页面的 .js 文件:

btnHandler() {
  wx.navigateTo({
    url: '/pages/info/info',  // 要跳转到的页面路径
    events: {
      // key: 被打开页面通过eventChannel传递回来的世事件
      // value: 回调函数
      // 为事件添加一个监听器,获取被打开页面传递给当前页面的数据
      currentEvent: (res) => {
        console.log(res)
      }
    },
    success: (res) => {
      // emit可以发射事件,同时传递数据
      res.eventChannel.emit('myevent', { name: 'tom' })
    }
  })
}

info 页面的 .js 文件:

onLoad(options) {
  // 获取channel对象
  const EventChannel = this.getOpenerEventChannel()

  // 通过EventChannel监听事件
  EventChannel.on('myevent', (res) => {
    // res为传递的数据
    console.log(res)
  })
    
  // 也可以把数据传递给上一级页面
  EventChannel.emit('currentevent', { age: 10 })
},

生命周期

生命周期(Life Cycle)是指一个对象从创建、运行到销毁的整个阶段,强调的是一个时间段。小程序启动,表示生命周期开始,小程序关闭,表示生命周期结束。在开启和关闭中间这个阶段,就是小程序的生命周期。

在小程序中,声明周期分为两类:

  1. 应用生命周期:特指小程序从启动、运行到销毁的过程。
  2. 页面生命周期:特指小程序中,每个页面的加载、渲染和销毁的过程。

由此可见,应用生命周期包含了页面生命周期(小程序开始、渲染各个页面、小程序结束)

生命周期函数

生命周期函数是由小程序框架提供的内置函数,会伴随着生命周期,自动按次序执行。

生命周期函数的作用:允许程序员在特定的时间点,执行某些特定的操作。例如,页面刚加载的时候,可以在onLoad 生命周期函数中初始化页面的数据。相对于生命周期,生命周期函数更强调时间点。

小程序中的生命周期函数分为两类:

  1. 应用的生命周期函数:特指小程序从启动、运行到销毁期间依次调用的那些函数。

    应用生命周期函数需要在 app.js 中进行声明,示例如下:

    // app.js
    App({
    
      /**
       * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
       * 可以从本地存储中读取数据然后加载到小程序中
       */
      onLaunch: function () {},
    
      /**
       * 当小程序启动,或从后台进入前台显示,会触发 onShow
       */
      onShow: function (options) {},
    
      /**
       * 当小程序从前台进入后台,会触发 onHide
       */
      onHide: function () {},
    
      /**
       * 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
       */
      onError: function (msg) {}
        
    })
  2. 页面的生命周期函数:特指小程序中,每个页面从加载、渲染到销毁期间依次调用的那些函数。

    页面声明周期函数需要在页面的 .js 文件中进行声明:

    Page({
      
      data: {
        query: {}
      },  
        
      /**
       * 生命周期函数--监听页面加载,一个页面只调用1次
       */
      onLoad(options) {
        // 数据转存到data中
        this.setData({
          query: options
        })
      },
    
      /**
       * 生命周期函数--监听页面初次渲染完成,一个页面只调用1次
       */
      onReady() {
        // 动态设置页面标题
        wx.setNavigationBarTitle({
          title: this.data.query.title
        })
      },
    
      /**
       * 生命周期函数--监听页面显示
       */
      onShow() {},
    
      /**
       * 生命周期函数--监听页面隐藏
       */
      onHide() {},
    
      /**
       * 生命周期函数--监听页面卸载,一个页面只调用1次
       */
      onUnload() {}
    
    })

WXS 脚本

WXS(WeiXinScript)是小程序独有的一套脚本语言,结合WXML,可以构建出页面的结构。

wxml 中无法调用在页面的 .js 中定义的函数,但是,wxml 中可以调用 wxs 中定义的函数。因此,小程序中 WXS 的典型应用场景就是 “过滤器”。

WXS 的代码可以编写在 wxml 文件中 wxs 标签内,就像 JavaScript 可以编写在 html 中 script 标签内一样。

wxml 文件中的每个 <Wxs></wxs> 标签,必须提供 module 属性,用来指定当前 wxs 的模块名称,方便在 wxml 中访问模块中的成员:

<view>{{ m1.toUpper(info) }}</view>

<!-- 利用module.exports对外暴露该模块 -->
<wxs module="m1">
  module.exports.toUpper = function(str) {
    return str.toUpperCase()
  }
</wxs>

wxs 代码还可以编写在以 wxs 为后缀名的文件内,就像 JavaScript 代码可以编写在以 .js 为后缀名的文件中一样。.wxs 文件一般存放在 utils 文件夹中。

// tools.wxs文件
// 定义方法
function toLower(str) {
  return str.toLowerCase()
}

// 对外暴露
module.exports = {
  toLower: toLower
}

而在 wxml 中引入外联的 wxs 脚本时,必须为 wxs 标签添加 module 和 src 属性,其中 module 用来指定模块的名称,src 用来指定要引入的脚本路径,且必须是相对路径。

// 定义方法
function toLower(str) {
  return str.toLowerCase()
}

// 对外暴露
module.exports = {
  toLower: toLower
}
<view>{{ m2.toLower('HELLO') }}</view>

<wxs src="../../utils/tools.wxs" module="m2"></wxs>

注意,wxs 中定义的函数不能作为事件回到函数来使用!!

<!-- 以下代码是错误的 -->
<button bindtap="m2.toLower">按钮</button>

并且,wxs 还具有一定的隔离性,主要体现在 wxs 不能调用 js 中的函数,且 wxs 不能调用小程序提供的 API。最后,wxs 还具有性能良好的特点,在 IOS 设备上,小程序内的 wxs 的运行效率要比 JavaScript 快 2 ~ 20 倍,而在 Android 设备上,二者的运行效率并无差异。

自定义组件

组件的创建与页面的创建类似,也是需要通过不同的文件夹来进行区分。与创建页面不同的是,组件的创建需要在对应的文件夹右键后选择 “新建 Components” 进行创建。

从表面来看,组件和页面都是由 js、json、.wxml 和.wxss 这四个文件组成的。但是,组件和页面的 js 与.json 文件有明显的不同:

  • 组件的 .json 文件中需要声明 "component":true 属性。
  • 组件的 .js 文件中调用的是 Component() 函数。
  • 组件的事件处理函数需要定义到 methods 节点中。

引用组件

在页面的 .json 配置文件中引用组件的方式,叫做 “局部引用”,在全局 app.json 配置文件中引用组件,叫做 “全局引用”,示例代码如下:

// .json文件中使用组件
{
  "usingComponents": {
    "my-test": "/components/test/test"
  }
}
<!-- 使用组件 -->
<my-test></my-test>

组件样式

默认情况下,自定义组件的样式只对当前组件生效,不会影响到组件之外的 UI 结构,这称为组件样式隔离。组件样式隔离的注意点:

  • app.wxss 中的全局样式对组件无效。
  • 只有 class 选择器会有样式隔离效果,id 选择器、属性选择器、标签选择器不受样式隔离的影响。

建议在组件和引用组件的页面中使用 class 选择器,不要使用 id、属性、标签选择器。

默认情况下,自定义组件的样式隔离特性能够防止组件内外样式互相干扰的问题。但有时,我们希望在外界能够控制组件内部的样式,此时,可以通过 styleIsolation 修改组件的样式隔离选项,有两种用法:

  1. 在组件的 .js 文件中新增如下配置:

    Component({
        options: {
            styleIsolation: 'isolated'
        }
    })
  2. 或者在组件的 .json 文件中新增如下配置:

    {
        "styleIsolation": "isolated"
    }

其中,styleIsolation 的可选值有三个:

可选值 默认值 描述
isolated 是 启用样式隔离
apply-shared 否 页面 wxss 样式会影响到组件,但是组件的 wxss 不会影响页面
shared 否 页面和组件的 wxss 相互影响

数据、方法和属性

  1. 在小程序组件中,用于组件模板渲染的私有数据,需要定义到 data 节点中,示例:
Component({
  // 组件的初始数据
  data: {
    count: 0
  }
})
  1. 在小程序组件中,事件处理函数和自定义方法需要定义到 methods 节点中,示例:
<!-- wxml -->
<view>count:{{ count }}</view>
<button bind:tap="addCount">+1</button>
/**
* 组件的方法列表
*/
methods: {
  // 事件处理函数
  addCount() {
    this.setData({
      count: this.data.count + 1
    })
    this._showCount()
  },
  // 自定义函数,一般以_开头
  _showCount() {
    wx.showToast({
      title: 'count: ' + this.data.count,
      icon: 'none'
    })
  }
}
  1. 在小程序组件中,properties 是组件的对外属性,用来接收外界传递到组件中的数据,示例:
/**
* 组件的属性列表
*/
properties: {
  // 完整定义属性的方式,当需要指定属性默认值时,使用此方式
  max: {
    type: Number,
    value: 10 // 属性默认值
  },
  // 简化定义属性的方式
  max: Number
}
<my-test max="10"></my-test>

在小程序中,data 和 properties 的用法相同,它们都是可读可写的,只不过 data 更倾向于存储组件的私有数据,properties 更倾向于存储外界传递到组件中的数据。

由于 data 数据和 properties 属性在本质上没有任何区别,因此 properties 属性的值也可以用于页面渲染,或使用 setData 为 properties 中的属性重新赋值。

数据监听器

数据监听器用于监听和响应任何属性和数据字段的变化,从而执行特定的操作。它的作用类似于 vue 中的 watch 侦听器。我们可以使用数据监听器来实现数据的动态变化:

<view>{{ n1 }} + {{ n2 }} = {{ sum }}</view>
<button bind:tap="addN1">n1 + 1</button>
<button bind:tap="addN2">n2 + 1</button>
Component({

  /**
   * 组件的属性列表
   */
  properties: {},

  /**
   * 组件的初始数据
   */
  data: {
    n1: 0,
    n2: 0,
    sum: 0
  },

  /**
   * 组件的方法列表
   */
  methods: {
    addN1() {
      this.setData({
        n1: this.data.n1 + 1
      })
    },
    addN2() {
      this.setData({
        n2: this.data.n2 + 1
      })
    }
  },
  
  /**
   * 数据监听器
   */
  observers: {
    // 左边的是监听的字段,右边函数参数中是字段的新值
    'n1, n2': function(newn1, newn2) {
      this.setData({
        sum: newn1 + newn2
      })
    }
  }
})

数据监听器也支持监听对象中单个或者多个属性的变化,示例如下:

observers: {
  '对象.属性A, 对象.属性B': function(属性A新值, 属性B新值) {
    // do something
  },
  
  // 使用通配符监听对象所有属性
  '对象.**': function(新对象) {
      // dosomething
  }
}

纯数据字段

纯数据字段指的是那些不用于页面渲染的 data 字段。

有些情况下,某些 data 中的字段既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。带有这种特性的 data 字段适合被设置为纯数据字段。

使用纯数据字段有利于提升页面的性能。

在 Component 构造器的 options 节点中,指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段:

Component({
  options: {
    pureDataPattern: /^_/
  },
  data: {
    _a: 0,	// 纯数据字段
    b: 0	// 非纯数据字段
  }
})

自定义组件的生命周期

小程序组件中可用的全部生命周期函数如下表所示:

生命周期函数 参数 描述说明
created 无 在组件实例刚刚被创建时执行
attached 无 在组件实例进入页面结点树时执行
ready 无 在组件在视图层布局完成后执行
moved 无 在组件实例被移动到节点树另一个位置时执行
detached 无 在组件实例被从页面节点树移除时执行
error Object Error 每当组件方法抛出错误时执行

在小程序组件中,最重要的生命周期函数有 3 个,分别是 created、attached、detached。各自的特点如下:

  1. 组件实例刚被创建好的时候,created 生命周期函数会被触发:
    • 此时还不能调用 setData。
    • 通常在这个生命周期函数中,只应该用于给组件的 this 添加一些自定义的属性字段。
  2. 在组件完全初始化完毕、进入页面节点树后,attached 生命周期函数会被触发:
    • 此时,this.data 已被初始化完毕。
    • 这个生命周期很有用,绝大多数初始化的工作可以在这个时机进行(例如发请求获取初始数据)。
  3. 在组件离开页面节点树后,detached 生命周期函数会被触发:
    • 退出一个页面时,会触发页面内每个自定义组件的 detached 生命周期函数。
    • 此时适合做一些清理性质的工作。

在小程序组件中,生命周期函数可以直接定义在 Component 构造器的第一级参数中,可以在 lifetimes 字段内进行声明,示例代码如下:

Component({
  lifetimes: {
    created() {
      console.log('created')
    },

    attached() {
      console.log('attached')
    }
  }
})

组件所在页面的生命周期

有时,自定义组件的行为依赖于页面状态的变化,此时就需要用到组件所在页面的生命周期。

在自定义组件中,组件可以交互的所在页面的生命周期函数有如下 3 个:

生命周期函数 参数 描述
show 无 组件所在页面被展示时执行
hide 无 组件所在页面被隐藏时执行
resize Object Size 组件所在页面尺寸变化时执行

组件所在页面的生命周期函数,需要定义在 pageLifetimes 节点中,示例代码如下:

Component({
  pageLifetimes: {
   show() {
      console.log('show')
    },

    hide() {
      console.log('hide')
    },

    resize() {
      console.log('resize')
    } 
  }
})

插槽

单个插槽

在自定义组件的 wxml 结构中,可以提供一个 <slot> 节点(插槽),用于承载组件使用者提供的 wxml 结构。简单来说,插槽就是对组件中暂时无法确定的元素进行占位,在之后的使用过程中,可以随时用别的元素替代这个插槽。

<!-- 组件的封装者 -->
<view>
  <view>这个是组件的内部节点</view>
  <!-- 对于不确定的内容,使用slot进行占位 -->
  <solt></solt>
</view>

<!-- 组件的使用者 -->
<component>
  <!-- 以下内容将被放置在组件的slot位置上 -->
  <view>这里是插入到组件slot位置上的内容</view>
</component>

多个插槽

在小程序的自定义组件中,需要使用多个 <slot> 插槽时,可以在组件的 .js 文件中,先通过如下方式进行启用。示例代码如下:

Component({
  options: {
    multipleSlots: true
  }
})

接下来就是在组件中使用多个插槽,不过对于这些插槽,我们需要用不同的 name 进行区分:

<!-- 组件的封装者 -->
<view>
  <slot name="before"></slot>
  <view>这是一段固定的文本内容</view>
  <slot name="after"></slot>
</view>

<!-- 组件的使用者 -->
<component>
  <!-- 以下内容将被放置在组件的slot位置上 -->
  <view slot="before">这里是插入到组件before-slot位置上的内容</view>
  <view slot="after">这里是插入到组件after-slot位置上的内容</view
</component>

组件通信

父子组件之间通信有 3 种方式:

  1. 属性绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容的数据。
  2. 事件绑定:用于子组件向父组件传递数据,可以传递任意数据。
  3. 获取组件实例:父组件还可以通过 this.selectComponent() 获取子组件实例对象,这样就可以直接访问子组件的任意数据和方法。

属性绑定

父组件的 data 节点:

data: {
 count: 0   
}

父组件的 wxml 结构:

<!-- 通过属性向子组件传递数据 -->
<son count="{{ count }}"></son>

子组件的 properties 节点:

properties: {
  count: Number
}

子组件使用父组件传递过来的数据:

<view>{{ count }}</view>

事件绑定

事件绑定用于实现子向父传值,可以传递任何类型的数据。使用步骤如下:

  1. 父组件的 .js 中,定义一个函数,这个函数即将通过自定义事件的形式,传递给子组件。

    // 在父组件中定义syncCount方法
    // 将来,这个方法会被传递给子组件,供子组件进行调用
    syncCount() {
        console.log('syncCount')
    }
  2. 在父组件的 wxml 中,通过自定义事件的形式,将步骤 1 中定义的函数引用,传递给子组件。

    <son bind:sync="syncCount"></son>
  3. 在子组件的 .js 中,通过调用 this.triggerEvent('自定义事件名称',{参数对象}),将数据发送到父组件。

    methods: {
      addCount() {
          this.setData({
              count: this.properties.count + 1
    	  })
          this.triggerEvent('sync', {value: this.properties.count})
      }
    }
  4. 在父组件的 .js 中,通过 e.detail 获取到子组件传递过来的数据。

    syncCount(e) {
      // console.log(e.detail.value)
      this.setData() {
        count: e.detail.value
      }
    }

获取组件实例

<son class="customA" id="cA"></son>
<button bindtap="getChild">获取子组件实例</button>
getChild() {
  const child = this.selectComponent('.customA')	// 也可传递id选择器 #cA
  child.setData({ count: child.properties.count + 1 })
  child.addCount()	// 调用子组件的方法
}

behaviors

behaviors 是小程序中,用于实现组件间代码共享的特性,类似于 Vue 中的 “mixins”。

每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中。每个组件可以引用多个 behavior,behavior 也可以引用其它 behavior。

调用 Behavior(Object object) 方法即可创建一个共享的 behavior 实例对象,供所有的组件使用:

module.exports = Behavior({
  // 属性节点
  properties: {},
  // 私有数据节点
  data: { username: 'zs' },
  // 事件处理函数和自定义方法节点
  methods: {}
  // 其他节点...
})

在组件中,使用 require() 方法导入需要的 behavior,挂载后即可访问 behavior 中的数据或方法,示例:

// 使用require导入需要的自定义behavior模块
const myBehavior = require('../../behaviors/my-behavior')

Component({
  behaviors: [myBehavior],
  //..其他节点
})

behavior 中所有可用的节点:

可用的节点 类型 是否必填 描述
properties Object Map 否 同组件的属性
data Object 否 同组件的数据
methods Object 否 同自定义组件的方法
behaviors String Array 否 引入其他的 behavior
created Function 否 生命周期函数
attached Function 否 生命周期函数
ready Function 否 生命周期函数
moved Function 否 生命周期函数
detached Function 否 生命周期函数

Vant Weapp

使用 npm 包

目前,小程序中已经支持使用 npm 安装第三方包,从而来提高小程序的开发效率。但是,在小程序中使用 npm 包有如下 3 个限制:

  1. 不支持依赖于 Node.js 内置库的包。
  2. 不支持依赖于浏览器内置对象的包(类似 jquery)。
  3. 不支持依赖于 C++ 插件的包。

Vant Weapp

Vant Weapp 是有赞前端团队开源的一套小程序 UI 组件库,助力开发者快速搭建小程序应用。它所使用的是 MIT 开源许可协议,对商业使用比较友好。官方文档地址戳我进入。

在小程序项目中,安装 Vant 组件库主要分为如下 3 步:

  1. 通过 npm 安装。

    npm init -y
    npm i @vant/weapp@1.11.6 -S --production
  2. 构建 npm 包。

    打开微信开发者工具,点击 “工具”、“构建 npm”,并勾选 “使用 npm 模块”选项(新版本可以不用手动勾选),构建完成后,即可引入组件。

  3. 修改 app.json。

    将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。

API Promise 化

默认情况下,小程序官方提供的异步 API 都是基于回调函数实现的。例如,网络请求的 API 需要按照如下方式调用:

wx.request({
  method: '',
  url: '',
  data: {},
  success: () => {},	// 请求成功的回调函数
  fail: () => {},		// 请求失败的回调函数
  complete: () => {}	// 请求完成的回调函数
})

过多的回调函数可能会造成回调地狱问题,代码的可读性和可维护性较差。

在小程序中,实现 APl Promise 化主要依赖于 miniprogram-api-promise 这个第三方的 npm 包。它的安装和使用步骤如下:

npm install --save miniprogram-api-promise@1.0.4

上述命令执行完后,记得构建 npm。(为了保证构建的时候不出错,建议每次构建之前都删除 miniprogram_npm 这个文件夹)

实现 API Promise 化

// 在小程序入口文件app.js中,调用一次promisifyAll方法即可实现异步API的promise化
import { promisifyAll } from 'miniprogram-api-promise'

const wxp = wx.p = {}
promisifyAll(wx, wxp)

接下来就可以直接用 async 和 await 优化 API 的调用:

async getInfo() {
  const { data: res } = await wx.p.request({
    method: 'GET',
    url: 'https://www.escook.cn/api/get',
    data: { name: 'zhangsan', age: 20 }
  })
  
  console.log(res)
}

全局数据共享

全局数据共享(又叫做:状态管理)是为了解决组件之间数据共享的问题。在小程序中,可使用 mobx-miniprogram 配合 mobx-miniprogram-bindings 实现全局数据共享。其中,第一个包是用来插件 Store 示例对象的,第二个包是用来把 Store 中的共享数据或方法,绑定到组件或页面中使用。

包的使用如下:

npm install --save mobx-miniprogram@4.13.2 mobx-miniprogram-bindings@1.2.1

执行上述命令后记得删除 miniprogram_npm 目录并重新构建 npm。

创建 Store 实例

// store.js 用于创建store实例对象
import { action, observable } from 'mobx-miniprogram'

export const store = observable({
  // 数据字段
  numA: 1,
  numB: 2,
  // 计算属性,依赖numA和numB的变化,只要这两个属性变化,就会触发
  get sum() { // 使用get,表示该值为只读
    return this.numA + this.numB
  },
  // action方法,用于修改数据
  updateNumA: action(function(newNumA) {
    this.numA = newNumA
  }),
  updateNumB: action(function(newNumB) {
    this.numB = newNumB
  })
})

使用 Store 实例

绑定到页面中

// 页面的.js
// 先导入需要的方法和实例
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { numStore } from '../../store/numStore'

Page({
    
  onLoad(options) {
    // 页面加载时绑定store实例
    this.storeBindings = createStoreBindings(this, {
      store: numStore,
      fields: ['numA', 'numB', 'sum'],
      actions: ['updateNumA', 'updateNumB']
    })
  },
    
  onUnload() {
    // 页面卸载时销毁store实例
    this.storeBindings.destroyStoreBindings()
  }
})
<!-- onLoad挂载之后,可以认为store里面的属性就已经导入到了data节点中了 -->
<veiw>{{ numA }} + {{ numB }} = {{ sum }}</veiw>
// js中需要注意设置两次数据
onchange() {
    let newNumA = this.data.numA;
    newNumA = newNumA + 2;
    
    this.setData({
      numA: newNumA
    })
    
    this.updateA(this.data.numA);
}

绑定到组件中

import { storeBindingBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'

Component({
  behaviors: [storeBindingBehavior],	// 通过storeBindingBehavior来实现自动绑定
  
  storeBindings: {
    store,	// 指定要绑定的store
    fields: {
      numA: () => store.numA,	// 绑定字段的第一种方式  
      numB: (store) => store.numB, // 绑定字段的第二种方式
      sum: 'sum'
    },
    actions: {	// 指定要绑定的方法
      updateNum2: 'updateNum2'
    }
  }
})

框架接口 getApp

在小程序中,可以通过 getApp 方法获取到小程序全局唯一的 App 实例。因此在 App() 方法中添加全局共享的数据、方法,从而实现页面、组件的数据传值。

app.js 文件:

App({
  // 全局共享的数据
  globalData: {
    token: ''
  },

  // 全局共享的方法
  setToken(token) {
    // 如果要获取app实例,可以直接使用this的方式进行获取
    this.globalData.token = token
  }
})

其他页面的 .js 文件:

// getApp获取全局唯一app实例
const appInstance = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {

  },

  login() {
    // 调用全局函数和全局数据
    appInstance.setToken('token')
    console.log(appInstance.globalData.token)
  }
})

注意:

  1. 不要在 App 方法中使用 getApp,使用 this 就可以直接拿到 app 实例。
  2. 通过 getApp 获取实例之后,不要私自调用生命周期函数。

分包

分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。对小程序进行分包的好处:

  • 可以优化小程序首次启动的下载时间。
  • 在多团队共同开发的时候可以更好的解耦协作。

分包前,小程序项目中所有的页面和资源都被打包到了一起,导致整个项目体积过大,影响小程序首次启动的下载时间。

分包后,小程序项目由 1 个主包 + 多个分包组成:

  • 主包:一般只包含项目的启动页面或 TabBar 页面、以及所有分包都需要用到的一些公共资源。
  • 分包:只包含和当前分包有关的页面和私有资源。

在小程序启动时,默认会下载主包并启动主包内页面(tabBar 页面需要放到主包中),当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示(非 tabBar 页面可以按照功能的不同,划分为不同的分包之后,进行按需下载)。

目前,小程序分包的大小有以下两个限制:

  1. 整个小程序所有分包大小不超过 16M(主包 + 所有分包)。
  2. 单个分包 / 主包大小不能超过 2M。

使用分包

小程序的目录结构:

|------app.js
|------app.json
|------app.wxss
|------pages		// 主包的所有页面
|    |------index
|    |------logs
|------packageA		// 第一个分包
|    |----pages		// 第一个分包的所有页面
|        |----cat
|        |----dog
|------packageB		// 第二个分包
|    |----pages		// 第二个分包的所有页面
|        |----apple
|        |----banana
|------utils

小程序的 app.json 文件:

{
  "pages": [	// 主包的所有页面
      "pages/index/index",
      "pages/logs/logs"
  ],
  "subPackages": [	// 通过subpackages节点声明分包结构
      {
          "root": "packageA",	// 第一个分包的根目录
          "pages": [
              "pages/cat/cat",
              "pages/dog/dog"
          ]
      },
      {
          "root": "packageB",	// 第二个分包的根目录
          "name": "pkgB",	    // 分包起别名
          "pages": [
              "pages/apple/apple",
              "pages/banana/banana"
          ]
      }
   ]
}

小程序的打包原则是:

  1. 小程序会按照 subPackage 的配置进行分包,subPackages 之外的目录被打包到主包中。
  2. 主包也可以有自己的 pages(即最外层的 pages 字段)。
  3. tabBar 页面必须在主包内。
  4. 分包之间不能相互嵌套。

分包的引用原则是:

  1. 主包无法引用分包的私有资源。
  2. 分包之间不能相互引用私有资源。
  3. 分包可以引用主包内的公共资源。

独立分包

独立分包本质上也是分包,只不过它比较特殊,可以独立于主包和其他分包而单独运行。一般情况下,用户通过访问主包才能启动小程序,而使用独立分包就可以让用户通过这个分包来启动小程序。(一个小程序中可以有多个独立分包)

实际上,普通分包和独立分包的区别便是是否依赖于主包才能运行:普通分包必须依赖于主包才能运行,而独立分包可以在不下载主包的情况下独立运行。

开发者可以按需,将某些具有一定功能独立性的页面配置到独立分包中。原因如下:

  • 当小程序从普通的分包页面启动时,需要首先下载主包。
  • 而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度。

小程序目录结构:

|------app.js
|------app.json
|------app.wxss
|------pages		// 主包的所有页面
|    |------index
|    |------logs
|------moduleA		// 普通分包
|    |----pages		
|        |----rabbit
|        |----squirrel
|------moduleB		// 独立分包
|    |----pages		
|        |----pear
|        |----pineapple
|------utils

小程序的 app.json 文件:

{
  "pages": [	// 主包的所有页面
      "pages/index/index",
      "pages/logs/logs"
  ],
  "subPackages": [	// 通过subpackages节点声明分包结构
      {
          "root": "moduleA",	// 第一个分包的根目录
          "pages": [
              "pages/rabbit/rabbit",
              "pages/squirrel/squirrel"
          ]
      },
      {
          "root": "moduleB",	// 第二个分包的根目录
          "name": "pkgB",	    // 分包起别名
          "pages": [
              "pages/pear/pear",
              "pages/pineapple/pineapple"
          ],
          "independent": true	// 通过此节点声明当前分包为独立分包
      }
   ]
}

独立分包和普通分包以及主包之间,是相互隔绝的,不能相互引用彼此的资源!例如:

  1. 主包无法引用独立分包内的私有资源。
  2. 独立分包之间,不能相互引用私有资源。
  3. 独立分包和普通分包之间,不能相互引用私有资源。
  4. 特别注意:独立分包中不能引用主包内的公共资源。

分包预下载

分包预下载指的是:在进入小程序的某个页面时,由框架自动预下载可能需要的分包,从而提升进入后续分包页面时的启动速度。同一个分包中的页面享有共同的预下载大小限额 2M。

预下载分包的行为,会在进入指定的页面时触发。在 app.json 中,使用 preloadRule 节点定义分包的预下载规则,示例代码如下:

{
  "preloadRule": {	// 分包预下载的规则
    "pages/contact/contact": {	// 触发分包预下载的页面路径
      "network": "all",	// 可选项有all和wifi,意味着在不限网络和wifi模式下进行预下载
      "package": ["pkgA"]	// 表示进入页面后预下载哪些分包
    }
  }
}

开放能力

获取微信头像

<!-- 通过open-type绑定获取微信头像的api -->
<view>
  <button open-type="chooseAvatar" bindchooseavatar="chooseAvatar">
    获取头像
  </button>
  <image src="{{ avatarUrl }}"></image>
</view>
data: {
  avatarUrl: ''
},

// 获取微信头像
chooseAvatar(event) {
  // console.log(event)
  const { avatarUrl } = event.detail
  this.setData({
    avatarUrl: avatarUrl
  })
}

获取微信昵称

<form bindsubmit="onSubmit">
  <!-- input的type改成nickname,可以获取昵称 -->
  <input type="nickname" name="nickname" placeholder="请输入昵称"/>
  <!-- submit按钮在点击的时候会触发表单的提交事件 -->
  <button type="primary" form-type="submit">点击获取昵称</button>
</form>
// 获取微信昵称
onSubmit(event) {
  // console.log(event)
  const { nickname } = event.detail.value
  this.setData({
    nickname: nickname
  })
}

转发功能

转发一共有两种方式:

  1. 页面 .js 文件声明 onShareAppMessage 事件监听函数,并自定义转发内容。只有定义了此事件处理函数,右上角菜单栏才会显示 “转发” 按钮。

    /**
    * 用户点击右上角分享
    */
    onShareAppMessage() {
      return {
        title: '自定义转发标题',
        path: '/pages/share/share',
        imageUrl: '自定义转发图标路径'
      }
    }
  2. 通过给按钮组件设置属性 open-type="share",可以在用户点击按钮后触发 Page.onShareAppMessage 事件监听函数。

    <view>
      <button open-type="share">转发</button>
    </view>

分享到朋友圈

小程序页面默认不能被分享到朋友圈,开发者需主动设置 “分享到朋友圈” 才可以,实现分享到朋友圈需满足两个条件:

  1. 页面必须设置允许 “发送给朋友”,即页面 .js 文件必须声明 onShareAppMessage 事件监听函数。
  2. 页面必须设置允许 “分享到朋友圈”,即页面 .js 文件必须声明 onShareTimeline 事件监听函数。
/**
* 监听右上角分享到朋友圈按钮
*/
onShareTimeline() {
  return {
    title: '自定义分享标题',
    query: 'id=1',
    imageUrl: '自定义转发图标路径'
  }
}

手机号验证组件

手机验证组件,用于帮助开发者向用户发起手机号申请,必须经过用户同意后,才能获得由平台验证后的手机号,进而为用户提供相应服务。手机号验证组件分为两种:手机号快速验证组件以及手机号实时验证组件。

  1. 手机号快速验证组件:平台会对号码进行验证,但不保证是实时验证。
  2. 手机号实时验证组件:在每次请求时,平台均会对用户选择的手机号进行实时验证。
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">快速验证</button>
<button open-type="getRealtimePhoneNumber" 
        bindgetrealtimephonenumber="getRealtimePhoneNumber">实时验证</button>
// 快速验证
getPhoneNumber(event) {
  /* 
    可以在event.detail中获取code 
    code是动态令牌,可以使用code换取用户手机号
    需要将code发送给后端,后端在接收到code以后也需要调用API,换取用户真正的手机号
    在换取成功后,后端将手机号返回给前端
  */ 
},

// 实时验证
getRealtimePhoneNumber(event) {

}

客服功能

小程序为开发者提供了客服能力,同时为客服人员提供移动端、网页端客服工作台便于及时处理消息。使用方式:

  1. 需要将 button 组件 open-type 的值设置为 contact,当用户点击后就会进入客服会话。
  2. 在微信公众后台,绑定后的客服账号,可以登陆网页端客服或移动端小程序客服接收、发送客服消息。

项目开发

通用模块封装

在项目开发的时候,我们会经常使用一些 API(例如消息提示),如果每次使用的时候都要专门调用,这会导致代码冗余,我们可以对这些常用 API 进行模块封装。

在 utils 文件夹下新建一个新的文件 extendApi.js,以后的新增 api 就写在这个文件中。

消息提示模块封装

消息提示模块如下:

wx.showToast({
  title: '消息提示框',    // 提示的内容
  icon: 'success',       // 提示的图标
  duration: 2000,        // 提示的延迟时间
  mask: true             // 是否显示透明蒙层,防止触摸穿透
})

模块封装如下:

// 在使用toast方法时,可以传入参数,也可以不传入参数
// 如果要传入参数,需要传入对象才可以
// 使用解构表达式可以给参数赋初值
// const toast = (options = {}) => {}

export const toast = ({title =  '数据加载中...', 
                icon = 'none', 
                duration = 2000, 
                mask = true} = {}) => {
  wx.showToast({
    title,
    icon,
    duration,
    mask
  })
}

其他文件想要使用 toast 方法就需要先导入 toast:

import { toast } from './utils/extendApi'

也可以把 toast 方法挂载到 wx 身上(extendApi.js 文件作以下修改):

wx.toast = toast

之后如果要使用 toast 直接调用 wx.toast 即可。

模态对话框模块封装

模态对话框模块如下:

wx.showModal({
  title: '提示',					// 提示的标题
  content: '这是一个模态弹窗',		// 提示的内容
  confirmColor: '#f3514f',		   // 确认键的颜色  
  success (res) {				  // 接口调用后的回调函数
    if (res.confirm) {
      console.log('用户点击确定')
    } else if (res.cancel) {
      console.log('用户点击取消')
    }
  }
})

封装如下:

export const modal = (options = {}) => {
  return new Promise((resolve) => {
    // 默认的参数
    const defaultOpt = {
      title: '提示',
      content: '您确定执行该操作吗',
      confirmColor: '#f3514f'
    }

    // 通过assign把默认参数和用户传递过来的参数进行合并
    const opts = Object.assign({}, defaultOpt, options)

    wx.showModal({
      // 通过展开运算符赋值
      ...opts,
      complete ({ confirm, cancel }) {
        confirm && resolve(true)
        cancel && resolve(false)
      }
    })
  })
}

本地存储模块封装

在小程序中,经常需要将一些数据存储到本地,方便多个页面的读取使用,例如:将用户的登录状态、用户的个人信息存储到本地。小程序提供了同步、异步两类 API 来实现本地存储操作:wx.setStorageSync、wx.setStorage 等方法。

在 utils 文件夹中新增 storage.js 文件:

/**
 * 存储数据
 * @param {*} key 本地缓存中指定的key 
 * @param {*} value  需要缓存的数据
 */
export const setStorage = (key, value) => {
  try {
    wx.setStorageSync(key, value)
  } catch (error) {
    console.error(`存储指定${key}数据发生了异常`, error)
  }
}

/**
 * 从本地读取指定key的数据
 * @param {*} key 指定的key
 */
export const getStorage = (key) => {
  try {
    const value = wx.getStorageSync(key)
    // 读取成功返回value
    if (value) {
      return value
    }
  } catch (error) {
    console.error(`读取指定${key}数据发生了异常`, error)
  }
}

/**
 * 从本地移除指定key数据
 * @param {*} key 指定的key
 */
export const removeStorage = (key) => {
  try {
    wx.removeStorageSync(key)
  } catch (error) {
    console.error(`移除${key}数据发生异常`, error)
  }
}

/**
 * 清空本地数据
 */
export const clearStorage = () => {
  try {
    wx.clearStorageSync()
  } catch (error) {
    console.error(`清空数据时发生了异常`, error)
  }
}

封装异步的请求:

/**
 * 异步将数据存储到本地
 * @param {*} key 本地缓存中指定的key
 * @param {*} data 需要缓存的数据
 */
export const asyncSetStorage = (key, data) => {
  return new Promise((resolve) => {
    wx.setStorage({
      key,
      data,
      complete (res) {
        resolve(res)
      }
    })
  })
}

/**
 * 异步从本地获取指定key的数据
 * @param {*} key 指定的key
 */
export const asyncGetStorage = (key) => {
  return new Promise((resolve) => {
    wx.getStorage({
      key,
      complete (res) {
        resolve(res)
      }
    })
  })
}

/**
 * 异步从本地获取移除key的数据
 * @param {*} key 指定的key
 */
export const asyncRemoveStorage = (key) => {
  return new Promise((resolve) => {
    wx.removeStorage({
      key,
      complete (res) {
        resolve(res)
      }
    })
  })
}

/**
 * 异步从本地清空全部缓存数据
 */
export const asyncClearStorage = () => {
  return new Promise((resolve) => {
    wx.clearStorage({
      complete(res) {
        resolve(res)
      }
    })
  })
}

网络请求模块封装

可以直接使用 npm 包使用封装好的微信小程序网络请求模块(官方地址):

npm install mina-request

安装完别忘记构建噢。

完成上述步骤之后在 utils 目录下新建 http.js 文件:

// 导入模块
import WxRequest from 'mina-request'

// 对类进行实例化
const instance = new WxRequest({
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
  timeout: 15000
})

// 添加请求拦截器
instance.interceptors.request = (config) => {
  const token = getStorage('token')
  // 请求头添加token
  if (token) {
    config.header['token'] = token
  }
  return config
}

// 添加响应拦截器
instance.interceptors.response = (response) => {

  // response.isSuccess = true,代码执行了 wx.request 的 success 回调函数
  // response.isSuccess = false,代码执行了 wx.request 的 fail 回调函数
    
  // response.statusCode // http 响应状态码
    
  // response.config // 网络请求请求参数
    
  // response.data 服务器响应的真正数据
    
  // 对响应数据做点什么
  return response
}

// 导出实例
export { instance }

环境变量

在实际开发中,不同的开发环境,调用的接口地址是不一样的。例如:开发环境需要调用开发版的接口地址,生产环境需要调用正式版的接口地址。

这个时候,我们可以使用小程序提供的 wx.getAccountInfoSync() 接口,用来获取当前账号信息,在账号信息中包含着小程序当前环境版本。

环境版本 合法值
开发版 develop
体验版 trial
正式版 release
// 获取当前小程序的账号信息
const accountInfo = wx.getAccountInfoSync()
// 获取小程序版本
console.log(accountInfo.miniProgram.envVersion)

在 utils 目录中新建文件 env.js:

// 解构出当前小程序账号信息
const { miniProgram } = wx.getAccountInfoSync()

// 获取小程序版本
const { envVersion } = miniProgram

// 根据不同版本调用不同接口地址
let env = {
  baseURL: 'https://gmall-prod.atguigu.cn/mall-api'
}

switch (envVersion) {
  case 'develop': // 开发版
    env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
    break

  case 'trial': // 体验版
    env.baseURL = '体验版url'
    break

  case 'release':  // 正式版
    env.baseURL = '正式版url'
    break

  default:
    env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
    break
}

export { env }

http.js 文件作以下更改:

import { env } from './env'

// 对类进行实例化
const instance = new WxRequest({
  // baseURL: 'https://gmall-prod.atguigu.cn/mall-api',
  baseURL: env.baseURL,
  timeout: 15000
})

接口调用方式说明

在开发中,我们会将所有的网络请求方法放置在 api 目录下统一管理,然后按照模块功能来划分成对应的文件,一个功能模块就是一个文件,在文件中将接口封装成一个个方法单独导出。

import { instance } from '../utils/http'

export const reqSwiperData = () => {
  return instance.get('/index/findBanner')
}
<button bind:tap="btnHandler">点我获取轮播图数据</button>
async btnHandler() {
  const res = await reqSwiperData()
  console.log(res)
}

首页请求数据

小程序的首页一般会需要请求多种数据(轮播图、商品列表等),我们可以使用并发请求来提高数据的获取速度:

api 文件:

import { instance } from '../utils/http'

export const reqIndexData = () => {
  // 通过并发请求获取首页数据,提升页面的渲染速度
  return instance.all(
    instance.get('/index/findBanner'),
    instance.get('/index/findCategory1'),
    instance.get('/index/advertisement'),
    instance.get('/index/findListGoods'),
    instance.get('/index/findRecommendGoods')
  )
}

页面 .js 文件:

import { reqIndexData } from '../../api/index'
Page({
  data: {
    bannerList: [],   // 轮播图数据
    categoryList: [],  // 商品导航数据
    activeList: [],   // 活动宣传区
    hotList: [],    // 人气推荐
    guessList: []   // 猜你喜欢
  },

  // 获取首页数据
  async getIndexData() {
    // 调用接口获取数据
    const res = await reqIndexData()

    // 数据赋值
    this.setData({
      bannerList: res[0].data,
      categoryList: res[1].data,
      activeList: res[2].data,
      hotList: res[3].data,
      guessList: res[4].data
    })
  },

  onLoad() {
    this.getIndexData()
  }
})

首页骨架屏组件

骨架屏的设计旨在优化用户体验。骨架屏是页面的一个空白版本,开发者会使用 css 绘制一些灰色的区块,将页面内容大致勾勒出轮廓。通常会在页面完全渲染之前,将骨架屏代码进行展示,待数据加载完成后,再替换成真实的内容。

在进行项目开发时,我们需要手工维护骨架屏的代码,当业务变更时,同样需要对骨架屏代码进行调整。为了方便开发者进行骨架屏的绘制,开发者工具提供了自动生成骨架屏代码的能力。

在模拟器右下角三点处点击 “生成骨架屏”,即可生成骨架屏代码,然后在 index 目录下新建一个目录 skeleton 专门存储骨架屏代码,根据生成的代码注释可以使用骨架屏代码。

引入代码后还需要更改 index.js 文件:

Page({
  data: {
    //...各种list
    loading: true   // 是否显示骨架屏,默认显示
  },

  // 获取首页数据
  async getIndexData() {
    // 调用接口获取数据
    const res = await reqIndexData()

    // 数据赋值
    this.setData({
      //...各种list赋值
      loading: false	// 赋值后骨架屏不显示
    })
  },

  onLoad() {
    this.getIndexData()
  }
})

用户登录

Token

Token 是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 Token 便将此 Token 返回给客户端,客户端在接收到 Token 以后,会使用某种方式将 Token 保存到本地。以后客户端发起请求,只需要在请求头上带上这个 Token,服务器通过验证 Token 来确认用户的身份,而无需再次带上用户名和密码。

Token 使用的具体流程:

  1. 客户端向服务器发起登录请求,服务端验证用户名与密码。
  2. 验证成功后,服务端会签发一个 Token,并将 Token 发送到客户端。
  3. 客户端收到 token 以后,将其存储起来,比如放在 localStorage、sessionStorage 中。
  4. 客户端每次向服务器请求资源的时候需要带着服务端签发的 Token,服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。

小程序登录

传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。

小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。免去了注册和输入账号密码的步骤,从而提高了用户体验。

小程序登录流程如下:

api 目录中新建 user.js 文件:

import { instance } from '../utils/http'

// 进行登录操作
export const reqLogin = (code) => {
  return instance.get(`/weixin/wxLogin/${code}`)
}

登录页面 .js 文件:

// 授权登录
login() {
  // 获取临时登录凭证
  wx.login({
    // 利用解构表达式解构出res.code
    success: async ({ code }) => {
      if (code) {
        // 将code发送给开发者服务器
        const res = await reqLogin(code)
        // 将token存储到本地
        const tokenData = res.data.data.token
        setStorage('token', tokenData) 
      } else {
        toast({ title: '授权失败,请重试' })
      }
    }
  })
}

token 存储到 Store

通过上一小节的代码编写以及结合之前的请求拦截器,我们可以顺利将 token 存储到本地中。但是将 token 直接存储到本地不方便对数据进行操作,要先从本地存储取出,然后再使用。最关键的是,存储到本地的数据不是响应式的,当本地存储里面的内容发生改变,页面不会发生改变。这个时候我们需要将 token 也存储到 Store 中。

新建 stores 目录,在其中新建 userStore.js:

import { action, observable } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'

export const userStore = observable({
  // 设置token数据
  token: getStorage('token') || '',

  // 定义action,用来修改更新token值
  setToken: action(function(token) {
    this.token = token
  })

})

在登录页面的 .js 文件中(上一节的 login 方法),别忘了使用 store 并把 token 添加进去。

头像替换

可以用原生的 api 进行资源上传操作:

// 选择微信头像
chooseAvatar(event) {
  const { avatarUrl } = event.detail

  // 把头像上传到服务器
  wx.uploadFile({
    filePath: avatarUrl,   // 要上传的文件资源路径
    name: 'file',
    url: 'https://gmall-prod.atguigu.cn/mall-api/fileUpload',   // 开发者服务器地址
    header: {
      token: getStorage('token')
    },
    success: (res) => {
      // console.log(res)
      // 调用uploadFile返回的是json字符串,需要额外进行转换
      const uploadRes = JSON.parse(res.data)
      this.setData({
        'userInfo.headimgurl': uploadRes.data
      })
    }
  })
}

文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录