微信小程序开发
准备工作
使用浏览器打开本网址,注册小程序开发账号。然后获取一个小程序 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 文件,分别是:
- app.js:是整个小程序项目的入口文件,通过调用
App()
函数来启动整个小程序。 - 页面的 .js 文件:是页面的入口文件,通过调用
Page()
函数来创建并运行页面。 - 普通的 .js 文件:是普通的功能模块文件,用来封装公共的函数或属性供页面使用。
json 配置文件
小程序项目中一共有 4 种 json 配置文件,分别是:
项目根目录中的 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" }
项目根目录中的 project.config.json 配置文件。
project.config.json 是项目配置文件,用来记录我们对小程序开发工具所做的个性化配置。例如:
- setting 保存了编译相关的配置。
- projectname 保存了项目名称。
- appid 保存的是小程序的账号 ID。(要运行别的小程序需要更改这个 appid 为自己的开发者 id)
项目根目录中的 sitemap.json 配置文件。
微信现已开放小程序内搜索,效果类似于 PC 网页的 SEO。sitemap.json 文件用来配置小程序页面是否允许微信索引。当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索关键字和页面的索引匹配成功的时候,小程序的页面将可能展示在搜索结果中。
每个页面文件夹中的 .json 配置文件。
小程序中的每一个页面,可以使用 json文件来对本页面的窗口外观进行配置,页面中的配置项会覆盖 app.json的 window 中相同的配置项。
WXML
WXML(WeiXin Markup Language)是小程序框架设计的一套标签语言,用来构建小程序页面的结构,其作用类似于网页开发中的 HTML。
WXML 与 HTML 的区别:
- 标签名称不同:
- HTML(div,span,img,a)
- WXML(view,text,image,navigator)
- 属性节点不同:
<a href="#">超链接</a>
<navigator url="/pages/home/home"></navigator>
- 提供了类似于 Vue 中的模板语法。
- 数据绑定。
- 列表渲染。
- 条件渲染。
WXSS
WXSS(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式,类似于网页开发中的 CSS。
WXSS 与 CSS 的区别:
- 新增了 rpx 尺寸单位。
- CSS 中需要手动进行像素单位换算,例如 rem。
- WXSS 在底层支持新的尺寸单位 rpx,在不同大小的屏幕上小程序会自动进行换算。
- 提供了全局的样式和局部样式。
- 项目根目录中的 app.wxss 会作用于所有小程序页面。
- 局部页面的 .wxss 样式仅对当前页面有效。
- WXSS 仅支持部分 CSS 选择器。
小程序的宿主环境
宿主环境(host environment)指的是程序运行所必须的依赖环境。例如:Android 系统和 IOS 系统是两个不同的宿主环境。安卓版的微信 App 是不能在 IOS 环境下运行的。所以,Android 是安卓软件的宿主环境,脱离了宿主环境的软件是没有任何意义的!
而小程序的宿主环境是手机微信。小程序借助宿主环境提供的能力,可以完成许多普通网页无法完成的功能,例如:微信扫码、微信支付、微信登录、地理定位、etc……
微信为小程序提供了以下环境内容支持:
- 通信模型。
- 运行机制。
- 组件。
- API。
通信模型
小程序中的主体是渲染层和逻辑层,其中:WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。小程序中的通信模型分为两部分:
- 渲染层和逻辑层之间的通信:由微信客户端进行转发。
- 逻辑层和第三方服务器之间的通信:由微信客户端进行转发。
运行机制
小程序启动的过程主要分成五个步骤:
- 把小程序的代码包下载到本地。
- 解析 app.json 全局配置文件。
- 执行 app.js 小程序入口文件,调用
App()
创建小程序实例。 - 渲染小程序首页。
- 小程序启动完成。
小程序页面的渲染过程:
- 加载解析页面的 .json 配置文件。
- 加载页面的 .wxml 模板和 .wxss 样式。
- 执行页面的 .js 文件,调用
Page()
创建页面实例。 - 页面渲染完成。
组件
组件内容见下一节。
API
小程序中的 API 是由宿主环境提供的,通过这些丰富的小程序 API,开发者可以方便的调用微信提供的能力,例如:获取用户信息、本地存储、支付功能等。
小程序官方把 API 分为三类:
- 事件监听 API:以 on 开头,用来监听某些事件的触发。
- 同步 API:以 sync 结尾的 API,同步 API 的执行结果,可以通过函数返回值直接获取,如果执行出错会抛出异常。
- 异步 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 | 缩放模式,高度不变,宽度自动变化,保持原图宽高比不变 |
navigator
- 页面导航组件。
- 类似于 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 文件是小程序的全局配置文件。常用的配置项如下:
- pages:记录当前小程序所有页面的存放路径。
- window:全局设置小程序窗口的外观。
- tabBar:设置小程序底部的 tabBar 效果。
- 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 个组成部分:
- backgroundColor:tabBar 的背景色。
- selectedIconPath:选中时图片路径。
- borderStyle:tabBar 上边框的颜色。
- iconPath:未选中时的图片路径。
- selectedColor:tab 上的文字选中时的颜色。
- 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 配置文件” 就可以实现这种需求。
页面配置的配置选项详情请见 “全觉配置” 一节,二者一致。
注意,对于下拉刷新这个选项,一般不要在全局中开启,而是把这一项放在具体的页面中进行单独配置。
网络数据请求
出于安全性方面的考虑,小程序官方对数据接口的请求做出了如下两个限制:
- 只能请求 HTTPS 类型的接口。
- 必须将接口的域名添加到信任列表中。
配置合法的 request 域名需要在小程序开发页面中,左侧一栏选择 “开发” 选项,进入 “开发设置”,在 “服务器域名” 一栏中进行域名的配置。
注意事项:
- 域名只支持 https 协议。
- 域名不能使用 IP 地址或 localhost。
- 域名必须经过 ICP 备案。
- 服务器域名一个月内最多可以申请 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请求”,而是叫做 “发起网络数据请求”。
页面导航
页面导航指的是页面之间的相互跳转,小程序中有两种方式进行页面导航:
- 声明式导航
- 在页面上声明一个 navigator 导航组件。
- 通过点击 navigator 组件实现页面跳转。
- 编程式导航
- 调用小程序的 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('触发上拉触底事件')
}
每一次上拉触底都会触发该方法的执行,如果前一次的事件还没有彻底执行完毕就再一次触发上拉触底,就会导致请求频繁更新,需要进行节流。
节流的步骤如下:
- 在 data 中定义 isloading 节流阀:
- false 表示当前没有任何数据请求。
- true 表示当前正在进行数据请求。
- 在上拉触底方法中修改 isloading 的值:
- 刚调用的时候立刻将节流阀设置为 ture。
- 在网络请求完毕后(complete 函数),将节流阀重置为 false。
- 在 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
打开一个新页面,这两个页面间将建立一条数据通道。
- 在
wx.navigateTo
的 success 回调中通过EventChannel
对象发射事件。 - 被打开的页面通过
this.getOpenerEventChannel
方法来获得一个EventChannel
对象,进行监听、发射事件。 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)是指一个对象从创建、运行到销毁的整个阶段,强调的是一个时间段。小程序启动,表示生命周期开始,小程序关闭,表示生命周期结束。在开启和关闭中间这个阶段,就是小程序的生命周期。
在小程序中,声明周期分为两类:
- 应用生命周期:特指小程序从启动、运行到销毁的过程。
- 页面生命周期:特指小程序中,每个页面的加载、渲染和销毁的过程。
由此可见,应用生命周期包含了页面生命周期(小程序开始、渲染各个页面、小程序结束)
生命周期函数
生命周期函数是由小程序框架提供的内置函数,会伴随着生命周期,自动按次序执行。
生命周期函数的作用:允许程序员在特定的时间点,执行某些特定的操作。例如,页面刚加载的时候,可以在onLoad 生命周期函数中初始化页面的数据。相对于生命周期,生命周期函数更强调时间点。
小程序中的生命周期函数分为两类:
应用的生命周期函数:特指小程序从启动、运行到销毁期间依次调用的那些函数。
应用生命周期函数需要在 app.js 中进行声明,示例如下:
// app.js App({ /** * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次) * 可以从本地存储中读取数据然后加载到小程序中 */ onLaunch: function () {}, /** * 当小程序启动,或从后台进入前台显示,会触发 onShow */ onShow: function (options) {}, /** * 当小程序从前台进入后台,会触发 onHide */ onHide: function () {}, /** * 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息 */ onError: function (msg) {} })
页面的生命周期函数:特指小程序中,每个页面从加载、渲染到销毁期间依次调用的那些函数。
页面声明周期函数需要在页面的 .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 修改组件的样式隔离选项,有两种用法:
在组件的 .js 文件中新增如下配置:
Component({ options: { styleIsolation: 'isolated' } })
或者在组件的 .json 文件中新增如下配置:
{ "styleIsolation": "isolated" }
其中,styleIsolation 的可选值有三个:
可选值 | 默认值 | 描述 |
---|---|---|
isolated | 是 | 启用样式隔离 |
apply-shared | 否 | 页面 wxss 样式会影响到组件,但是组件的 wxss 不会影响页面 |
shared | 否 | 页面和组件的 wxss 相互影响 |
数据、方法和属性
- 在小程序组件中,用于组件模板渲染的私有数据,需要定义到 data 节点中,示例:
Component({
// 组件的初始数据
data: {
count: 0
}
})
- 在小程序组件中,事件处理函数和自定义方法需要定义到 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'
})
}
}
- 在小程序组件中,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。各自的特点如下:
- 组件实例刚被创建好的时候,created 生命周期函数会被触发:
- 此时还不能调用
setData
。 - 通常在这个生命周期函数中,只应该用于给组件的 this 添加一些自定义的属性字段。
- 此时还不能调用
- 在组件完全初始化完毕、进入页面节点树后,attached 生命周期函数会被触发:
- 此时,
this.data
已被初始化完毕。 - 这个生命周期很有用,绝大多数初始化的工作可以在这个时机进行(例如发请求获取初始数据)。
- 此时,
- 在组件离开页面节点树后,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 种方式:
- 属性绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容的数据。
- 事件绑定:用于子组件向父组件传递数据,可以传递任意数据。
- 获取组件实例:父组件还可以通过
this.selectComponent()
获取子组件实例对象,这样就可以直接访问子组件的任意数据和方法。
属性绑定
父组件的 data 节点:
data: {
count: 0
}
父组件的 wxml 结构:
<!-- 通过属性向子组件传递数据 -->
<son count="{{ count }}"></son>
子组件的 properties 节点:
properties: {
count: Number
}
子组件使用父组件传递过来的数据:
<view>{{ count }}</view>
事件绑定
事件绑定用于实现子向父传值,可以传递任何类型的数据。使用步骤如下:
父组件的 .js 中,定义一个函数,这个函数即将通过自定义事件的形式,传递给子组件。
// 在父组件中定义syncCount方法 // 将来,这个方法会被传递给子组件,供子组件进行调用 syncCount() { console.log('syncCount') }
在父组件的 wxml 中,通过自定义事件的形式,将步骤 1 中定义的函数引用,传递给子组件。
<son bind:sync="syncCount"></son>
在子组件的 .js 中,通过调用
this.triggerEvent('自定义事件名称',{参数对象})
,将数据发送到父组件。methods: { addCount() { this.setData({ count: this.properties.count + 1 }) this.triggerEvent('sync', {value: this.properties.count}) } }
在父组件的 .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 个限制:
- 不支持依赖于 Node.js 内置库的包。
- 不支持依赖于浏览器内置对象的包(类似 jquery)。
- 不支持依赖于 C++ 插件的包。
Vant Weapp
Vant Weapp 是有赞前端团队开源的一套小程序 UI 组件库,助力开发者快速搭建小程序应用。它所使用的是 MIT 开源许可协议,对商业使用比较友好。官方文档地址戳我进入。
在小程序项目中,安装 Vant 组件库主要分为如下 3 步:
通过 npm 安装。
npm init -y npm i @vant/weapp@1.11.6 -S --production
构建 npm 包。
打开微信开发者工具,点击 “工具”、“构建 npm”,并勾选 “使用 npm 模块”选项(新版本可以不用手动勾选),构建完成后,即可引入组件。
修改 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)
}
})
注意:
- 不要在
App
方法中使用getApp
,使用 this 就可以直接拿到 app 实例。 - 通过
getApp
获取实例之后,不要私自调用生命周期函数。
分包
分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。对小程序进行分包的好处:
- 可以优化小程序首次启动的下载时间。
- 在多团队共同开发的时候可以更好的解耦协作。
分包前,小程序项目中所有的页面和资源都被打包到了一起,导致整个项目体积过大,影响小程序首次启动的下载时间。
分包后,小程序项目由 1 个主包 + 多个分包组成:
- 主包:一般只包含项目的启动页面或 TabBar 页面、以及所有分包都需要用到的一些公共资源。
- 分包:只包含和当前分包有关的页面和私有资源。
在小程序启动时,默认会下载主包并启动主包内页面(tabBar 页面需要放到主包中),当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示(非 tabBar 页面可以按照功能的不同,划分为不同的分包之后,进行按需下载)。
目前,小程序分包的大小有以下两个限制:
- 整个小程序所有分包大小不超过 16M(主包 + 所有分包)。
- 单个分包 / 主包大小不能超过 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"
]
}
]
}
小程序的打包原则是:
- 小程序会按照 subPackage 的配置进行分包,subPackages 之外的目录被打包到主包中。
- 主包也可以有自己的 pages(即最外层的 pages 字段)。
- tabBar 页面必须在主包内。
- 分包之间不能相互嵌套。
分包的引用原则是:
- 主包无法引用分包的私有资源。
- 分包之间不能相互引用私有资源。
- 分包可以引用主包内的公共资源。
独立分包
独立分包本质上也是分包,只不过它比较特殊,可以独立于主包和其他分包而单独运行。一般情况下,用户通过访问主包才能启动小程序,而使用独立分包就可以让用户通过这个分包来启动小程序。(一个小程序中可以有多个独立分包)
实际上,普通分包和独立分包的区别便是是否依赖于主包才能运行:普通分包必须依赖于主包才能运行,而独立分包可以在不下载主包的情况下独立运行。
开发者可以按需,将某些具有一定功能独立性的页面配置到独立分包中。原因如下:
- 当小程序从普通的分包页面启动时,需要首先下载主包。
- 而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度。
小程序目录结构:
|------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 // 通过此节点声明当前分包为独立分包
}
]
}
独立分包和普通分包以及主包之间,是相互隔绝的,不能相互引用彼此的资源!例如:
- 主包无法引用独立分包内的私有资源。
- 独立分包之间,不能相互引用私有资源。
- 独立分包和普通分包之间,不能相互引用私有资源。
- 特别注意:独立分包中不能引用主包内的公共资源。
分包预下载
分包预下载指的是:在进入小程序的某个页面时,由框架自动预下载可能需要的分包,从而提升进入后续分包页面时的启动速度。同一个分包中的页面享有共同的预下载大小限额 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
})
}
转发功能
转发一共有两种方式:
页面 .js 文件声明
onShareAppMessage
事件监听函数,并自定义转发内容。只有定义了此事件处理函数,右上角菜单栏才会显示 “转发” 按钮。/** * 用户点击右上角分享 */ onShareAppMessage() { return { title: '自定义转发标题', path: '/pages/share/share', imageUrl: '自定义转发图标路径' } }
通过给按钮组件设置属性
open-type="share"
,可以在用户点击按钮后触发Page.onShareAppMessage
事件监听函数。<view> <button open-type="share">转发</button> </view>
分享到朋友圈
小程序页面默认不能被分享到朋友圈,开发者需主动设置 “分享到朋友圈” 才可以,实现分享到朋友圈需满足两个条件:
- 页面必须设置允许 “发送给朋友”,即页面 .js 文件必须声明
onShareAppMessage
事件监听函数。 - 页面必须设置允许 “分享到朋友圈”,即页面 .js 文件必须声明
onShareTimeline
事件监听函数。
/**
* 监听右上角分享到朋友圈按钮
*/
onShareTimeline() {
return {
title: '自定义分享标题',
query: 'id=1',
imageUrl: '自定义转发图标路径'
}
}
手机号验证组件
手机验证组件,用于帮助开发者向用户发起手机号申请,必须经过用户同意后,才能获得由平台验证后的手机号,进而为用户提供相应服务。手机号验证组件分为两种:手机号快速验证组件以及手机号实时验证组件。
- 手机号快速验证组件:平台会对号码进行验证,但不保证是实时验证。
- 手机号实时验证组件:在每次请求时,平台均会对用户选择的手机号进行实时验证。
<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) {
}
客服功能
小程序为开发者提供了客服能力,同时为客服人员提供移动端、网页端客服工作台便于及时处理消息。使用方式:
- 需要将 button 组件 open-type 的值设置为 contact,当用户点击后就会进入客服会话。
- 在微信公众后台,绑定后的客服账号,可以登陆网页端客服或移动端小程序客服接收、发送客服消息。
项目开发
通用模块封装
在项目开发的时候,我们会经常使用一些 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 使用的具体流程:
- 客户端向服务器发起登录请求,服务端验证用户名与密码。
- 验证成功后,服务端会签发一个 Token,并将 Token 发送到客户端。
- 客户端收到 token 以后,将其存储起来,比如放在 localStorage、sessionStorage 中。
- 客户端每次向服务器请求资源的时候需要带着服务端签发的 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
})
}
})
}