小程序进阶概念
开篇
在上一大章中我们讲解了 小程序的核心概念,那么这一章节我们来讲解 小程序的进阶概念。
在本章节中我们将会通过一个 案例 来贯穿整个进阶内容的知识。
什么是组件化思想
对于组件化思想,如果你没有实际的开发经验得话,那么可能很难立刻理解它。
不过没有关系!
随着你对课程的逐渐深入学习,我们会一直为大家灌输组件化思想的概念!
想要明白什么是组件,那么就必须要先搞清楚,组件解决了什么样的问题! 所有的技术都是因为解决了一个或多个问题,才有存在的价值!
场景
以上的 两 张截图是【饿了么小程序】中的数据截图,两个截图来自两个不同的页面。
但是我们可以发现:虽然他们来自于不同页面,但是其中店铺的 item 项的结构非常类似。
那么我们现在假象一种场景:
现在你在【饿了么】工作,现在你领导让你去完成这两个页面,那你怎么做?
你会创建两个页面,然后在一个页面中用
wx:for指令,创建一堆item的dom结构然后再去渲染它们的wxss。然后在到另外一个页面里面,重复这一系列的操作吗?
如果你是这么做的话?那么我打赌你在【饿了么】待不了 3 天。原因就是因为:你的代码太冗余了!,换句话来说就是:不可维护,不可扩展!
对于代码来说,应该遵循 所有重复的,都是可以封装的!
这里的封装放到当前场景下,指的就是 组件化 封装。
问题
内容
那么明确了,为什么要进行 组件化 封装之后,接下来我们来看什么是组件化。
我们可以把【整个项目】比喻成一个 乐高积木:
那么,【每个组件】都可以认为是【其中一块小的积木】,这些小的积木可以在当前的项目中使用,也可以直接拿走放到另外的项目中使用。
一个大的项目,由多个小的组件组成,每一个组件都封装了单独的【结构】、【样式】和【行为】。
这就是现在的组件化思想。
如果之前大家有过 vue、react、angular 的开发经验的化,那么应该很好理解 组件化 指的是什么意思。
如果大家之前只有过 html + css + js 的开发经验得话,那么可以把组件理解为:包含了【结构】和【样式】的模块。
最后不要忽略,本小节开头的话:
对于组件化思想,如果你没有实际的开发经验得话,那么可能很难立刻理解它。
不过没有关系!
随着你对课程的逐渐深入学习,我们会一直为大家灌输组件化思想的概念!
答案
创建第一个组件
场景
就像在【开篇】中说到的,我们会开发一个【案例】,来贯穿整个第四章的内容。
那么下面我们就来去实现这个案例:
对于这个案例,我们把它拆成两个部分:
- 顶部的 tabs
- 底部的 list
两个部分,将分别通过两个组件来进行开发。
所以,我们接下来要做的第一件事情,就是【创建这两个组件,并在页面中使用它们】,而这个也就是本小节的主要内容
问题
- 组件应该被放入到哪个文件夹中?
- 如何在页面中使用【自定义组件】?
内容
- 创建组件
- 创建
components文件夹 - 创建
tabs和list文件夹 - 右键 新建
Component
- 创建
- 使用组件
- 找到页面的
.json文件 - 在
usingComponents选项下 注册组件key为当前组件在该页面中的标签名value为组件的代码路径
- 在页面的
wxml文件中,以 注册的key为标签名,使用组件
- 找到页面的
答案
- 组件应该被放入到哪个文件夹中?
- components
- 如何在页面中使用【自定义组件】?
- 在页面的
.json文件中,通过usingComponents进行注册- 在
wxml文件中,以注册的key为 标签名 进行使用
组件的生命周期
场景
接下来我们希望在 tabs 组件被渲染之后,获取 tabs 的数据,并且进行渲染。
参考下,上一大章中的 列表案例 ,那么要完成这个功能,我们就必须要有一个 前置条件:
那就是:明确组件的生命周期函数,明确在什么时候去获取接口数据
问题
- 组件的 生命周期 和 方法 分别应该被放入到哪个节点下?
created函数中可以调用setData吗?- 获取数据的操作应该在哪个函数中进行?
内容
组件 的生命周期应该被定义在 lifetimes 中,而方法必须要放入到 methods 中。
组件的生命周期一共有三个:
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
数据监听器
场景
通过 接口文档 我们可以看出,如果想要获取 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- 数据监听器的使用场景是什么?(需要同学自己思考)
- 需要监听数据的变化
- 在数据变化之后,进行一些操作的时候
组件之间的关系与通讯
场景
- 监听用户选中项的变化
- 获取用户选中的数据的
id- 把
id传递给list组件list组件根据接收到的id获取对应的数据
在上一节中,我们实现了前面两步,但是接下来当我们想要把 id 传递给 list 组件的时候,却遇到了一些问题。我们不知道如何才能 在一个组件中把数据传递给另外一个组件 ,那么这一小节我们就来去解决这个问题。
问题
- 组件之间数据传递的关系可以分为哪几种?
- 兄弟组件之间传递数据的方式是什么?
内容
组件之间的关系:
组件之间的关系和 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 组件】
-
答案
- 组件之间数据传递的关系可以分为哪几种?
- 父向子传递数据
- 子向父传递数据
- 兄弟组件之间传递数据
- 兄弟组件之间传递数据的方式是什么?
组件的插槽
场景
整个案例还剩下最后一个功能:在列表的头部和尾部分别展示文本。
而这个功能我们有一个额外的要求:具体展示的文本和样式,我们需要在父组件中指定。
因为,组件可以进行复用,当 list 组件应用到 index 时,我们展示的文本和样式,并不一定为在其他页面时想要展示的文本和样式。所以这个文本和样式不可以在 list 中写死。
那么要实现这个功能就需要使用到 插槽 的知识了。
问题
- 什么时候需要使用插槽?
- 小程序中如何定义多个插槽?
内容
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
本章小结
- 组件化思想
- 创建组件
- 组件的生命周期
- 数据监听器
- 组件的关系
- 组件的通讯
- 组件的插槽