04-小程序进阶概念
1)开篇
在上一大章中我们讲解了 小程序的核心概念,那么这一章节我们来讲解 小程序的进阶概念。
在本章节中我们将会通过一个 案例 来贯穿整个进阶内容的知识。
这个案例看起来似乎很简单,但麻雀虽小,五脏俱全!
同样一个功能可能会有很多不同的一个做法 -> 对于这个案例,我们会通过我们之前没有接触过的小程序知识来实现这样一个功能,其中包括组件化思想、数据监听器、组件的关系和通信、组件中插槽的使用……
2)什么是组件化思想
想要理解小程序中这个「组件」概念 -> 你首先得搞明白一件事,那就是「什么是组件化思想」
一个声明:
对于组件化思想,如果你没有实际的开发经验的话,那么你可能很难立刻理解它。
不过没有关系!
随着你对课程的逐渐深入学习,我们会一直为大家灌输组件化思想的概念!
后边用 uni-app 写那个项目的时候,会反复灌输这个「组件化思想」,如果你此刻不能理解,那这也没有关系,随着你接触到越来越多的场景,你就会越来越明白啥是组件化思想了!
想要搞明白什么是组件,那么就必须要先搞清楚,组件解决了什么样的问题! 所有的技术都是因为解决了一个或多个问题,才有存在的价值!
1、场景
以上的两张截图是【饿了么小程序】中的数据截图,两个截图来自两个不同的页面。
但是我们可以发现:虽然他们来自于不同页面,但是其中店铺的 item
项的结构非常类似。
那么我们现在假象一种场景:
现在你在【饿了么】工作,现在你领导让你去完成这两个页面,那你怎么做?
你会创建两个页面,然后在一个页面中用
wx:for
指令,创建一堆item
的dom 结构
然后再去渲染它们的wxss
。然后在到另外一个页面里面,重复这一系列的操作吗?
如果你是这么做的话?那么我打赌你在【饿了么】待不了 3 天。原因就是因为:你的代码太冗余了!,换句话来说就是:不可维护,不可扩展!
对于我们软件工程师来说,一般情况下,我们会去遵循 所有可重复的代码,都是可以封装的!
这里的封装放到我们当前这个场景下,指的就是 组件化 封装。
明确了我们为啥要进行组件化封装之后,接下来我们就来看一下什么是组件化
2、内容
我们可以把【整个项目】比喻成一个 乐高积木:
那么,对于当前这个项目之中,【每个组件】都可以认为是【其中一块小的积木】(当前一个积木中的一个一个的小零件),这些小的积木可以在当前的项目中使用,也可以直接拿走放到另外的项目中使用。
一个大的项目,由多个小的组件组成,每一个组件都封装了单独的【结构】、【样式】和【行为】。
这就是现在的组件化思想。
图中的的 6 个小积木人,是用来插入到那个大积木里边去的,这些小积木人(有不同的颜色和结构,还有不同的行为)就是组件,如果你有其他大积木,那么你可以把这些小积木人插入到其他大积木里边去
如果之前大家有过 vue、react、angular
的开发经验的话,那么应该很好理解 组件化 指的是什么意思。
如果大家之前只有过 html + css + js
的开发经验的话,那么可以把组件理解为:是一个包含了【结构】和【样式】的模块。
最后不要忽略,本小节开头的话:
3)创建第一个组件
1、场景
指的是业务场景,简单来说就是「需求」
就像在【开篇】中说到的,我们会开发一个【案例】,来贯穿整个第四章的内容。
那么下面我们就来去实现这个案例:
对于这个案例,我们把它拆成两个部分:
- 顶部的 tabs
- 底部的 list
这两个部分,将分别通过两个组件来进行开发 -> 而且这两个组件会产生一定的交互
所以,我们接下来要做的第一件事情,就是【创建这两个组件,并在页面中使用它们】,而这个也就是本小节的主要内容
2、问题
- 组件应该被放入到哪个文件夹中?
- 如何在页面中使用【自定义组件】?
3、内容
- 创建组件
- 创建
components
文件夹 - 创建
tabs
和list
文件夹 -> 用来承载组件的文件夹 - 右键 新建
Component
-> 组件的结构几乎和页面一模一样,都是 4 个文件
- 创建
- 使用组件
- 找到页面的
.json
文件 -> 哪个页面需要用到组件? - 在
usingComponents
选项下 注册组件key
为当前组件在该页面中的标签名value
为组件的代码路径 -> 绝对路径
- 在页面的
wxml
文件中,以 注册的key
为标签名,使用组件
- 找到页面的
注册组件:
使用组件:
4、答案
- 组件应该被放入到哪个文件夹中?
components
- 如何在页面中使用【自定义组件】?
- 在页面的
.json
文件中,通过usingComponents
进行注册- 注册完后,在
wxml
文件中,以注册的key
为 标签名 进行使用
以上就是我们「创建第一个组件」所对应的内容了
4)组件的生命周期
构建这两个组件的代码 -> 先处理
tabs
里边的相关操作 ->tabs
组件被渲染出来之后,可以获取到对应的数据,并且把这个结构给渲染出来
1、场景
接下来我们希望在 tabs
组件被渲染之后,获取 tabs
的数据,并且进行渲染。
参考下,上一大章中的 列表案例
-> 上一章是在页面里边完成,这一次我们要在组件里边完成
要完成这个功能,我们就必须要有一个 前置条件:
那就是:明确组件的生命周期函数,明确在什么时候去获取接口数据
2、问题
之前,我们说到小程序里边的生命周期主要分成了两个部分,第一个是页面的生命周期,第二个则是组件的生命周期
- 组件的 生命周期 和 方法 分别应该被放入到哪个节点下?
created
函数中可以调用setData
吗?- 获取数据的操作应该在哪个函数中进行?
3、内容
💡:前置知识
组件 的生命周期应该被定义在 lifetimes
中,而方法必须要放入到 methods
中。 -> 这跟页面有非常大的不同,在页面里边我们直接写在Page({})
的选项对象里边就好了
组件的生命周期一共有三个:
created
: 组件实例刚刚被创建好。此时还不能调用setData
-> 用的很少,因为无法调用setData
,不要跟 Vue 的created
给混淆了attached
:组件完全初始化完毕、进入页面节点树后才进行调用。绝大多数初始化工作可以在这个时机进行 -> 用的最多detached
:在组件离开页面节点树后才进行调用
💡:实现需求
步骤:
- 定位到组件的
js
文件,在Component({})
的{}
里边添加一个lifetimes
属性,它是一个列表对象,在这里对象里边添加生命周期函数attached
- 在
methods
里边添加loadTabsData
方法,用来发送请求获取数据 -> 把获取到的数据赋值给data
旗下的listData
- 根据
listData
渲染视图 -> 添加相应样式 -> 处理默认选中 -> 添加一个active
数据
代码:
tabs.js
:
// components/tabs/tabs.js
Component({
/**
* 组件的属性列表
*/
properties: {},
/**
* 组件的初始数据
*/
data: {
// 数据源
listData: [],
// 选中项
active: -1,
},
/**
* 组件的方法列表(组件中的方法必须定义到 methods 中)
*/
methods: {
/**
* 获取数据的方法
*/
loadTabsData() {
wx.request({
url: 'https://api.imooc-blog.lgdsunday.club/api/hot/tabs',
success: (res) => {
console.log(res)
this.setData({
listData: res.data.data.list,
active: 0,
})
},
})
},
},
/**
* 生命周期函数
*/
lifetimes: {
attached() {
this.loadTabsData()
},
},
})
请求结果:
渲染情况:
4、答案
- 组件的 生命周期 和 方法 分别应该被放入到哪个节点下?
- 生命周期应该被定义在
lifetimes
这个选项中- 方法必须要放入到
methods
这个节点中created
函数中可以调用setData
吗?
- 不可以
- 获取数据的操作应该在哪个函数中进行? -> 主要有 3 个生命周期函数
attached
-> 我们的大部分操作一般都这个函数中进行
5)数据监听器
1、场景
通过 接口文档 我们可以看出,如果想要获取 list
那么我们需要传递一个 type
的参数,而这个 type
就是用户选中的 tab 项
的 id
所以接下来我们就需要来做一件事情:监听用户选中的 tab
,根据用户选中的 tab
来切换底部 list
的数据
明确对应的场景之后,就可以来看一下对应的问题了!
2、问题
- 小程序中通过哪个选项来声明数据监听器?
- 数据监听器的使用场景是什么? -> 这个答案需要自己总结出来
3、内容
目标:监听用户选中的 tab
,根据用户选中的 tab
来切换底部 list
的数据
有了这个目标后,我们接下来该怎么去完成这对应的一件事情呢?
我们之前说过:
当我们面临一个复杂的需求时,我们需要把这个 复杂的需求,拆解为几个可执行的步骤
大家看到这里,可以先思考一下,我们如何拆解以上需求。..
注意:这是一个「可具体执行」的步骤
步骤拆解如下:
- 监听用户选中项的变化
- 获取用户选中的数据的
id
- 把
id
传递给list
组件list
组件根据接收到的id
获取对应的数据
以上步骤就是根据目标所拆解出的这么四步需求!
💡:实现这四步需求
- 点击事件 -> 传参
- 修改选中项 -> 也就是点谁谁高亮
- 监听选中项的变化 -> 也就是
active
的变化 ->observers
-> 拿到用户选中的数据id
效果:
4、答案
- 小程序中通过哪个选项来声明数据监听器
observers
- 数据监听器的使用场景是什么?(需要同学自己思考)
- 有一天你需要监听数据的变化
- 并且在数据变化之后,进行一些操作的时候
到目前,我们已经实现了需求的前两步,那么后面的两步怎么做呢?
请查看我们下一节:组件之间的关系与组件之间的通讯
6)组件之间的关系与通讯
1、场景
- 监听用户选中项的变化
- 获取用户选中的数据的
id
- 把
id
传递给list
组件list
组件根据接收到的id
获取对应的数据
在上一节中,我们实现了前面两步,但是接下来当我们想要把 id
传递给 list
组件的时候,却遇到了一些问题。我们不知道如何才能 在一个组件中把数据传递给另外一个组件 ,那么这一小节我们就来去解决这个问题。
2、问题
- 组件之间数据传递的关系可以分为哪几种? -> 组件之间进行数据传递,可以分为哪几种传递的形式
- 兄弟组件之间传递数据的方式是什么?
3、内容
分为两块来讲
💡:组件之间的关系
组件之间的关系和 html
标签之间的关系其实是相同的:
html
标签 -> 父标签和子标签,就是父子关系 -> 同级标签就是兄弟关系 -> 同理,组件也是如此
父子关系
兄弟关系
💡:不同关系之间的传递数据方式
我们已经知道了组件之间的关系分为两大类 -> 一个叫父子,一个叫兄弟
我们去分析组件之间的数据传递的时候,也会根据这两大类去进行一个分析
首先是父子关系
对于父子关系,它的数据传递形式可以分成两种:
- 父向子传参
- 子向父传参
在小程序中,我们想要实现父组件向子组件里边传递数据,我们可以分成两步去完成:
- 在子组件里边进行操作:通过
properties
声明要从父组件中接收的数据 - 在父组件里边进行操作:通过自定义属性的形式传递数据,以子组件中定义的
key
为属性名,以要传递的数据为属性值
实现子向父传参,同样也是分成了两个大步:
- 在子组件里边做一些事情:小程序提供了一个
triggerEvent
方法,我们可以通过triggerEvent
方法发送一个通知,通知父组件接收数据- 方法的第一个参数为:通知名
- 方法的第二个参数为:要传递的数据
- 在父组件里边做一些事情:通过
bind
监听子组件中发送的通知bind
后的内容为 子组件发送的通知名,表达式为接收到该通知时所触发的方法- 方法被触发后可以通过
e.detail
的形式获取子组件传递过来的数据对象
可以不加
:
->bindchange
-> 不过,推荐加:
,即bind:change
至此,我们已经明确了父向子传参和子向父传参这种形式。接下来,就来看第二个关系:兄弟关系
其次是兄弟关系
想要搞明白兄弟组件之间传参,首选需要搞明白一点内容:那就是我们一直所说的兄弟关系到底指的是什么呢?
其实,所谓的兄弟关系:兄弟关系 === 没有关系
兄弟关系或者兄弟组件只是为了方便称呼的一个叫法而已
所以想要完成兄弟组件之间的传参,就需要:为它们建立关系
而建立关系说白了就是为了找到一个:和这两个组件都认识的 ”中间人“ 。
这个中间人一般为:统一的父组件。
曹丕和曹植 -> 谁认识他们俩兄弟? -> 他们的父亲——曹操
我们给元素之间说兄弟关系,其实根本就不同于现实生活中的兄弟、姐妹这种实际有关系的情况,两个元素处于同级的,实际上就是没有关系!
而最后:兄弟组件之间想要传递数据,就需要利用 ”中间人进行传递“,也就是:
- 【兄弟 A 组件】传递数据给 父组件(中间人)
- 父组件(中间人)再把数据传递给 【兄弟 B 组件】
搞明白这一点之后,我们就可以来实现我们的代码了
💡:第三步「把 id
传递给 list
组件」
这一步的实现逻辑:
子向父传参:
tabs
组件 ->observers
->active
->triggerEvent
一个change
通知,参数是id
index.wxml
-><tabs bind:change="onTabChange"></tabs>
index.js
-> 写onTabChange
方法
父向子传参:
💡:最后一步「list
组件根据接收到的 id
获取对应的数据」
- 获取数据
- 渲染数据
- 写样式 -> 让列表更好看 -> 注意,垂直滚动列表时, tabs 是固定的,这个固定 tabs 的样式写法可以记住,一种固定形式!
每次切换 tab,发起的数据请求:
实现的效果:
4、答案
- 组件之间数据传递的关系可以分为哪几种? -> 三种
- 父向子传递数据
- 子向父传递数据
- 兄弟组件之间传递数据
- 兄弟组件之间传递数据的方式是什么? -> 理解兄弟关系等于没有关系,让它们之间有关系,也就是产生联系,需要有一个中间人,也就是都认识它们的父组件
- 【兄弟 A 组件】传递数据给 父组件(中间人)
- 父组件(中间人)再把数据传递给 【兄弟 B 组件】
7)组件的插槽
1、场景
整个案例还剩下最后一个功能:在列表的头部和尾部分别展示文本。
list 组件的头部和尾部将来会展示一块内容,但是这块内容具体展示什么,在不同的页面中会不一样,比如
logs
页面,需要的是图片,而不是文本
而这个功能我们有一个额外的要求:具体展示的文本和样式,我们需要在父组件中指定。
因为,组件可以进行复用,当 list
组件应用到 index
页面时,我们展示的文本和样式,并不一定为在其他页面时所想要展示的文本和样式。所以这个文本和样式不可以在 list
中写死。
那么要实现这个功能就需要使用到 插槽 的知识了。
2、问题
- 什么时候需要使用组件的插槽?
- 小程序中如何定义多个插槽?
3、内容
小程序里边提供了一个插槽的支持,通过插槽可以帮我们解决对应的问题 -> 啥问题? -> 就是「头部」和「尾部」的内容不是固定的!
小程序里的插槽分成了两块知识点:
- 定义插槽
- 使用插槽
从「定义插槽」和「使用插槽」这两个维度来讲
💡:定义插槽
定义插槽又分为两块:
- 定义单一插槽
- 定义多个插槽
定义单一插槽:
在 组件 中使用
slot
组件(小程序提供的)定义插槽。我们定义了一个插槽,可这有啥用呢?(也就是插槽的作用) -> 这表示:占据了这一块空间,等待父组件填充。 -> 这块空间将来放啥内容,完全由父组件来决定
定义多个插槽:
小程序默认只能定义一个插槽,如果要定义多个插槽,那么需要:在组件中指定
options
选项的multipleSlots
选项为true
然后通过
slot
的name
属性为插槽命名。例如:<slot name="header"></slot>
-> 因为有多个插槽,所以需要指定名字,以此来指定这个内容往哪个插槽里边填充内容
💡:使用插槽
同样,使用插槽也分为两块:
- 使用单一插槽
- 使用多个插槽
使用单一插槽:
在组件使用时,以 innerHTML
的形式插入内容:
<component>
<view>单一插槽插入的 DOM</view>
</component>
使用多个插槽:
在组件使用时,以 innerHTML
的形式插入内容,以 slot
属性标记当前 DOM
插入到哪个插槽中:
<component>
<view slot="header">该元素将被插入到 name=header 的插槽中</view>
<view slot="footer">该元素将被插入到 name=footer 的插槽中</view>
</component>
4、答案
- 什么时候需要使用插槽?
- 由 父组件 来指定 子组件 中某一部分展示的内容和样式时 -> 此时你就应该想到「我要使用插槽的知识了」
- 小程序中如何定义多个插槽?
- 到组件的
js
文件里边指定options
的multipleSlots
为true
8)总结
- 组件化思想 -> 没有开发经验,很难理解这个思想,不过这没关系,在后边 uniapp 项目之中,会频繁地给大家灌输组件化的思想内容 -> 整个课程学完,这个组件化思想你也就懂了!
- 创建组件 -> 放在
components
文件夹中,每个组件跟小程序页面一样,也是由四个文件组成的 - 组件的生命周期 ->
attached
-> 组件完全初始化完毕、进入页面节点树后。绝大多数初始化工作可以在这个时机进行 -> 比如在tabs
中获取数据 - 数据监听器
- 数据监听器,在小程序里边是通过
observers
这个选项来定义的 - 定义出来的数据监听器,跟 Vue 中的
watch
所起的作用是一模一样的 -> 都是用来监听数据的变化 -> 数据变化后回去回调对应的一个函数
- 数据监听器,在小程序里边是通过
- 组件之间的关系
- 大致分为两种:父子关系、兄弟关系
- 根据不同的关系,传递数据的方式也不一样
- 父子关系的传递数据的方式分为:父向子传递、子向父传递
- 兄弟组件之间的数据传递方式分为:需要找到「中间人」,一般就是兄弟组件统一的父组件
- 组件之间的通讯
- 组件的插槽
- 必须搞明白你应该在什么时候去使用插槽 -> 搞明白这点,关于插槽的用法对你而言就不是一个难点了
以上的内容,就是整个第四章的所有内容了!