微信小程序


微信小程序开发

准备工作

使用浏览器打开本网址,注册小程序开发账号。然后获取一个小程序 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>

hiddenwx: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.setStorageSyncwx.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 许可协议。转载请注明来源 热心市民灰灰 !
  目录