小程序开发-微信原生
一、组成结构
1.小程序开发前的准备
- 微信公众平台注册账号【微信公众平台 (qq.com)】
- 下载【微信开发者工具】建议稳定版
2.创建微信小程序
- 打开微信开发者工具


看到此页面,表示小程序已经创建成功

3.基本组成结构
pages所有的 **小程序页面,**每个页面以 单独的文件夹存在
index:index 页面文件夹,文件夹下面都包含 4 个 基本的页面文件,这四个文件共同组成 index 页面
- index.js:
.js文件 -- 页面的脚本文件,存放页面的数据、事件处理函数、生命周期等 - index.json:
.json文件 -- 当前页面的配置文件,配置页面的外观、表现等 - index.wxml:
.wxml文件 -- 页面的模板结构文件 - index.wxss:
.wxss文件 -- 当前页面的样式表文件
- index.js:
logs
- ...
utils:工具类文件夹
- utils.js
app.js:小程序的项目逻辑文件,用来 注册小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。
app.json:小程序公共配置文件,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
app.wxss:小程序公共样式表
project.config.json:是项目配置文件,用来记录我们对小程序开发工具所做的个性化配置
setting中保存了编译相关的配置projectname中保存的是项目名称appid中保存的是小程序的账号 ID
sitemap.json:配置小程序及其页面是否允许被微信索引
- rules中保存了索引规则列表的配置
- rules规则中,action表示页面是否能被索引(allow,disallow),page表示生效的页面
- 一个小程序的页面由什么组成?
- 小程序的页面由 4 个文件组成
- 并且 这4个文件 应该被放入到 pages 文件夹下的 同一个文件夹中
- project.config.json 文件的作用是什么?
- 项目配置文件,用来记录我们对小程序开发工具所做的个性化配置
- sitemap.json 文件的作用是什么?
- 配置小程序及其页面是否允许被微信索引
4.wxml
什么是
WXMLWXML(WeiXin Markup Language)是框架设计的一套标签语言(组件),用来构建小程序页面的结构,其作用类似于网页开发中的HTMLWXML和HTML的区别标签名称不同
- HTML(div, span, img, a)
- WXML(view, text, image, navigator)
属性节点不同
- <a href="#">超链接</a>
- <navigator url="/pages/home/home">跳转到home页</navigator>
提供了动态渲染数据的模板语法
数据绑定
条件渲染
列表渲染
- 我是否可以在
wxml文件中,写入div标签?- 可以写入
div标签,但是不要使用。div标签会被解析成类 view标签效果 wxml中需要写入 小程序提供的组件,如果写入的为 非小程序组件,则会被解析为类 view标签效果
- 可以写入
5.wxss
- 什么是
WXSS- WXSS
(WeiXin Style Sheets)是一套样式语言,用于描述WXML` 的组件样式 - 类似于网页开发中的
CSS WXSS具有CSS大部分的特性
- WXSS
- 新增了尺寸单位 -- rpx,一个
rpx为页面宽度的1 / 750 - 提供了全局的样式和局部样式。
- 全局样式:写入到 根目录的
wxss中的样式 - 局部样式:写入到 **页面的
wxss** 中的样式
- 全局样式:写入到 根目录的
- 此外
WXSS仅支持部分CSS选择器.class(推荐使用) 和#idelement- 并集选择器和后代选择器
::after和::before等伪类选择器
wxss新增了什么尺寸单位?
rpx尺寸单位,一个rpx为页面宽度的1 / 750wxss中推荐使用什么选择器?
.class类选择器
6..json 配置文件
小程序中,额外多出了一个 .json 的配置文件,.json 文件主要分为两个:
- 项目根目录下的 .json 文件
- 修改项目的基本配置
- 首页
- navigationBarBackgroundColor
- 修改项目的基本配置
- 页面中的 .json 文件
- 修改页面的基本配置(优先级高)
- navigationBarBackgroundColor
- 修改页面的基本配置(优先级高)
- 小程序中的 .json 文件主要分为哪两种?
- 项目根目录下的 .json 文件
- 页面中的 .json 文件
- 当这两种配置文件出现相同配置时,会出现什么结果?
- 以 页面的.json 文件 为主
7.小程序的宿主环境
- 宿主环境指的是 程序运行所必须的依赖环境
- web 前端的宿主环境为:浏览器
android软件的宿主环境为:android系统IOS软件的宿主环境为:IOS系统1
- 小程序的宿主环境是什么?
- 小程序的宿主环境为:手机微信
- 所以:小程序可以调用 手机微信 中的
- 扫码
- 支付
- 登录
- 分享
- 等等功能
- 所以:小程序可以调用 手机微信 中的
- 小程序的宿主环境为:手机微信
- 小程序的运行环境:

- 分成渲染层和逻辑层
- 其中
WXML模板和WXSS样式工作在渲染层- 渲染层的界面使用了
WebView进行渲染 - 一个小程序存在多个界面,所以渲染层存在**多个
WebView**线程
- 渲染层的界面使用了
- JS 脚本工作在逻辑层。
- 逻辑层采用
JsCore线程运行JS脚本
- 逻辑层采用
- 其中
- 这两个线程的通信会经由微信客户端做中转
- 逻辑层发送网络请求也经由
Native(手机原生系统)转发
- 逻辑层发送网络请求也经由
8.小程序的内置组件
二、核心语法
1.数据驱动原则
数据驱动:
// 商品
let product = {
price: 10,
num: 5
}
// 总价格
let total = 0;
// 计算总价格的方法
function getTotal(product) {
return product.price * product.num
}
// 计算商品的总价格
total = getTotal(product)
// 进行打印
console.log('总价格:' + total);
// 50 太贵了,所以我们少购买了两个商品,也就是让 num = 3
product.num = 3;
// 问:总价格是多少?
console.log('总价格:' + total); // 此时,打印发现总价格还是 50 元,如果要说原因的话,那么应该很简单,【因为我们没有重新进行价格的计算嘛】
// 但是,此时大家有没有想过一点?我们为什么要进行价格的计算呢?
// ----------------------------------------------------
// 当商品的数量发生变化时,商品的总价格【理应发生变化】,不是吗?
上面的例子,就是我想要跟大家说的:【当数量发生变化时,商品的总价格理应发生改变】。
那么同样的道理,在我们的页面中,假如:
某一个 DOM 依赖于某个数据进行展示,那么【当数据发生变化时,视图也理应发生变化】。
而这个就是【响应式数据驱动】。
PS:如果大家想要跟深入的了解,那么可以查看博客:聊一聊响应式构建的那些经历
小程序中完成响应式:
在
data中定义数据// index.js // 获取应用实例 const app = getApp() Page({ data: { product: { price: 10, num: 5 } } })在
wxml中使用数据<view> <view> <!-- wxml 中访问数据,必须使用 {{}} 语法,{{}} 语法中可以放置【任意的、单一的 JavaScript 表达式】 --> 商品的单价:{{product.price}} </view> <view> 商品的数量:{{product.num}} </view> <view> 商品的总价格:{{product.price * product.num}} </view> </view>
现在我们已经可以在 js 的 data 中定义数据,并且在 wxml 中通过 {{}} 语法使用数据。
- 什么是数据驱动?
- 当数据发生变化时,视图理应发生变化
- 在小程序中如何完成数据绑定?
- 在 data 中定义数据
- 在 wxml 中通过 使用数据
2.常用事件与属性列表
处理点击事件
接下来我们希望做一件事情:
创建一个按钮
当用户点击按钮时
让 product 的 num + 1
创建按钮的方式非常简单:
<button type="primary">num + 1</button>
问题在于:我们如何给这个按钮添加点击事件呢?
有过开发经验的同学,可能会猜到:我们可以给 button 一个 click 事件来监听按钮的点击。
可是大家需要知道,现在我们是在【小程序】中,那么如果想要给 button 添加点击事件则不可以使用 click 而是 bind:tap / bindtap。
其中 bind: / bind 表示【绑定事件】,tap 为绑定的具体事件。小程序具体事件列表,可以点击 这里 查看。
<button type="primary" bind:tap="onAddNum">num + 1</button>
接下来需要在 js 中定义对应的 事件
/**
* 定义事件处理的方法
*/
onAddNum () {
console.log('onAddNum')
}
到目前:我们已经 监听了按钮的点击事件,并且写入了对应的处理函数 ,接下来就需要 **修改 num 的值 **
修改 data 的数据
想要修改 data 中的数据,那么我们需要借助一个函数 setData。
setData 接收一个 对象作为参数,这个对象就是最新的 data 数据。
其中 key 为要修改的数据, value 为最新的值
访问 data 的数据
因为我们想要让 num + 1 ,所以我们还需要拿到 num 的当前值,想要访问 num 的值,可以通过 this.data.product.num 的形式访问
所以最终的修改 num 的代码为:
/**
* 定义事件处理的方法
*/
onAddNum () {
this.setData({
'product.num': this.data.product.num + 1
})
此时,当我们点击 button ,可以发现:【当 num 发生改变时,总价格也发生了对应的变化】
- 如何为按钮添加点击事件?
bindtap||bind:tap- 如何修改 data 中数据的值?
- 通过
this.setData({})定义新的值- 通过
this.data访问具体的值
3.事件传参
现在让我们把需求变得更加复杂一些。
我们希望
onAddNum方法可以接收一个参数,每次点击num增加的数量为传入的参数
那么如果想要实现这个需求的话,那么就需要涉及到一个知识点:【事件传参】。
如果大家有过开发经验的话,那么可能会认为这是一个非常简单的需求,顺便可以写下如下代码:
// html
<button type="primary" bind:tap="onAddNum(5)">num + 1</button>
// js
onAddNum (step) {
this.setData({
'product.num': this.data.product.num + step
})
}
可是,假如我们真按照以上代码进行实现的话,那么 你应该会收到以下如下的警告:

这个警告的意思是:没有一个叫做 onAddNum(5) 的方法用来处理当前的这个 tap 事件。
也即是说:onAddNum(5) 会被当做一个 完整的方法名字,而不是 方法名为:onAddNum,传入了参数为 5 !
那么如果我们想要传递参数应该怎么做呢?
在小程序中,如果想要给 **点击事件传递参数的话,**那么需要借助 event 对象 和 data- 属性 !
参数的传递包含两个部分:
- 形参
- 实参
形参:
首先先来看 形参,对于 点击事件的回调方法 而言,默认会接收一个参数 event (事件对象)。这个 event 对象为:回调方法的唯一参数
实参:
对于 小程序 中,我们不能直接为 回调方法传递实参。
而是需要通过:属性绑定的形式,把需要传递的参数绑定到 当前 DOM 元素中,绑定数据的属性需要以 data- 开头。该属性可以通过 e.target.dataset 进行访问。
// html
<button type="primary" bind:tap="onAddNum" data-step="5">num + 1</button>
// js
onAddNum (e) {
// 获取 data-step 的值
let step = parseInt(e.target.dataset.step);
this.setData({
'product.num': this.data.product.num + step
})
}
- 如果想要在【点击事件中】传递参数,那么需要怎么做?
- 通过属性绑定(data-xx)的形式,把需要传递的参数绑定到 当前
DOM元素中- 在对应的回调函数中,通过
e.target.dataset进行访问
4.实现【双向数据绑定】
上一章节中我们通过【事件传参】实现了【每次点击 + 5】 的功能,但是这样的功能未免还是有些太单调了。
所以我们接下来希望实现一个新的功能:
创建一个数字输入框,输入框 与【商品数量】完成 【双向数据绑定】。
即:
- 输入框内容发生变化时,商品数量同步跟随变化
- 商品数量发生变化时,输入框内容同步跟随变化
那么这样的功能我们应该如何去实现呢?
如果想要实现这个功能,那么我们需要先把这个功能进行拆解,【把一个复杂的功能拆解成多个简单的功能】是实现一个复杂逻辑的标准方式。
那么如何进行拆解呢? 大家可以先进行以下思考,然后再继续向下进行学习!
以上功能拆解如下:
- 创建一个【数字输入框】
- 设置 【商品数量】 为输入框的初始值
- 监听用户的输入行为
- 获取用户输入的值
- 赋值给【商品数量】
// html
<view>
商品的数量:
<!-- 1. 创建一个【数字输入框】 -->
<!-- 2. 设置 【商品数量】 为输入框的初始值 -->
<input class="num-input" type="number" value="{{ product.num }}" bindinput="onInput" />
</view>
// js
/**
* 3. 监听 input 的输入事件
*/
onInput (e) {
// 4. 获取用户输入的值
const val = parseInt(e.detail.value);
// 5. 赋值给【商品数量】
this.setData({
'product .num': val
})
:
什么叫做双向数据绑定?
当视图发生变化时,数据跟随发生变化。
当数据发生变化时,视图跟随发生变化.
小程序中如何实现双向数据绑定?
通过
value为input视图绑定数据通过监听
bindinput获取视图的变化,在回调方法中修改数据
5.条件渲染
现在你已经买了很多的商品了,可是当你出去结账的时候,售货员小姐姐对你发出了一声惊呼:
- 如果【总价格 <= 100 】:hello 帅哥
- 如果【总价格 > 100 && 总价格 < 1000】:哇哦 有钱人哦
- 如果【总价格 >= 1000】:土豪你好
如果想要实现这么一个功能的话,那么就需要使用【条件渲染】的功能了。
小程序中提供了两个 API 都可以实现【条件渲染】的功能:
wx:if ... wx:elif ... wx:elsehidden
那么下面我们就分别用这两个语法来实现一下这个功能:
<!-- wx:if ... wx:elif ... wx:else:判断结果为 true 则进行渲染,否则不进行渲染 -->
<view>
售货员小姐姐惊呼:
<text wx:if="{{ product.price * product.num <= 100 }}">hello 帅哥</text>
<text wx:elif="{{ product.price * product.num > 100 && product.price * product.num < 1000 }}">哇哦 有钱人哦</text>
<text wx:else>土豪你好</text>
</view>
<!-- hidden:结果为 true 则隐藏,否则不隐藏 -->
<view>
售货员小姐姐惊呼:
<text hidden="{{ !(product.price * product.num <= 100) }}">hello 帅哥</text>
<text hidden="{{ !(product.price * product.num > 100 && product.price * product.num < 1000) }}">哇哦 有钱人哦</text>
<text hidden="{{product.price * product.num < 1000}}">土豪你好</text>
</view>
- v-if 和 hidden 的区别是什么?
v-if用来控制 【组件是否会被渲染】hidden用来控制【组件是否会被隐藏】- 一般来说,
wx:if有更高的切换消耗而hidden有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用hidden更好,如果在运行时条件不大可能改变则wx:if较好。
6.列表渲染
新的需求:
如果我们有一组商品,并且希望把这组商品全部渲染出来得话,那么就需要使用到【列表渲染】的功能。
小程序中为我们提供了 v-for 指令,让我们进行【列表渲染】的实现。
同时也为我们提供了一个:包裹性质的容器 block 组件,当我们去循环多个元素时,可以使用 block 进行包裹,block 组件只起到包裹的其他组件的作用,本身并不会进行渲染。
// html
<!--
利用 wx:for 循环渲染商品
默认数组的当前项的下标变量名默认为 index,
数组当前项的变量名默认为 item
-->
<view class="product-box">
<block wx:for="{{ products }}" wx:key="index">
<view class="product-item">
<text>商品名:{{item.name}}</text>
<text>价格:{{item.price}}</text>
</view>
</block>
</view>
// js
data: {
products: [
{
name: '苹果',
price: 3.2
},
{
name: '面包',
price: 5.0
},
{
name: '可乐',
price: 2.5
}
]
}
- 使用
wx:for时,当前项的【下标变量名】和【当前项变量名】默认分别是什么?
- 默认数组的当前项的下标变量名默认为 index
- 数组当前项的变量名默认为 item
block组件是否会被渲染?
block只是一个包裹性质的容器,不会被渲染。
7.配置文件解读
app.json配置文件:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.htmlpages数组:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#pages- 创建
list页面
- 创建
window对象:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#windowtabbar对象:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#tabBarindex页面list页面
页面.json配置文件:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html
8.数据请求
wx.request 发起网络请求,请求的方式主要分为两种:
- get 请求
- post 请求
这里准备了两个数据请求接口,可以用来测试 wx.request 的数据请求(详见接口文档):
- /api/test/getList
- /api/test/postData
那么接下来我们就根据 wx.request 来完成一个基本的接口请求
// html
<view>
<button type="primary" bindtap="onGetClick">发起 get 请求</button>
</view>
// js
// index.js
// 获取应用实例
onGetClick () {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/test/getList',
method: 'GET',
success: (res) => {
console.log(res);
}
})
}
这样的代码看起来没有任何问题,但是我们却得到了一个错误(可测试的 APPID:wxf01e2ce0eb588aac):

而要解决这个问题,我们就需要明确一个问题:小程序中的数据请求有什么限制?
- 只能请求
HTTPS类型的接口 - 必须将接口的域名添加到信任列表中
解决方案:
- 生产环境:将想要请求的域名协议【更改为 HTTPS】并【添加到域名信任列表】
- 开发环境:通过勾选

当 get 请求完成,接下来来测试一下 post 请求:
// html
<button type="primary" bindtap="onPostClick">发起 post 请求</button>
// js
onPostClick () {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/test/postData',
method: 'POST',
data: {
msg: '愿大家心想事成,万事如意'
},
success: (res) => {
console.log(res);
}
})
}
题外话(扩展内容:针对有 web 前端开发经验的同学):
- 跨域问题: 跨域问题主要针对 浏览器 而言,而小程序宿主环境为【微信小程序客户端】,所以小程序中不存在【跨域问题】
ajax请求:ajax依赖于XMLHttpRequest对象,而小程序宿主环境为【微信小程序客户端】,所以小程序中的【网络请求】不是ajax请求
小程序中的数据请求有什么限制?以及如何解决这种限制
- 限制:
- 只能请求
HTTPS类型的接口- 必须将接口的域名添加到信任列表中
- 解决方案:
- 生产环境:将想要请求的域名协议【更改为 HTTPS】并【添加到域名信任列表】
- 开发环境:通过勾选
小程序的数据请求会存在跨域问题吗?为什么?
- 不会
【跨域问题】只存在于基于浏览器的
Web开发中由于小程序的宿主环境不是浏览器,而是微信客户端
所以小程序中不存在跨域问题
小程序的数据请求可以叫做
ajax请求吗?为什么?不可以
ajax的核心是依赖于 【浏览器端】 的XMLHttpRequest对象由于小程序的宿主环境不是浏览器,而是微信客户端
所以小程序的数据请求不可以叫做
ajax请求
9.异步编程新方案 - promise
首先先去假设一个场景:
目前有一个需求,需要你按照以下的逻辑去进行接口请求:
- 先去请求接口 A
- 在接口 A 获取到数据之后,再去请求接口 B
- 在接口 B 获取到数据之后,再去请求接口 C
- 在接口 C 获取到数据之后,再去请求接口 D
如果按照上一小节学习到的内容,那么我们会得到以下的代码(接口代码请见:03-小程序核心语法/02-回调地狱.html):
A(function (res) {
console.log(res);
B(function (res) {
console.log(res);
C(function (res) {
console.log(res);
D(function (res) {
console.log(res);
})
})
})
})
在这个 颜值即正义 的世界里面,我们这样的代码结构应该是 没有前途的。 因为它太丑了,并且太难以阅读了。
假想一下,如果我们要请求 10 个接口的话,那么代码会变成什么样子?
所以在编程圈里对这样的代码有一个非常学术的名字:回调地狱 -> 回调函数的大量嵌套导致出现 复杂且难以阅读 的逻辑
点击 Promise 进入官方文档:
<!--
使用 Promise 进行定义接口:
Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
它一个构造函数,所以可以通过 new 关键字来构建它,获取实例。
在 Promise 中,分为了三种状态:
1. 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
2. 已兑现(fulfilled): 意味着操作成功完成。
3. 已拒绝(rejected): 意味着操作失败。
可以通过 promise 实例的
1. 成功:promise.then()
2. 失败:promise.catch()
3. 结束:promise.finally()
的三个方法,进行链式调用来解决回调地狱的问题。
-->
<script>
const isA = true
const isB = true
const isC = true
const isD = true
function A() {
// 1. 创建 Promise 实例
return new Promise((resolve, reject) => {
// 2. 当前处于 【待定(pending)】 状态下
console.log('执行 A 接口的逻辑')
setTimeout(() => {
if (isA) {
// 3. 进入 【已兑现(fulfilled)】 状态下
resolve('接口 A 执行完成')
} else {
// 4. 进入 【已拒绝(rejected)】 状态下
reject('接口 A 执行失败')
}
}, 1000)
})
}
function B() {
return new Promise((resolve, reject) => {
console.log('执行 B 接口的逻辑')
setTimeout(() => {
if (isB) {
resolve('接口 B 执行完成')
} else {
reject('接口 B 执行失败')
}
}, 1000)
})
}
function C() {
return new Promise((resolve, reject) => {
console.log('执行 C 接口的逻辑')
setTimeout(() => {
if (isC) {
resolve('接口 C 执行完成')
} else {
reject('接口 C 执行失败')
}
}, 1000)
})
}
function D() {
return new Promise((resolve, reject) => {
console.log('执行 D 接口的逻辑')
setTimeout(() => {
if (isD) {
resolve('接口 D 执行完成')
} else {
reject('接口 D 执行失败')
}
}, 1000)
})
}
// 获取 Promise 实例
A()
// 通过 .then 方法获取当前 Promise 的执行结果
.then(res => {
console.log(res);
// 标记下一步进入 B 方法
return B()
})
// 继续 .then 进行下一次的异步操作
.then(res => {
console.log(res);
// 标记下一步进入 C 方法
return C()
})
// 继续 .then 进行下一次的异步操作
.then(res => {
console.log(res);
// 标记下一步进入 D 方法
return D()
})
// 继续 .then 进行下一次的异步操作
.then(res => {
console.log(res);
// 结束
})
Promise 与 回调地狱的结果代码对比截图

- promise 是如何解决回调地狱的问题呢?
- 通过
.then的方式进行 链式调用- Promise 的状态分为几种,分别是什么?
- 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled): 意味着操作成功完成。
- 已拒绝(rejected): 意味着操作失败。
- 如何让 Promise 变成 已兑现(fulfilled)的状态,如何接收已兑现(fulfilled)的结果
- 通过
resolve可以把Promise的状态,从 【待定(pending)】转变为 【已兑现(fulfilled)】- 通过
promise实例.then方法可以接收 已兑现(fulfilled) 的结果
但是看到这里之后,可能还会有很多同学 充满疑惑, “ 我并不感觉 promise 的这种方式更加简单呀? ”,如果你确实有这样的 疑问 的话,那么你应该相信这样的疑问在之前也被人提出过。
那么这个问题是怎么解决的呢?请看下一节 异步编程再升级 - async + await
10.异步编程再升级 - async + await
Promise 的方案解决了 回调地狱 的问题,但是 Promise 又带来了新的问题,那就是:大量的链式调用,让我们的代码变得又臭又长!
// 使用 async 和 awiat 可以简化 Promise 的异步操作,把 Promise 的异步操作变为同步写法
// async:标记一个函数为异步函数
// await:标记当前操作为异步操作,await 关键字只能使用在被 【async标记的函数中】
async function test() {
const resA = await A();
console.log(resA);
const resB = await B();
console.log(resB);
const resC = await C();
console.log(resC);
const resD = await D();
console.log(resD);
}
三种实现方案截图对比:

async和await的作用是什么?
async和await可以简化promise操作- 使
promise的异步操作拥有 同步写法- 使用
await的注意事项是什么?
await必须在被async标记的函数中使用
11.小程序中使用 promise 解决异步编程
回过头来来看【小程序的代码】,同时回顾一下之前我们解决过的需求:
目前有一个需求,需要你按照以下的逻辑去进行接口请求:
- 先去请求接口 A
- 在接口 A 获取到数据之后,再去请求接口 B
- 在接口 B 获取到数据之后,再去请求接口 C
- 在接口 C 获取到数据之后,再去请求接口 D
那么接下来我们需要做的就很简单了,我们要 **使用 async 和 await ** 简化以上操作。
如果要达到我们的目标,那么我们需要分成两步来去操作:
- 获取到
promise实例对象 - 使用
async和await简化promise的操作
获取到 promise 实例对象:
因为 wx.request 不支持 promise 化,所以我们需要:使用 promise 封装 wx.request 请求
pA () {
return new Promise((resolve, reject) => {
console.log('执行 A 接口的逻辑');
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/test/A',
success: (res) => {
resolve(res)
},
fail: (err) => {
reject(err)
}
})
})
}
使用 async 和 await 简化 promise 的操作(PS:注意不要勾选 ES6 转 ES5)
async onPromiseGetClick () {
const resA = await this.pA()
console.log(resA.data.data.msg);
const resB = await this.pB()
console.log(resB.data.data.msg);
const resC = await this.pC()
console.log(resC.data.data.msg);
const resD = await this.pD()
console.log(resD.data.data.msg);
}
- 如何使
wx.request配合async和await使用?
- 使用
promise封装wx.request请求- 使用
async和await简化promise的操作
12.生命周期
到现在为止我们已经学习了非常多的小程序核心知识点,那么接下来我们就需要去实现一个小的案例了。
那么接下来我们就先去实现这个案例的第一个功能:
我们希望 页面出现之后,可以获取接口数据,并进行渲染
那么这样的一个简单需求,根据我们现在所学到的知识是:没有办法实现的。
如果想要实现这个功能,就需要掌握 页面的生命周期
什么是生命周期:
想要学习【小程序】的生命周期,那么我们必须要先搞清楚,什么是【生命周期】
所谓 生命周期 就是:一件事物由 创建 到 销毁 的全过程。
在这个过程中会有很多 ” 关键的时刻 “,这些关键的时刻就是 生命周期函数
在 【小程序】中,生命周期主要分为两部分:
- 页面的生命周期(本章节内容)
- 组件的生命周期(后续章节讲解)
创建新的页面 list,在新创建的页面中,我们可以发现在 js 文件中已经默认生成了很多的代码:
// pages/list/list.js
Page({
...
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
console.log('onLoad');
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
console.log('onReady');
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
console.log('onShow');
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
console.log('onHide');
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
console.log('onUnload');
},
...
})
在这些代码中,我们重点关注 生命周期函数--xx 相关的内容。
这 5 个函数,就是 【小程序中的生命周期函数】,我们把鼠标放入到【函数上】,那么【小程序开发工具】会提示出对应的【函数解释】。
这些生命周期函数不需要全部掌握,我们只需要着重掌握其中两个就可以:
onLoad:最先被调用,可以用来【接收别的页面传递过来的数据】。在后面的【页面跳转】中会再去进行讲解。onReady:页面初次渲染完成后调用。我们可以 在这里从服务端获取数据
那么知道了这个之后,回到我们最初的需求上,我们希望 页面出现之后,可以获取接口数据,并进行渲染。 那么怎么去进行实现呢?
很简单!只需要在 onReady 中调用获取接口数据的方法就可以了。
那么现在 我们已经在页面出现之后,获取到了接口的数据,所以接下来我们只需要根据数据完成页面的渲染就可以了:
// html
<scroll-view class="list-box" scroll-y>
<block wx:for="{{ listData }}" wx:key="index">
<view class="list-item">{{ index }} -- {{ item.title }}</view>
</block>
</scroll-view>
// js
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: async function () {
console.log('onReady');
const data = await this.getList()
this.setData({
listData: data.list
})
},
getList() {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/test/getList',
method: 'GET',
success: (res) => {
resolve(res.data.data)
}
})
})
}
// css
.list-item {
padding: 26px;
font-size: 20px;
font-weight: bold;
border-bottom: 1px solid #cccccc;
}
- 什么是生命周期?什么是生命周期函数?
- 所谓 生命周期 就是:一件事物由 创建 到 销毁 的全过程。
- 在这个过程中会有很多 ” 关键的时刻 “,这些关键的时刻就是 生命周期函数
onReady的调用时机是什么?
- 页面初次渲染完成后调用。我们可以 在这里从服务端获取数据
13.pullToRefresh - 下拉刷新与上拉加载
那么如果我们想要在当前项目中实现【分页请求】,就需要借助【下拉刷新于上拉加载】的功能,也就是 pullToRefresh
整个【分页加载】分为两个部分:
- 上拉加载
- 下拉刷新
这两部分需要分别来进行处理,首先我们先来看【上拉加载】
上拉加载:
在 【小程序】中,默认已经实现了【上拉加载】的功能,可以直接通过监听 onReachBottom 函数,来监听:页面上拉触底事件。当页面滑动到底部时,会触发 onReachBottom 函数。
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('onReachBottom');
},
在【用户上拉】时,我们希望获取【下一页】的数据,所以我们需要对当前的数据进行分页:
/**
* 页面的初始数据
*/
data: {
// 当前页数
page: 1,
// 每页的数据量
size: 10
},
然后【当页面进入时】,我们获取第一页的数据,所以我们需要对代码进行一下修改:
getList() {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/test/getList',
method: 'GET',
// 请求当前页的数据
data: {
page: this.data.page,
size: this.data.size
},
success: (res) => {
resolve(res.data.data)
}
})
})
}
然后在【上拉操作】时,持续进行后续的数据请求:
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: async function () {
console.log('onReachBottom');
// 修改 page
this.setData({
page: this.data.page + 1
})
// 获取最新数据
const data = await this.getList()
// 将最新的数据补充到现有数据的后面
this.setData({
listData: [...this.data.listData, ...data.list]
})
},
同时我们希望 数据加载完成后,给用户一个提示,同时不在发起数据请求
// html
<!-- 底线 -->
<view class="bottom" wx:if="{{ listData.length === total }}">-- 我也是有底线的! --</view>
// js
data: {
// 总数据量
total: -1
},
onReady: async function () {
const data = await this.getList()
this.setData({
listData: data.list,
// 为总数据量赋值
total: data.total
})
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: async function () {
console.log('onReachBottom');
// 如果当前数据量已经 === 总数据量,则表示数据已经加载完成了
if (this.data.listData.length === this.data.total) {
return;
}
...
}
那么到目前,我们已经完成了【上拉加载】的操作,但是我们知道,我们还缺少一个【下拉刷新】的操作!
大家如果看到这里已经感觉代码量很大了,那么可以先暂停一下,把以上代码实现一遍,完成之后,继续往下去看!
下拉刷新:
想要在【小程序】中实现【下拉刷新】不同于上拉加载,需要首先开启【下拉刷新】:
// 页面.json
{
"backgroundColor": "#cccccc",
"enablePullDownRefresh": true
}
当我们开启了【下拉刷新】之后,我们就可以去监听 onPullDownRefresh 函数,这个函数会在:用户下拉刷新时进行回调
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
console.log('onPullDownRefresh');
},
在此回调中,我们需要进行的操作就非常简单了,我们只需要:重置页数,重置数据源,关闭下拉动画 就可以了:
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: async function () {
console.log('onPullDownRefresh');
// 重置页数
this.setData({
page: 1
})
// 获取最新数据
const data = await this.getList()
// 将最新的数据补充到现有数据的后面
this.setData({
listData: data.list
})
// 关闭下拉刷新的动作(在真机中,下拉刷新动作不会自动关闭)
wx.stopPullDownRefresh()
},
- 何开启下拉刷新
- 对 页面对应的
json文件 中通过"enablePullDownRefresh": true开启- 在
onPullDownRefresh中,一般进行什么操作
- 重置页数
- 重置数据源
- 关闭下拉动画
- 在
onReachBottom中,一般进行什么操作
- 判断数据是否已经加载完成
- 自增页数
- 累加数据
14.页面跳转
我们现在已经把【数据列表】全部展示出来了,然后接下来我们需要完成【文章详情页的渲染】,也就是点击【item 跳转到文章详情页面】
小程序的页面跳转分为两种方式:
- 声明式导航
- 跳转到
tabbar页面 - 跳转到
非tabbar页面 - 后退页面
- 跳转到
- 编程式导航
- 跳转到
tabbar页面 - 跳转到
非tabbar页面 - 后退页面
- 跳转到
声明式导航:
【小程序】中提供了一个:跳转页面的组件 navigator ,使用这个组件可以完成【声明式导航】
<!-- 跳转到 非 tabbar 页面 -->
<block wx:for="{{ listData }}" wx:key="index">
<view class="list-item">
<!-- 注意:url 的表达式必须为 / 开头的页面路径 -->
<navigator url="/pages/detail/detail">{{ index }} -- {{ item.title }}</navigator>
</view>
</block>
----
<!-- 跳转到 tabbar 页面 -->
<!-- 注意:跳转到 tabbar 页面,必须要指定 open-type="switchTab"-->
<navigator open-type="switchTab" url="/pages/index/index">跳转到首页</navigator>
-----
<!-- 后退页面 -->
<!-- 注意:后退页面必须指定 open-type="navigateBack" -->
<navigator open-type="navigateBack">后退</navigator>
编程式导航:
【小程序】中提供了三个 API ,用来帮助我们进行 编程式导航:
wx.switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
<!-- 编程式导航跳转到首页 --> <button type="primary" bindtap="onSwitchToHome">利用 switchTab 跳转到首页</button> onSwitchToHome () { wx.switchTab({ url: '/pages/index/index', }) }wx.navigateTo:保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
<!-- 编程式导航跳转到详情页面 --> <button type="primary" bindtap="onNavigateToDetail">利用 navigateTo 进入详情页</button> onNavigateToDetail () { wx.navigateTo({ url: '/pages/detail/detail', }) }wx.navigateBack:关闭当前页面,返回上一页面或多级页面。
<!-- 编程式导航后退页面 --> <button type="primary" bindtap="onNavigateBack">利用 navigateBack 后退页面</button> onNavigateBack () { wx.navigateBack({ delta: 1, }) }
导航传参:
【小程序】的导航传参遵循:get 请求的标准 。
- 以
?分割url和参数- 以
=连接参数的key和value- 以
&来拼接参数
那么下面我们来完成案例的最后一个功能:点击跳转时,传递当前 item 的索引和标题,并且在 detail 页面中展示:
// 声明式导航传递参数
<navigator url="/pages/detail/detail?index={{index}}&title={{item.title}}">{{ index }} -- {{ item.title }}</navigator>
// 编程式导航传递参数
<button type="primary" bindtap="onNavigateToDetail" data-index="{{index}}" data-title="{{item.title}}">利用 navigateTo 进入详情页</button>
onNavigateToDetail (e) {
const { index, title } = e.target.dataset
wx.navigateTo({
url: `/pages/detail/detail?index=${index}&title=${title}`,
})
}
// 在 detail 中接收数据,并展示
<view class="msg">index:{{index}} -- title:{{title}}</view>
onLoad: function (options) {
const {index, title} = options;
this.setData({
index,
title
})
}
- 页面跳转的方式有哪几种?
- 声明式导航
- 编程式导航
- 跳转到 【tabbar】页面和【非 tabbar】页面的方式分别是什么?
- 声明式导航
<navigator open-type="switchTab" url="xx" /><navigator open-type="navigate"(默认可不指定) url="xx" />- 编程式导航
wx.switchTab({ url: 'xx'})wx.navigateTo({ url: 'xx'})- 如何进行导航传参
【小程序】的导航传参遵循:
get请求的标准 。
- 以
?分割url和参数- 以
=连接参数的key和value- 以
&来拼接参数
小结
- 数据驱动原则:由数据来驱动视图 是 现代前端开发的核心思想之一
- 商品案例:
- 数据驱动
- 组件的事件处理
- 处理事件方法传递参数
- 双向数据绑定
- 条件渲染
- 列表渲染
- 列表展示案例:
- 利用配置文件生成
tabbar - 数据请求的限制
- 如何封装
promise的数据请求 - 利用
async + await简化异步请求 - 页面的生命周期概念
- 实现
pullToRefresh - 页面跳转
- 利用配置文件生成
三、进阶概念
1.什么是组件化思想
一个大的项目,由多个小的组件组成,每一个组件都封装了单独的【结构】、【样式】和【行为】。
这就是现在的组件化思想。
如果之前大家有过 vue、react、angular 的开发经验的化,那么应该很好理解 组件化 指的是什么意思。
如果大家之前只有过 html + css + js 的开发经验得话,那么可以把组件理解为:包含了【结构】和【样式】的模块。
2.创建组件
创建组件
- 创建
components文件夹 - 创建
tabs和list文件夹 - 右键 新建
Component
- 创建
使用组件
找到页面的
.json文件在
usingComponents选项下 注册组件key为当前组件在该页面中的标签名value为组件的代码路径index.json
"usingComponents": { "navigation-bar": "/components/navigation-bar/navigation-bar", "tabs":"/components/tabs/tabs", "list":"/components/list/list" }index.vxml
<view> <tabs></tabs> <list></list> </view>
在页面的
wxml文件中,以 注册的key为标签名,使用组件
- 组件应该被放入到哪个文件夹中?
- components
- 如何在页面中使用【自定义组件】?
- 在页面的
.json文件中,通过usingComponents进行注册- 在
wxml文件中,以注册的key为 标签名 进行使用
3.组件的生命周期
组件 的生命周期应该被定义在 lifetimes 中,而方法必须要放入到 methods 中。
/**
* 生命周期函数
*/
lifetimes: {
attached() {
this.loadTabsData()
}
}
组件的生命周期一共有三个:
created: 组件实例刚刚被创建好。此时还不能调用setDataattached:组件完全初始化完毕、进入页面节点树后。绝大多数初始化工作可以在这个时机进行detached:在组件离开页面节点树后

/**
* 组件的初始数据
*/
data: {
// 数据源
listData: [],
// 选中项
active: -1
},
/**
* 生命周期函数
*/
lifetimes: {
attached() {
this.loadTabsData()
}
},
/**
* 组件的方法列表(组件中的方法必须定义到 methods 中)
*/
methods: {
/**
* 获取数据的方法
*/
loadTabsData() {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/hot/tabs',
success: (res) => {
this.setData({
listData: res.data.data.list,
active: 0
})
}
})
}
}
<scroll-view class="tabs-box" scroll-x>
<view wx:for="{{ listData }}" wx:key="index" class="tab {{ active === index ? 'active' : '' }}">
{{item.label}}
</view>
</scroll-view>
.tabs-box {
/* 指定宽度 + 不换行 */
width: 750rpx;
white-space: nowrap;
border-bottom: 1px solid #cccccc;
}
.tab {
/* 指定 display */
display: inline-block;
padding: 12px 22px;
}
.active {
color: #f94d2a;
}
- 组件的 生命周期 和 方法 分别应该被放入到哪个节点下?
- 生命周期应该被定义在
lifetimes中- 方法必须要放入到
methods中created函数中可以调用setData吗?
- 不可以
- 获取数据的操作应该在哪个函数中进行?
attached
4.数据监听器
场景
通过 接口文档 我们可以看出,如果想要获取 list 那么我们需要传递一个 type 的参数,而这个 type 就是用户选中的 tab 项 的 id
所以接下来我们就需要来做一件事情:监听用户选中的 tab,根据用户选中的 tab 来切换底部 list 的数据
目标:监听用户选中的 tab,根据用户选中的 tab 来切换底部 list 的数据
当我们面临一个复杂的需求时,我们需要把 复杂的需求,拆解为几个可执行的步骤
大家看到这里,可以先思考一下,我们如何拆解以上需求...
步骤拆解如下:
- 监听用户选中项的变化
- 获取用户选中的数据的
id- 把
id传递给list组件list组件根据接收到的id获取对应的数据
<view wx:for="{{ listData }}" wx:key="index" class="tab {{ active === index ? 'active' : '' }}" bindtap="onItemClick" data-index="{{index}}">
{{item.label}}
</view>
/**
* 1. 监听用户选中项的变化
* item 点击事件处理
*/
onItemClick(e) {
// 1.1:获取用户选中的 下标
const {
index
} = e.target.dataset;
// 1.2:修改选中项
this.setData({
active: index
})
}
/**
* 1.3:监听 active 的变化
* 通过 observers 定义数据监听器
*/
observers: {
// key 为要监听的数据
// value 为当数据发生变化时,调用的函数
active: function (active) {
// 2:获取用户选中的数据的 `id`
const {id} = this.data.listData[active]
}
}
到目前,我们已经实现了需求的前两步,那么后面的两步怎么做呢?
- 小程序中通过哪个选项来声明数据监听器
observers- 数据监听器的使用场景是什么?(需要同学自己思考)
- 需要监听数据的变化
- 在数据变化之后,进行一些操作的时候
5.组件之间的关系与通讯
组件之间的关系:
组件之间的关系和 html 标签之间的关系其实是相同的:
- 父子关系

- 兄弟关系

不同关系之间的传递数据方式:
父子关系
父向子传参:
// 子组件:通过 properties 声明要从父组件中接收的数据 /** * 组件的属性列表 */ properties: { tabId: String }, // 父组件:通过自定义属性的形式传递数据,以子组件中定义的 key 为属性名,以要传递的数据为属性值 <list tabId="{{tabSelectId}}">子向父传参:
// 子组件:通过 triggerEvent 方法发送一个通知,通知父组件接收数据。 // 方法的第一个参数为:通知名 // 方法的第二个参数为:要传递的数据 this.triggerEvent('change', { id }) // 父组件:通过 bind 监听子组件中发送的通知 // bind 后的内容为 子组件发送的通知名,表达式为接收到该通知时所触发的方法 <tabs bind:change="onTabChange"></tabs> // 方法被触发后可以通过 e.detail 的形式获取子组件传递过来的数据对象 onTabChange (e) { const {id} = e.detail; this.setData({ tabSelectId: id }) }
兄弟关系
兄弟组件之间传参:兄弟关系 === 没有关系。 兄弟组件只是为了方便称呼的一个叫法而已。

所以想要完成兄弟组件之间的传参,就需要:为它们建立关系。
而建立关系说白了就是为了找到一个:和这两个组件都认识的 ”中间人“ 。
这个中间人一般为:统一的父组件。
而最后:兄弟组件之间想要传递数据,就需要利用 ”中间人进行传递“,也就是:
- 【兄弟 A 组件】传递数据给 父组件(中间人)
- 父组件(中间人)再把数据传递给 【兄弟 B 组件】
- 组件之间数据传递的关系可以分为哪几种?
- 父向子传递数据
- 子向父传递数据
- 兄弟组件之间传递数据
- 兄弟组件之间传递数据的方式是什么?
6.组件的插槽
1:定义:
1.1:定义单一插槽:
在 组件 中使用 slot 组件定义插槽。
表示:占据了这一块空间,等待父组件填充。
1.2:定义多个插槽:
小程序默认只能定义一个插槽,如果要定义多个插槽那么需要:在组件中指定 options 选项的 multipleSlots 选项为 true
然后通过 slot 的 name 属性为插槽命名。例如:<slot name="header"></slot>
2:使用:
2.1:使用单一插槽:
在组件使用时,以 innerHTML 的形式插入内容:
<component>
<view>单一插槽插入的 DOM</view>
</component>
2.2:使用多个插槽:
在组件使用时,以 innerHTML 的形式插入内容,以 slot 属性标记当前 DOM 插入到哪个插槽中:
<component>
<view slot="header">该元素将被插入到 name=header 的插槽中</view>
<view slot="footer">该元素将被插入到 name=footer 的插槽中</view>
</component>
- 什么时候需要使用插槽?
- 由 父组件 来指定 子组件 中某一部分展示的内容和样式时
- 小程序中如何定义多个插槽?
- 指定
options的multipleSlots为true
本章小结
- 组件化思想
- 创建组件
- 组件的生命周期
- 数据监听器
- 组件的关系
- 组件的通讯
- 组件的插槽

