学习笔记:微信小程序

介绍

  • 记录在开发微信小程序时的一些知识点

一、储备

  • 理解事件机制

  • 理解组件化

  • 理解数据绑定

  • Flex 布局

  • 移动端适配方案

二、基础

1.Flex布局

(1).介绍

相关知识请前往这里查看

  • 即弹性布局 —— display: flex;

  • 采用 Flex 布局的元素,称为 Flex容器(flex container),简称”容器”。它的所有子元素自动成为容器成员,称为 Flex项目(flex item),简称”项目”

  • 注意:当一个元素设置为 flex,其子元素会自动成为 block 元素

(2).容器属性

1).flex-direction

  • 决定主轴的方向(即项目的排列方向)

  • 可选值:
    row(默认值):主轴为水平方向,起点在左端
    row-reverse:主轴为水平方向,起点在右端
    column:主轴为垂直方向,起点在上沿
    column-reverse:主轴为垂直方向,起点在下沿

2).flex-wrap

  • 定义如果一条轴线排不下如何换行

  • 可选值:
    nowrap(默认):不换行
    wrap:换行,第一行在上方
    wrap-reverse:换行,第一行在下方

3).flex-flow

  • flex-direction 属性和 flex-wrap 属性的简写形式

  • 可选值:默认值为 row nowrap
    其余自行组合

4).justify-content

  • 定义了项目在主轴上的对齐方式

  • 可选值(具体对齐方式与轴的方向有关。下面假设主轴为从左到右):
    flex-start(默认值):左对齐
    flex-end:右对齐
    center: 居中
    space-between:两端对齐,项目之间的间隔都相等
    space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍

5).align-items

  • 定义项目在交叉轴上如何对齐

  • 可选值(具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下):
    flex-start:交叉轴的起点对齐
    flex-end:交叉轴的终点对齐
    center:交叉轴的中点对齐
    baseline: 项目的第一行文字的基线对齐
    stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度

6).align-content

  • 定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用

  • 可选值:
    flex-start:与交叉轴的起点对齐
    flex-end:与交叉轴的终点对齐
    center:与交叉轴的中点对齐
    space-between:与交叉轴两端对齐,轴线之间的间隔平均分布
    space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍
    stretch(默认值):轴线占满整个交叉轴

(3).项目属性

1).order

  • 定义项目的排列顺序

  • 数值越小,排列越靠前,默认为0

2).flex-grow

  • 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大

  • 如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)

  • 如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍

3).flex-shrink

  • 定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小

  • 如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小

  • 如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小

  • 负值对该属性无效

4).flex-basis

  • 定义了在分配多余空间之前,项目占据的主轴空间(main size)

  • 浏览器根据这个属性,计算主轴是否有多余空间

  • 默认值为auto,即项目的本来大小

5).flex

  • flex-growflex-shrinkflex-basis的简写,默认值为0 1 auto,其中后两个属性可选

  • 该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto) 以及 1(1 1 0%)

  • 建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值

6).align-self

  • 允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性

  • 默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

  • 该属性可能取6个值,除了auto,其他都与align-items属性完全一致

2.移动端

(1).物理像素

  • 指屏幕的分辨率,即设备能控制显示的最小单元,可看成对应的像素点

(2).设备独立像素

  • 设备独立像素(也叫密度无关像素):可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用并控制的虚拟像素(比如:css像素,只是在 android 机中 css 像素就不叫"css像素"了而是叫”设备独立像素"),然后由相关系统转换为物理像素

  • 设备独立像素相当于 CSS 像素

(3).dpr 比

  • dpr:即设备像素比=物理像素/设备独立像素,一般以iPhone 6 的dpr为准,即dpr=2

  • PPI:一英寸显示屏上的像素点个数

  • DPI:最早指的是打印机在单位面积上打印的墨点数,墨点越多越清晰

(4).适配方案

0).视口

  • 视觉视口:手机窗口大小

  • 布局视口:指网页

  • 完美视口/理想视口:为了使上面的两个视口基本一致

1).viewport 适配

  • 目的:为了将页面完全显示在不同手机屏幕上且不会出现滚动条

  • 实现方案:

1
<meta name="viewport" content="width=device-width,initial-scale=1.0">
  • 微信底层就进行了 viewport 适配,即 width=device-width

2).rem 适配

  • 目的:一套设计稿的内容在不同的机型上呈现的效果一致,根据屏幕大小不同的变化,页面中的内容也相应变化

  • 实现方案(通过原生js实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function remRefresh() {
let clientWidth = document.documentElement.clientWidth;
// 将屏幕等分10份
let rem = clientWidth / 10;
document.documentElement.style.fontSize = rem + 'px';
document.body.style.fontSize = '12px';
}
window.addEventListener('pageshow', () => {
remRefresh()
})
// 函数防抖
let timeoutId;
window.addEventListener('resize', () => {
timeoutId && clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
remRefresh()
}, 300)
})
  • 实现方案(第三方库):lib-flexible + px2rem-loadelr

3.小程序

(1).特点

  • 没有 DOM

  • 组件化开发:具备特定功能效果的代码集合

  • 体积小,单个压缩包体积不大于2M

(2).文件结构

  • wxml —— view结构

  • wxss —— view样式

  • js —— view行为

  • json —— 数据及配置

!js中固定格式

  • 注册小程序:App() 里面需要写个对象

  • 注册页面:Page() 里面需要写个对象

(3).适配方案

  • 小程序适配单位:rpx(响应式像素单位)

  • 规定任何屏幕下宽度为 750rpx

  • 小程序会根据屏幕的宽度自动计算 rpx值的大小

  • 在 iPhone 6下:1rpx=1个物理像素=0.5px

(4).图片路径

  • 本地图片为静态资源,建议放在 static/images 目录下

  • 建议使用绝对路径,当绝对路径无法使用时再使用相对路径

(5).项目文件说明

a).app.js

  • 在该文件中必须要有首字母大写的 App ,用来注册整个小程序应用

b).app.json

  • pages 中哪个是第一个哪个默认就是首页,且不能加根路径

  • navigationBarBackgroundColor :导航栏背景颜色,只能使用16进制格式

  • navigationBarTextStyle :导航栏标题颜色,仅支持 black / white

c).app.wxss

  • 存放公共样式

  • 小程序中只能使用 class 类

  • 小程序会自动的为所有的页面加一个 <page> 标签,而该标签宽度指定但高度未指定,如果想要给整个页面添加背景颜色,那么需要在这里将高度设置为 100%,这样所有的页面就都可以继承它的大小了

d).index.js

  • 在该文件中必须要有首字母大写的 Page ,用来注册当前页面的实例

(6).编译模式

  • 当我们想要同时看两个页面的显示情况时,可以自定义编译模式

  • 在开发者工具的最上方自定义一个,然后更改启动页面路径即可

4.字体图标

(1).介绍

  • 小程序中可以使用阿里云的 iconfont 字体图标库

(2).使用方法

  • 在官网选好自己需要的字体图标并加入购物车

  • 选好之后将其添加进项目中去

  • 在当前项目中点击 Font class 选项并查看在线链接生成最新代码(该代码可以在网页中直接引用,而在小程序中无法引入)

  • 在浏览器地址栏输入该地址,将里面的内容复制到小程序的 wxss 文件中去

  • 我们可以将该文件放入 /static/iconfont 目录下,并在全局的 app.wxss 中使用如下语句进行引入

1
@import "/static/iconfont/iconfont.wxss";
  • 如果想要使用某图标,在网站复制该图标的class名,按如下即可:

1
<text class="iconfont icon-diantai"></text>
  • 想要修改字体图标的样式,直接修改 iconfont 的样式即可

5.内网穿透

  • 我们一般在做项目的时候使用的是电脑上本地的服务器,如果想要在真机上调试小程序需要使用到内网穿透

  • 推荐使用 utools 工具,搜索安装内网穿透插件

  • 按提示设置地址,修改项目的服务器配置文件即可

  • 如果无法正常显示,可能是预览不会去发送请求,可以打开小程序的调试模式即可

6.登录流程

(1).收集表单项数据

  • 用户名,账号以及密码等

(2).前端验证

  • 验证用户信息(账号、密码)是否合法

    • 验证不通过,提示用户,不需要发请求给后端
    • 验证通过,发请求(携带账号、密码)给服务器端

(3).后端验证

  • 验证用户是否存在

    • 用户不存在直接返回,告诉前端用户不存在
    • 用户存在需要验证密码是否正确
      • 密码不正确返回给前端提示密码不正确
      • 密码正确返回给前端数据,提示用户登录成功(会携带用户的相关信息)

7.视口单位

  • 视口单位分别为:vh 和 vw

  • 1vh = %1 的视口高度

  • 1vw = %1 的视口宽度

  • calc() 可以动态计算 css 的宽高,运算符左右两侧必须加空格,否则计算会失效

8.数据分页

  • 前端分页:总共接收到100条数据,每次给用户显示10条

  • 后端分页:每发一次请求给用户显示10条数据

9.日期函数

  • 小程序中支持内置的日期函数用来获取当前的年月日

1
2
3
4
5
// 更新日期的状态
this.setData({
day: new Date().getDate(),
month: new Date().getMonth() + 1
})

10.npm 包

(1).使用方法

1).初始化 package.json

  • 在小程序调试器的终端中输入以下命令:

1
npm init -y
  • 该文件相当于整个项目的配置文件,并可初始化包名,一般只保留包名和版本号即可

2).修改小程序设置

  • 在详情 - 本地设置中勾选 “使用npm模块”

3).下载npm包

  • 在小程序调试器的终端中输入以下命令:

1
npm install 包名

4).构建npm

  • 在开发工具的 工具 - 构建npm 选项,这样会将下载的包自动转化为小程序可以使用的格式,并放入指定路径

  • 当小程序引入npm包时,该页面首先会从小程序指定npm包路径查找,查找不到会去当前文件夹内进行查找,所以如果不构建 npm 那么就会出现找不到该npm包

5).导入npm包

  • 在需要导入npm的 js 文件中使用以下语句进行导入

1
import 自定义名 from '包名'

(2).常见第三方库

1).PubSub

  • 功能:用来在两个页面之间进行通信

  • 地址:点击这里

  • 使用 npm install pubsub-js 命令进行安装

  • 关于订阅方与接收方的内容,请点击 这里 跳转查看

2).Moment

  • 功能:使用JavaScript 日期处理类库来格式化时间

  • 地址:点击这里

  • 使用 npm install moment 命令进行安装

  • 该js语法中使用的是单位是ms

3).Fly

  • 功能:一个支持所有JavaScript运行环境的基于Promise的、支持请求转发、强大的http请求库。可以让您在多个端上尽可能大限度的实现代码复用

  • 地址:点击这里

  • 使用 npm install flyio 命令进行安装

  • Node 中引入的方式:

1
2
const Fly=require("flyio/src/node");
const fly=new Fly;

4).jsonwebtoken

  • 功能:可以对传输的数据进行加密解密

  • 地址:点击这里

  • 使用 npm install jsonwebtoken 命令进行安装

  • Node 中引入的方式:

1
const jwt = require('jsonwebtoken');
  • 加密与解密:

1
2
3
4
5
// 加密
const token = jwt.sign({ foo: 'bar' }, 'shhhhh');
// 解密
const decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo)

11.函数节流与防抖

  • 使用延时定时器???

12.小程序登录

  • 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系

(1).获取登录凭证

  • wx.login():使用该API进行获取,code是从成功的回调中获取到的

  • 如下:

1
2
3
4
5
wx.login({
success: (res) => {
let code = res.code;
}
})

(2).将登录凭证发送给服务器

  • 这里需要自己在个人服务器中写一个接口,然后这里调用接口即可:

1
let result = await request('/getOpenId', {code});

13.调试错误

  • 当在写 js 语句时,如果发现有些地方有问题,可以在该处写一个 return 来阻断一下

  • 这样该条语句下面的语句就不会执行了

三、框架

  • 关于 框架 的详细内容请查看小程序官方文档

1.小程序配置

(1).全局配置

1).tabBar

  • 可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面(需全局json文件中配置)

  • 常用属性:

    • color:tab 上的文字默认颜色,仅支持十六进制颜色
    • selectedColor:tab 上的文字选中时的颜色,仅支持十六进制颜色
    • backgroundColor:tab 的背景色,仅支持十六进制颜色
    • list:tab 的列表,最少 2 个、最多 5 个 tab
    • position:tabBar 的位置,仅支持 bottom / top
  • **<list>**属性:

    • pagePath:页面路径,必须在 pages 中先定义
    • text:tab 上按钮的文字
    • iconPath:图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片
    • selectedIconPath:选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片

2.框架接口

(1).getAPP

  • 获取到小程序全局唯一的 App 实例

3.wxml语法

(1).数据绑定

1).初始化数据

  • 在当前页面的 js 文件中的 data 选项中

1
2
3
4
5
Page({
data: {
msg:"Wrysmile的博客",
},
})

2).使用数据

  • 在页面模板结构中使用双大括号来使用数据

1
<text class="userName">{ {msg} }</text>

3).修改数据

  • 修改数据需要在生命周期函数的 onLoad() 中进行

  • 获取数据需要使用 this.data.msg 来获取

  • 修改数据需要使用 this.setData 来修改,如下(回调函数可省):

1
2
3
this.setData({
msg:"Wrysmile的小站",
});
  • 特点:

    • 同步修改:this.data 的值同步修改,在非自身的钩子函数也是同步的
    • 异步更新:异步将 setData 函数用于将数据从逻辑层发送到视图层

4).与Vue和React的对比

a).小程序
  • data 中初始化数据

  • 修改数据:this.setData()(该行为始终是同步的)

  • 数据流:

    • 单向数据流:Model --> View
b).Vue
  • data 中初始化数据

  • 修改数据:this.key = value

  • 数据流:

    • 单向数据流:Model --> View
    • 实现了双向数据绑定:v-model
c).React
  • state 中初始化状态数据

  • 修改数据:this.setState()

    • 在自身的钩子函数中(componentDidMount)中是异步的
    • 在非自身的钩子函数中(定时器的回调)中是同步的
  • 数据流:

    • 单向数据流:Model --> View

扩展:数据劫持代理

  • 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Vue数据劫持代理
// 模拟Vue中data选项
let data = {
username: 'curry',
age: 33
}
// 模拟组件的实例
let _this = {
}
// 利用Object.defineProperty()
for(let item in data){
// console.log(item, data[item]);
Object.defineProperty(_this, item, {
// get:用来获取扩展属性值的,当获取该属性值的时候调用get方法
get(){
console.log('get()');
return data[item]
},
// set: 监视扩展属性的, 只要已修改就调用
set(newValue){
console.log('set()', newValue);
// _this.username = newValue; 千万不要在set方法中直接修改当前扩展属性的值,会出现死循环
data[item] = newValue;
}
})
}
console.log(_this);
// 通过Object.defineProperty的get方法添加的扩展属性不能直接对象.属性修改
_this.username = 'wade';
console.log(_this.username);

(2).事件绑定

1).冒泡事件

  • 当一个组件上的事件被触发后,该事件会向父节点传递

  • bind+事件名 :该绑定不会阻止冒泡事件向上冒泡

1
2
3
<view class="goStudy" bindtap="handleParent">
<text bindtap="handleChild">Hello World!</text>
</view>

2).非冒泡事件

  • 当一个组件上的事件被触发后,该事件不会向父节点传递

  • catch+事件名 :该绑定会阻止冒泡事件向上冒泡

1
2
3
<view class="goStudy" catchtap="handleParent">
<text catchtap="handleChild">Hello World!</text>
</view>

3).事件回调

  • 对应的事件回调应该在对应页面的 js 文件中定义,且位置应该与 data生命周期函数 平行

1
2
3
4
5
6
handleParent(){
console.log("parent");
},
handleChild(){
console.log("child");
},

4).事件委托

a).概念
  • 将子元素的事件委托(绑定)给父元素

b).好处
  • 减少绑定的次数

  • 后期新添加的元素也可以享用之前委托的事件

c).原理
  • 冒泡

d).触发事件的是谁
  • 子元素

e).如何找到触发事件的对象
  • event.target:绑定事件的元素不一定是触发事件的元素

  • event.currentTarget:要求绑定的元素一定是触发事件的元素

拓展:事件流三阶段

  • 捕获:从外向内

  • 执行目标阶段

  • 冒泡:从内向外

拓展:绑定事件

  • bindtap

  • bindinput:表单项内容实时发生改变时触发

  • bindchange:表单项内容实时发生改变且失去焦点时触发

拓展:事件分类

  • 标准DOM事件

    • 事件名固定,事件由浏览器触发
  • 自定义事件

    • 绑定事件
      • 事件名
      • 事件的回调
      • 订阅方:是接收数据的一方,例如PubSub.subscribe(消息名,事件的回调)、PubSub.unsubscribr(消息名)取消订阅
    • 触发事件
      • 事件名
      • 提供事件参数对象,等同于原生事件的event事件
      • 发布方:是提供数据的一方,例如PubSub.publish(消息名,提供的数据)

(3).列表渲染

  • wx:for="{ {array} }" (这里双大括号之间没有空格)

    • 下标为 index,个体为 item
  • wx:key="unique" 的值以两种形式提供:

    • 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变
    • 保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字(几乎用不上)

为什么wx:key不使用下标index而是ID?

  • 因为使用下标时,当一个元素发生变化时,后面的其余元素全都需要重新计算

  • 而使用ID时,只需要计算不同ID即可

(4).条件渲染

  • wx:if=‘条件’

  • wx:elif=‘条件’

  • wx:else

引申:动态隐藏元素

  • 一般情况下使用 wx:if 来进行判断来使某元素动态显示或隐藏,但这种性能并不算好

  • 这里可以使用 hidden="" 来进行动态显示隐藏,这里需要一个布尔值来判断

(5).生命周期

  • 生命周期如下图:
    mp01.png

(6).模板

  • 模板(template),可以在模板中定义代码片段,然后在不同的地方调用

  • template 页面只需要 wxml 和 wxss 两个页面即可

1).定义模板

  • 使用 name 属性,作为模板的名字,然后在<template/>内定义代码片段

2).引入模板

  • 在目标页面的 wxml 中用下面语法引入页面结构

1
<import src="页面结构路径" />
  • 在目标页面的 wxss 中用下面语法引入页面样式

1
@import "页面样式路径";
  • 在需要模板的地方通过下面语法引入模板

1
<template is="name属性值" data="{ {...item} }"/>

3).使用模板

  • 使用 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入,如上

(7).引用

  • wxml 提供两种文件引用方式 importinclude

  • wxss 提供一种文件引用方式 @import ""

(8).数组与对象

  • 在中括号中的是数组,在大括号中的是对象

四、组件

  • 关于 组件 的详细内容请查看小程序官方文档

1.swiper

  • 滑块视图容器。其中只可放置 <swiper-item> 组件

  • 有些属性默认为 flase,如果想要开启时需要在 <swiper> 中声明,但可以省略 true,因为只要声明了就为 true

(1).常用属性

  • indicator-dots:是否显示面板指示点

  • indicator-color:指示点颜色

  • indicator-active-color:当前选中的指示点颜色

  • autoplay:是否自动切换

  • circular:是否采用衔接滑动

  • previous-margin:前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值

  • next-margin:后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值

2.scroll-view

  • 可滚动视图区域。使用竖向滚动时,需要给 <croll-view> 一个固定高度,通过 WXSS 设置 height

  • 如果想要设置 flex 布局,必须将 <croll-view> 中的 enable-flex 属性打开才可以生效

  • 注意:该组件横向滚动时,也是需要给其设置高度的,否则高度会自动变大

(1).常用属性

  • enable-flex:启用 flexbox 布局

  • scroll-x/y:允许横/纵向滚动

  • scroll-into-view:设置哪个方向可滚动,则在哪个方向滚动到该元素。注意:值应为某子元素id(id不能以数字开头)

  • scroll-with-animation:在设置滚动条位置时使用动画过渡

  • refresher-enabled:开启自定义下拉刷新

  • refresher-triggered:设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发

  • bindscrolltoupper:滚动到顶部/左边时触发

  • bindscrolltolower:滚动到底部/右边时触发

  • bindrefresherrefresh:自定义下拉刷新被触发

3.video

  • bindplay:当开始/继续播放时触发play事件

  • object-fit:当视频大小与 video 容器大小不一致时,视频的表现形式
    contain:包含
    fill:填充
    cover:覆盖

  • bindtimeupdate:播放进度变化时触发,event.detail = {currentTime, duration} 。触发频率 250ms 一次

  • bindended:当播放到末尾时触发 ended 事件

4.block

  • 可以将 <view> 放在 <block> 标签中,表示块的意思,同时圈住多个元素表示一个整体

  • 该标签在页面结构中并不会加载,所以可通过条件判断来让他显示哪个块

5.button

  • form-type:用于 form 组件,点击分别会触发 form 组件的 submit/reset 事件

  • type:按钮的样式类型

6.form

  • bindsubmit:携带 form 中的数据触发 submit 事件,此时需要组件中带有 name 属性

7.radio

  • 修改 <radio> 组件的大小方法:

1
2
3
radio{
transform: scale(0.7);
}
  • 具体大小自行修改括号中数值,0-1以内

8.封装功能函数/组件

(1).封装功能函数

  • 功能点明确

  • 函数内部应该保留固定代码(静态的)

  • 将动态的数据抽取成形参,由使用者根据自身的情况动态的传入实参

  • 一个良好的功能函数应该设置形参的默认值(ES6的形参默认值)

(2).封装功能组件

  • 功能点明确

  • 组件内部保留静态的代码

  • 将动态的数据抽取成 props 参数,由使用者根据自身的情况以标签属性的形式动态传入 props 数据

1
2
3
4
5
6
7
props: {
msg: {
required: true,
default: 默认值,
type: String
}
}
  • 一个良好的组件应该设置组件的必要性及数据类型

(3).小程序封装函数方法

  • 在根目录新建 utils 目录,将需要封装的函数放入该目录中

  • 关于服务器中的配置内容,我们可以将其单独放在另一个 js 文件中,方便配置

1
2
3
4
// 配置服务器相关信息
export default {
host: 'http://localhost:3000'
}
  • 在封装好的函数中引入服务器配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 引入服务器配置信息
import config from './config'
// 发送ajax请求
// 为了复用,应该以模块化的思想将函数暴露出去;动态的内容应该抽取为参数
export default (url,data={},method="GET") => {
return new Promise((resolve,reject) => {
// new Promise 初始化 promise 实例的状态为 pending
wx.request({
// ES6 同名属性可以省略不写
url: config.host + url,
data,
method,
success: (res) => {
// 修改promise的状态为成功状态resolved
resolve(res.data);
},
fail: (err) => {
// 修改promise的状态为失败状态rejected
reject(err);
}
})
})
}
  • 在需要引用该函数的文件中引入封装好的函数,并调用该函数

1
2
3
4
5
6
7
import request from '../../utils/request'
····
onLoad: async function (options) {
// 使用 await 等待异步任务的结果,且需要搭配 async 来使用
let result = await request('/banner',{type:2});
console.log('结果数据为:',result);
},

9.自定义组件

  • 自定义组件可以将相同的结构提取出来,提高复用率

(1).参数

  • 我们主要用到 propertiesdata 两个参数

  • properties:组件的对外属性,是属性名到属性设置的映射表,其定义有:

    • type: 属性的类型
    • optionalTypes:属性的类型(可以指定多个)
    • value:属性的初始值
  • data:组件的内部数据,和 properties 一同用于组件的模板渲染

(2).方法

  • 在项目根目录新建 components 目录,并在其中创建所需的组件文件夹,在文件夹右键新建component

  • 在主页面的 json 文件中注册该组件

1
2
3
4
5
{
"usingComponents": {
"NavHeader":"/components/NavHeader/NavHeader"
}
}
  • 将页面中所需自定义部分的结构和样式提取到组件相应文件中去

  • 组件一般是复用的,所以其中内容我们应该用变量的形式

  • 在组件的 js 文件中的 properties 中给这些变量设置type和value,如下:

1
2
3
4
5
6
7
8
9
10
properties: {
title: {
type: String,
value: "title默认值"
},
nav: {
type: String,
value: "nav默认值"
}
},
  • 在主页面中以标签名(即json文件中设置的属性值)的方式引入设置好的组件,并设置相应变量值

1
<NavHeader title="推荐歌曲" nav="为你精心推荐"></NavHeader>

五、API

  • 关于 API 的详细内容请查看小程序官方文档

  • 小程序的全局对象是 wx

1.路由跳转

  • 可以从当前页面跳转到另一个页面

  • 使用路由跳转的相关函数时,路径需要使用绝对路径

(1).路由传参

  • 原生小程序中路由传参,对参数的长度有限制,如果参数长度过长会自动截取掉

2.本地存储

  • 将数据存储在本地缓存中指定的 key 中

  • 语法:wx.setStorage() || wx.setStorageSync() || ···

  • 注意:

    • 建议存储的数据为 json 数据
    • 单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB
    • 属于永久存储,同 H5 的 localStorage 一样

3.界面

(1).交互 —— 显示等待提示框

  • 语法:wx.showLoading()

  • 需主动调用 wx.hideLoading 才能关闭提示框

(2).交互 —— 显示模态对话框

  • 语法:wx.showModal()

  • content:提示的内容

  • 其 success 的回调中 confirm 为 true 时,表示用户点击了确定按钮

(3).导航栏 —— 动态设置当前页面的标题

  • 语法:wx.setNavigationBarTitle(Object object)

4.媒体

(1).背景音频

  • BackgroundAudioManager 实例,可通过 wx.getBackgroundAudioManager 获取

  • 要想播放音乐,必须设置该实例的 title 与 src 属性

  • 若需要小程序在退到后台后继续播放音频,需要在 app.json 中配置 requiredBackgroundModes 属性

  • BackgroundAudioManager 实例,可通过 wx.getBackgroundAudioManager 获取

    • BackgroundAudioManager.onPlay(function callback)监听背景音频播放事件
    • BackgroundAudioManager.onPause(function callback)监听背景音频暂停事件
    • BackgroundAudioManager.onStop(function callback)监听背景音频停止事件
    • BackgroundAudioManager.onTimeUpdate(function callback)监听背景音频播放进度更新事件,只有小程序在前台时会回调

六、动画

1.触摸滑动回弹

(1).事件绑定

  • bindtouchstart:手指触摸时

  • bindtouchmove:手指移动时

  • bindtouchend:手指离开时

(2).计算手指移动的距离

  • 在js文件中初始化三个值为0
    手指起始的坐标:startY
    手指移动的坐标:moveY
    手指移动的距离:moveDistance

  • 获取手指的起始坐标需要用到 event 对象

1
2
3
4
5
6
7
8
handleTouchStart(event){
// 拿到第一个手指的纵向起始坐标
startY = event.touches[0].clientY;
},
handleTouchMove(event){
moveY = event.touches[0].clientY;
moveDistance = moveY - startY;
},

(3).设置滑动效果

  • 这里可以使用 CSS3 的 transform

  • 在 data 中初始化数据,然后让其动态更新translateY的值

1
2
3
4
this.setData({
// 使用ES6的模板字符串,因为需要写变量值
coverTransform: `translateY($ {moveDistance}rpx)`,
})
  • 然后在手指松开时设置过渡让其恢复原样,并在手指点击时清除过渡效果

2.导航过渡效果

  • 导航一般使用 <scroll-view> 来编写,当切换导航时,应当使该标签自动滑动到第一位

  • 使用 scroll-into-view 来使其滚动

  • 使用 scroll-with-animation 来设置滚动的过渡效果

3.留声机磁盘旋转效果

  • 使用 animation 来设置旋转动画

  • 使用 animation-dely 来设置动画延迟

  • 使用 @keyframes 来设置动画桢

    • from to:使用于简单的动画,只有起始帧和结束帧
    • 百分比:多用于复杂的动画,动画不止两帧
  • 使用 transform 来设置旋转角度

七、分包

1.介绍

  • 小程序要求压缩包体积不能大于2M,否则无法发布,而实际开发中小程序体积如果大于2M就需要使用分包机制进行发布上传

  • 作用:分包后可解决2M限制,并且能分包加载内容,提高性能

  • 官方文档点击 这里

2.分包形式

(1).常规分包

  • 开发者通过在 app.json 中的 subpackages 字段声明项目分包结构

  • 特点:

    • 加载小程序的时候先加载主包,当需要访问分包的页面时候才加载分包内容
    • 分包的页面可以访问主包的文件,数据,图片等资源
  • 适用于:通常放置启动页/tabBar页面

(2).独立分包

  • 只要在 app.json 中的 subpackages 字段中设置 independent 为 true即可

  • 特点:

    • 独立分包可单独访问分包的内容,不需要下载主包
    • 独立分包不能依赖主包或者其他包的内容
  • 适用于:通常某些页面和当前小程序的其他页面关联不大的时候可进行独立分包,如:临时加的广告页或活动页

(3).分包预下载

  • 配置:

    • app.json 中设置 preloadRule 选项
    • 语法:key(页面路径) : { packages: [预下载的包名 || 预下载的包的根路径] }
  • 特点:

    • 在加载当前包的时候可以设置预下载其他的包
    • 缩短用户等待时间,提高用户体验

八、上传

1.注意

  • 上传正式版本时,一定要记得取消不校验合法域名的勾选,并将自己的外网域名添加到小程序的后台中

  • 上传正式版本之前一定要进行本机测试

2.版本命名规则

  • 1.1.0

    • 第一位代表大版本更新或迭代
    • 第二位代表重要功能的更新
    • 第三位代表最小的功能,如修补bug、修复补丁、进行局部优化之类的

3.提交审核

  • 从小程序开发工具上传以后并不会上线

  • 需要去小程序后台管理网站中进行提交审核

九、云开发

  • 关于云开发的详细内容请查看小程序官方文档

0.区分this

  • .onLoad() 中的 this 指向的是这个页面,即 page

  • .get() 中的成功回调中的 this 为这个成功回调函数,即 function

  • .then() 中的回调中的 this 指向的是这个页面,即 page

如何将.get()中的this指向页面?

  • 如果想要在 .get() 的回调函数中获取到全局 this,则需要使用中间变量来获取

  • .onLoad() 中使用如下语句来获取到 this

1
let that = this;
  • .get() 的成功回调中使用如下语句来更新 data 中的数据

1
2
3
that.setData({
xxx: xxx
})

1.数据库

(0).权限

  • 读取、操作数据库时,必须修改 云开发-数据库-数据权限 中为第一个选项,否则无法读取到集合中的内容

(1).初始化

  • 引入数据库

1
2
3
4
5
6
// 引入默认环境的数据库
const db = wx.cloud.database()
// 引入其他环境的数据库
const testDB = wx.cloud.database({
env: 'test'
})
  • 引入数据库中的集合

1
const todos = db.collection('todos')

(2).查询

  • 获取某个集合中的数据

1
2
3
4
5
db.collection('todos').get({
success: (res) => {
console.log(res);
}
})
  • 获取某个集合中某个id的数据

1
2
3
4
5
db.collection("demolist").get({
success: (res) => {
console.log(res);
}
})

!解决多层成功回调嵌套

  • 这里建议使用 ES6 的 promise

  • 具体执行如下:

1
2
3
4
5
6
7
db.collection("demolist").get().then(res => {
console.log(res)
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
  • then() 为成功的回调,catch() 为失败的回调

!查询指令

  • .doc(""):只通过id来进行查询

  • .where({}):可通过各个属性进行查询

  • .count():查询数据库集合中的数据个数

  • .watch():监听数据库集合中的数据变化,需要使用 onChange()onError() 属性来返回成功与失败的回调

    • 其中 res 返回的 docChanges 代表改变的数据、docs 代表数据库中最新的数据
  • .limit():指定查询结果集数量上限(小程序端默认及最大上限为 20,在云函数端默认及最大上限为 1000)

  • .orderBy():指定查询排序条件,参数一为需要排序的属性,参数二中定义顺序(asc)、逆序(desc)

  • .skip():指定查询返回结果时从指定序列后的结果开始返回,常用于分页

  • .field():指定返回结果中记录需返回的字段

(3).添加

a).普通添加

  • 代码如下:

1
2
3
4
5
6
7
8
9
10
db.collection("demolist").add({
data: {
title: "我是新添加的",
author: "张三",
content: "法外狂徒"
}
}).then(res => {
// 成功的回调
console.log(res);
})

b).表单提交添加

  • 方法一:定义相同变量来进行数据添加

1
2
3
4
5
6
7
8
9
10
11
12
let title = res.detail.value.title;
let author = res.detail.value.author;
let content = res.detail.value.contebbbb//nt;
db.collection("demolist").add({
data: {
title: title,
bb author: author,
content: content
}
}).then(res => {
console.log(res)
})
  • 方法二:通过ES6的解构赋值来进行数据添加

1
2
3
4
5
6
7
8
9
10
let {title,author,content} = res.detail.value;
db.collection("demolist").add({
data: {
title,
author,
content
}
}).then(res => {
console.log(res)
})
  • 方法三:通过同名对象来进行数据提交(此方法必须保证数据库中对象名和表单中的name属性值相同)

1
2
3
4
5
6
let detailValue = res.detail.value;
db.collection("demolist").add({
data: detailValue
}).then(res => {
console.log(res)
})

(4).更新

  • 通过 .update() 来更新:只更新对应数据的某一项(可以使用where进行查询)

1
2
3
4
5
6
7
8
9
db.collection("demolist").where({
author: "Wrysmile"
}).update({
data: {
author: "wrysmile"
}
}).then(res => {
console.log(res);
})
  • 通过 .set() 来更新:直接将对应数据覆盖掉所有内容(不可以使用where进行查询)

1
2
3
4
5
6
7
8
db.collection("demolist").doc("b00064a7606af40c0d563c370374ae1f").set({
data: {
author: "wrysmile",
title: "通过set设置的标题"
}
}).then(res => {
console.log(res)
})

(5).删除

  • 通过 .remove() 来删除:小程序端只可以删除一条数(不可以使用where进行查询)

1
2
3
4
5
6
db.collection("demolist")
.doc(inputValue)
.remove()
.then(res => {
console.log(res);
})

(6).command

  • 数据库操作符,通过 db.command 获取

a).定义

  • 一般在定义数据库的时候就顺便定义command指令

1
2
const db = wx.cloud.database();
const _ = db.command;

b).逻辑操作符

  • 查询:

    • .and():用于表示逻辑 “与” 的关系,表示需同时满足多个查询筛选条件
    • .or():用于表示逻辑 “或” 的关系,表示需同时满足多个查询筛选条件
    • .not():用于表示逻辑 “非” 的关系,表示需不满足指定的条件
    • .nor():用于表示逻辑 “都不” 的关系,表示需不满足指定的所有条件

c).比较操作符

  • 查询:

    • .eq():表示字段等于某个值,可接收 number, boolean, string, object, array, Date
    • .neq():表示字段不等于某个值,可接收 number, boolean, string, object, array, Date
    • .lt/gt():表示需小于/大于指定值,可以传入 Date 对象用于进行日期比较
    • .lte/gte():表示需小于/大于或等于指定值,可以传入 Date 对象用于进行日期比较
    • .in/nin():表示要求值在/不在给定的数组内

d).字段操作符

  • 查询:

    • .exists():判断字段是否存在
  • 更新:

    • .set():用于设定字段等于指定值
    • .remove():用于表示删除某个字段
    • .inc()/mul():原子操作,用于指示字段自增/自乘某个值
    • .min()/max():给定一个值,只有该值小于/大于字段当前值才进行更新
    • .rename():字段重命名
      • 如果需要对嵌套深层的字段做重命名,需要用点路径表示法
      • 不能对嵌套在数组里的对象的字段进行重命名

e).数组操作符

  • 查询:

    • .all():用于数组字段的查询筛选条件,要求数组字段中包含给定数组的所有元素
    • .elemMatch():用于数组字段的查询筛选条件,要求数组中包含至少一个满足 elemMatch 给定的所有条件的元素
    • .size():用于数组字段的查询筛选条件,要求数组长度为给定值
  • 更新:

    • .push():对一个值为数组的字段,往数组添加一个或多个值;若字段原为空,则创建该字段并设数组为传入值
    • .pop():对一个值为数组的字段,将数组尾部元素删除
    • .unshift():对一个值为数组的字段,往数组头部添加一个或多个值;若字段原为空,则创建该字段并设数组为传入值
    • .shift():对一个值为数组的字段,将数组头部元素删除
    • .pull():给定一个值或一个查询条件,将数组中所有匹配给定值或查询条件的元素都移除掉
    • .pullAll():给定一个值或一个查询条件,将数组中所有匹配给定值的元素都移除掉。跟 pull 的差别在于只能指定常量值、传入的是数组
    • .addToSet():原子操作,给定一个或多个元素,除非数组中已存在该元素,否则添加进数组

2.云函数

  • 我们一般将调用数据库的操作放在云函数中,实现前后端分离

  • 优势:突破小程序端只能返回20条数据的限制,最大支持100条数据

(1).云函数调用数据库

  • cloud.init() 后面定义数据库

1
2
// 此时这里就不需要使用wx.了
const db = cloud.database();
  • 在入口函数中返回数据库的请求

1
2
// 这里使用ES6的promise语法
return await db.collection("demolist").get()
  • 在需要调用数据库的页面的onload中调用:

1
2
3
4
5
wx.cloud.callFunction({
name: "getData",
}).then(res => {
console.log(res)
})

(2).前端与云函数传递数据

  • 前端是在调用云函数方法的 data 属性中传递需要给云函数的值

1
2
3
4
5
6
7
8
wx.cloud.callFunction({
name: "getData",
data: {
num: 3
}
}).then(res => {
console.log(res)
})
  • 云函数则通过 event 来接收前端传送来的数据

1
2
let num = event.num;
return await db.collection("demolist").limit(num).get()

(3).修改数据

  • 小程序端只能创建数据的用户修改本条数据

  • 而云函数端拥有最高权限,无论是谁都可以修改别人或者自己的数据

3.云存储

(0).选择图片

  • 上传文件之前需要用户自行选择本地文件来进行上传

  • 使用 wx.chooseImage() API 来调用系统文件夹或者相册,属性有:

    • count:最多可以选择的图片张数
    • sizeType:所选的图片的尺寸
    • sourceType:选择图片的来源
    • success:成功的回调函数
  • 使用该 API 上传图片时会返回一个临时链接,该链接无法在浏览器中打开,只用作上传作用

(1).上传文件

a).小程序端

  • wx.cloud.uploadFile():将本地资源上传至云存储空间,如果上传至同一路径则是覆盖写

  • 属性:

    • cloudPath:云存储路径
    • filePath:要上传文件资源的路径
    • success:成功回调

b).云函数端

  • uploadFile():将本地资源上传至云存储空间,如果上传至同一路径则是覆盖

  • 属性:

    • cloudPath:云存储路径
    • fileContent:要上传文件的内容

(2).文件下载地址

  • 当文件传入云开发存储后,可以点击文件查看相关信息

  • 其中有个下载地址,可以直接预览文件

    • 如果整条链接都复制,则文件有一定的有效期
    • 如果只复制问好前面的,则文件为永久有效

4.问题集合

(1).Environment not found

  • 此问题一般出现在部署云函数时多个云环境共存导致的无法找到当前云环境

  • 解决办法有如下两种:

a).云函数中指定当前云环境

  • 在云函数中按如下进行当前环境的设置

1
2
3
cloud.init({
env: '环境id'
})
  • 缺点:当切换云环境时,需要手动将这里的环境id进行更改

b).使用常量动态获取当前云环境(推荐)

  • 使用 DYNAMIC_CURRENT_ENV 常量值时,后续的 API 请求会自动请求当前所在环境的云资源

1
2
3
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})

十、基础案例

0.页面跳转并携带数据

(1).发送页面

  • 在A页面中可以通过 data-xx 来设置需要传递的值

  • 而上面设置的值可以在函数的 res.currentTarget.dataset 中获取到

  • 然后在跳转页面的 API 中将URL以如下形式进行连接:

1
2
3
wx.navigateTo({
url: '/pages/demo-2/demo-2?title=' + res.currentTarget.dataset.title,
})

(2).接收页面

  • 在B页面中的 onLoad() 函数中可以通过 options.title 来获取到A页面传送来的数据,然后渲染到页面即可

1.用户信息

(1).获取用户信息

1).用户未授权(首次登陆)

  • 所有需要调用的数据都需要在 data 中初始化,所以在 data 中添加 userInfo:{ },

  • 使用 <button> 组件,该组件有个 open-type 属性可以设置微信开放能力,该属性中的 getUserInfo 可以获取到用户信息,并且可通过 bindgetuserinfo 回调来取得信息,即:

1
<button open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo">授权</button>
  • 进行 handleGetUserInfo 回调函数的编写

1
2
3
4
5
6
7
8
9
// 获取用户基本信息
handleGetUserInfo(res){
if(res.detail.userInfo){
// 修改 userInfo的状态数据
this.setData({
userInfo : res.detail.userInfo,
});
}
},
  • 首页动态获取用户名和头像,变量需要使用双大括号括住

1
2
<image src="{{userInfo.avatarUrl}}" class="avatarUrl"></image>
<text class="userName">{ {userInfo.nickName} }</text>

2).用户已授权(再次登陆)

  • 使用 API 中的开放接口 wx.getUserInfo 来获取用户信息,且应该在页面加载时就获取到,所以需要写在 onload()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onLoad: function (options) {
// 授权以后获取用户的信息
wx.getUserInfo({
// 这里使用箭头函数时 this可以指向当前页面
success: (res) => {
// 更新用户信息
this.setData({
userInfo : res.userInfo,
})
},
fail: (err) => {
console.log(err);
},
})
},

3).头像与按钮的隐藏

  • 用户名、头像和按钮是互斥关系,所以需要使用条件渲染

1
2
3
<image wx:if="{ {userInfo.avatarUrl} }" src="{ {userInfo.avatarUrl} }" class="avatarUrl"></image>
<button wx:else open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo">授权</button>
<text wx:if="{ {userInfo.nickName} }" class="userName">{ {userInfo.nickName} }</text>
  • 修改按钮样式,使其占据原本头像位置,当授权后不会显示很突兀

(2).获取用户cookie

  • 获取cookie需要在用户登录请求时获取,将cookie存储在本地

  • 在发请求时增加 header 字段,并在其中读取本地存储的cookie(为了防止用户未登录时报错,应该使用三元运算符进行判断,cookie为空时将其设置为空串)

2.轮播图

(1).放置轮播图容器

  • 在小程序中使用 <swiper> 组件来制作轮播图,其中只可放置 <swiper-item> 组件,而文字和图片可以放在 <swiper-item> 组件中

(2).设置容器样式

  • 根据需求设置容器大小,并将图像的长宽设置为父元素的100%

(3).设置组件属性

  • <swiper> 组件有很多属性,如果想要面板指示点的相关设置,直接去开发者文档中找

3.文本溢出隐藏

  • 当单行或者多行文本溢出某个容器时,我们一般将溢出部分隐藏起来,并在后面以省略号展示

(1).单行文本溢出

  • 单行文本溢出时的解决办法:

1
2
3
4
5
6
7
8
/* 转换为块元素 */
display: block;
/* 规定段落中的文本不进行换行,属性值为文本不会换行,文本会在在同一行上继续,直到遇到 <br> 标签为止 */
white-space: nowrap;
/* 将溢出部分隐藏 */
overflow: hidden;
/* 规定当文本溢出包含元素时发生的事情,属性值为显示省略符号来代表被修剪的文本 */
text-overflow: ellipsis;

(2).多行文本溢出

  • 多行文本溢出时的解决办法:

1
2
3
4
5
6
7
display: -webkit-box;
/* 设置对齐模式 */
-webkit-box-orient: vertical;
/* 设置盒子内容的行数 */
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;

4.前后端交互

  • 使用 API 来进行前后端交互,语法:wx.request()

  • 发起请求可以在生命周期函数的 onLoad 和 onReady 中填写

  • 注意点:

    • 协议必须是 https 协议
    • 一个接口最多配置20个域名
    • 并发限制上限是10个
    • 设置不检验合法域名:开发工具-详情-本地设置-不检验
  • 调用接口时,可以根据接口的参数在 data 中进行相应的调用,如下:

1
2
3
4
5
6
7
8
9
10
wx.request({
url: 'http://localhost:3000/banner',
data: {type:2},
success: (res) => {
console.log(res);
},
fail: (err) => {
console.log(err);
}
})

5.登录界面逻辑

(1).收集表单项数据

  • 给元素绑定 bindinput 事件

  • 这里可以给不同输入框绑定相同事件名,然后用id或者data-来区分,详见这里

  • 在js中初始化数据

  • 在事件回调中更新数据

    • 若使用id,则使用 event.currentTarget.id 来获取不同输入框
    • 若使用data-,则使用 event.currentTarget.dataset.type 来获取不同输入框

(2).前端验证实现

  • 首先需要从表单项中获取到数据,即 let {phone,password} = this.data;

  • 然后进行前端验证,是否账号为空,是否账号合法等

  • 验证完以后使用 wx.showToast API 来进行提示用户

(3).后端验证实现

  • 首先需要发送请求给后端,并将账号密码作为参数传给后端

  • 根据不同的返回值判断登录情况(账号错误、密码错误、账号不存在、登录成功)

(4).个人中心与登录界面交互

1).跳转到登录界面

  • 我们需要给头像与用户名区域绑定单击事件

  • 通过单击事件的回调函数跳转到登录界面

2).本地存储用户数据

  • 在登录界面的后端验证中,如果手机与密码正确,则需要将我们获取到的用户信息存储在本地

  • 使用 wx.setStorageSync() 来存储数据,建议这里使用 JSON.stringify() 转换为JSON对象

  • 跳转到个人中心页面,这里为了让个人中心页面一进入就获取信息,使用 wx.reLaunch() 跳转,即关闭之前所有页面,跳转到目标页面

3).本地读取用户数据

  • 在个人中心页面的 onLoad() 中使用 wx.getStorageSync() 来获取数据

  • 进行判断,如果用户数据不为空,则更新用户数据并将用户数据更新到 data 中去,同时使用 JSON.parse() 将JSON对象转换为js对象

4).修改页面结构

  • 此时页面结构中的头像和用户名需要动态更新,这里可以使用三目运算符

    • 存在用户数据,将用户数据更新到页面上
    • 不存在用户数据,则显示默认内容
  • 代码如下:

1
<image src="{ {userInfo.avatarUrl?userInfo.avatarUrl:'/static/images/personal/missing-face.png'} }"></image>

6.视频播放

(1).多个视频同时播放

1).需求

  • 在点击播放的事件中需要找到上一个播放的视频

  • 在播放新的视频之前关闭上一个正在播放的视频

2).关键

  • 如何找到上一个视频的实例对象

  • 如何确认点击播放的视频和正在播放的视频不是同一个视频

3).方案

  • 将相关参数添加到 this 中来进行判断

1
2
3
4
5
6
7
8
9
10
11
handlePlay(event){
let vid = event.currentTarget.id;
/*
* 当第一次点击时,没有videoContext所以不执行关闭操作,且将当前的vid和videoContext赋给this
* 当第二次点击时,判断当前vid是否与this中相等,相等不执行关闭操作;不相等说明不是同一个视频,此时执行关闭操作
*/
this.vid !== vid && this.videoContext && this.videoContext.stop();
this.vid = vid;
// 创建控制video标签的实例对象
this.videoContext = wx.createVideoContext(vid);
},
  • 以上方案使用了 单例模式 ,即需要创建多个对象的场景下,通过一个变量接收,始终保持只有一个对象,可以节省内存空间

4).终极方案

  • 通过 <image> 代替 <video> 来进行性能优化同时解决多个视频同时播放

  • <video> 标签中通过poster属性获取到当前视频的封面图地址

  • <image> 标签中的地址就使用上面获取到的封面图地址,并绑定和video一样的点击事件与样式和id属性

  • 在相应的点击回调函数中更新data中的视频id数据

  • <video> 中使用wx:if来判断视频id是否和当前项目的id一致,一致显示视频,不一致显示图片

1
2
3
4
5
6
7
8
9
10
11
12
// 点击播放、继续播放的回调
handlePlay(event){
let vid = event.currentTarget.id;
// 更新data中videoId的状态数据
this.setData({
videoId: vid
})
// 创建控制video标签的实例对象
this.videoContext = wx.createVideoContext(vid);
// 当点击图片时,自动播放视频
this.videoContext.play();
},

7.音乐播放

(1).音乐播放时系统栏的控制

1).问题

  • 当用户在操作系统的控制音乐播放/暂停的按钮时,页面并不知道播放状态而导致页面播放状态与真实播放状态不一致

2).方案

  • 我们需要在页面刚加载时监听音乐的播放/暂停/停止状态

  • 当监听到某种状态时就执行回调函数中的内容

  • BackgroundAudioManager 实例,可通过 wx.getBackgroundAudioManager 获取

    • BackgroundAudioManager.onPlay(function callback)监听背景音频播放事件
    • BackgroundAudioManager.onPause(function callback)监听背景音频暂停事件
    • BackgroundAudioManager.onStop(function callback)监听背景音频停止事件

(2).页面销毁时音乐的播放状态

1).问题

  • 当从音乐播放页面返回时,再打开该首歌时,音乐的播放状态与页面显示并不相同

2).关键

  • 使用 globalData 来存放音乐的播放状态

  • 使用 getApp 获取到小程序全局唯一的 App 实例

3).方案

  • 先在 app.js 页面中定义相应的全局数据

1
2
3
4
globalData: {
isMusicPlay: false, // 某个音乐的播放状态
musicId: ''
},
  • 在当前页面的 js 中获取全局数据

1
2
// 获取全局实例
const appInstance = getApp();
  • 在监听音乐播放/暂停/停止的回调中修改全局音乐播放的状态

1
2
3
// 修改全局音乐播放的状态
appInstance.globalData.isMusicPlay = isPlay;
appInstance.globalData.musicId = musicId;
  • 最后在页面加载刚开始判断全局数据中是否有播放状态,如果有则修改当前音乐的播放状态为播放

(3).歌曲播放性能优化

  • 当我们在播放/暂停歌曲时,如果代码编写不当,会重复发送多次请求,这样会很消耗我们的性能

  • 所以我们应该在获取音乐链接时进行判断,如果音乐链接为空我们就发请求,如果不为空我们就不发送请求

8.历史搜索

(1).保存历史搜索记录

  • 获取用户的搜索数据并更新到 data 中

  • 将搜索的关键字添加到搜索历史记录中(如果历史记录中没有该字段,则直接添加;如果有,将该字段放在第一位)

  • 将历史记录的相关内容存放在本地,然后将获取本地历史封装为一个函数,如果本地历史有值则将其更新到 data 中

  • 在生命周期函数中的 onLoad() 中调用之前的函数

(2).清除当前搜索框中的内容

  • 给相应的组件绑定单击响应事件

  • 用户输入的表单项数据关键字模糊匹配的数据都置为空并更新到 data 中(可以给input组件添加 value 值,将用户输入的表单项数据绑定到该 value 值中即可同时清空)

(3).删除历史搜索记录

  • 使用 wx.showModal() 来提示用户是否进行删除

  • 在 success 的回调中可以通过 res.confirm 来判断用户是否点击了确认

  • 当用户点击确认时,清空 data 中 historyList 并且移除本地的历史记录缓存

(4).动态显示历史栏与清除按钮

  • 使用条件渲染,当 historyList 的长度存在时就显示,否则不显示

  • 使用 hidden="" 属性来动态显示或隐藏清除按钮

十一、云开发案例

1.点击数据增加阅读量

(1).获取点击的id和index

  • 将获取到的id和index传给云函数

1
2
3
4
5
6
7
8
let {id,index} = res.currentTarget.dataset;
wx.cloud.callFunction({
name: "updateData",
data: {
id: id,
index: index
}
})

(2).云函数进行更新操作

  • 创建一个专门用来更新的云函数

  • 在云函数中通过id获取到当前点击元素的阅读量并进行自增更新操作

1
2
3
4
5
6
let id = event.id;
return await db.collection("demolist").doc(id).update({
data: {
hits: _.inc(1)
}
})

(3).前后端交互

  • 前端在接收返回值的成功回调中执行获取数据的云函数

1
2
3
4
5
6
wx.cloud.callFunction({
name: "getData",
data: {
id: id
}
})
  • 这里获取数据的云函数可以通过判断传来的值来执行不同的返回语句

1
2
3
4
5
6
7
8
9
let num = event.num;
let page = event.page;
let id = event.id;
if(event.id){
return await db.collection("demolist").doc(id).get()
}
if(event.num || event.page){
return await db.collection("demolist").skip(page).limit(num).get()
}

(4).重新渲染列表数据

  • 因为我们只是更新了data中数组的一项数据,所以只需要更新该条数据就可以,无需更新整个数组

  • 只要定义一个新的变量使用拼串的方法获取到当前数组中的某个值

  • 最后使用ES6语法即可实现重新渲染指定列表数据

1
2
3
4
let newHits = "listData["+index+"].hits";
this.setData({
[newHits]: res.result.data.hits
})