2 Vue组件
2.1 简介
人面对复杂问题的处理方式:
- 任何一个人处理信息的逻辑能力都是有限的
- 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容
- 但是,我们人有一种天生的能力,就是将问题进行拆解。口如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
组件也是似的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
- 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化是Vue.js中的重要思想:
- 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一棵组件树
组件化思想的应用:
- 尽可能的将页面拆分成一个个小的、可复用的组件
- 让我们的代码更加方便组织和管理,并且扩展性也更强
2.2 组件开发的步骤
组件的使用分成三步:
即:
- 调用Vue.extend() 方法,创建组件构造器
- 调用Vue.component() 方法,注册组件
- 在Vue实例的作用范围内使用组件
示例:
<div id="app"> <my-cpn></my-cpn> <my-cpn></my-cpn> <my-cpn></my-cpn> <my-cpn></my-cpn> </div> <script src="../js/vue.js"></script> <script> const cpnC = Vue.extend({ template: ` <div> <h2>我是标题</h2> <p>我是内容</p> <p>我是内容</p> </div>` }) Vue.component('my-cpn', cpnC) const app = new Vue({ el: '#app', data: { } }) </script>
|
- Vue.extend:
调用Vue.extend)创建的是一个组件构造器。
通常在创建组件构造器时,传入template代表我们自定义组件的模板。
该模板就是在使用到组件的地方,要显示的HTML代码。
事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会是到这种方式,而且这种方式是学习后面方式的基础
2.Vue.component
- 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
- 所以需要传递两个参数:1、注册组件的标签名2、组件构造器
3.组件必须挂载在某个Vue实例下,否则它不会生效
如果需要使组件只能局部使用而非全局使用,则在对应的vue实例中注册它即可:
const app = new Vue({ el: '#app', components: { cpn: cpnC }, data: { } })
|
2.3 父子组件
前面我们看到了组件树:
- 组件和组件之间存在层级关系
- 而其中一种非常重要的关系就是父子组件的关系
父子组件错误用法:以子标签的形式在Vue实例中使用
- 因为当子组件注册到父组件的components时, Vue会编译好父组件的模块
- 该模板的内容已经决定了父组件将要渲染的HTML (相当于父组件中已经有了子组件中的内容了)
- 是只能在父组件中被识别的
- 类似这种用法, 是会被浏览器忽略的
注册组件语法糖写法
Vue.component('cpn1', { template: ` <div> <h2>我是标题1</h2> <p>我是内容1</p> <p>我是内容1</p> </div>`})
|
组件模板抽离写法
直接上代码:
<body> <div id="app"> <cpn></cpn> <cpn></cpn> <cpn></cpn> </div>
<script type="text/x-template" id="cpn"> <div> <h2>我是标题</h2> <p>我是内容</p> </div> </script>
<script src="./js/vue.js"></script> <script> Vue.component('cpn', { template: '#cpn' }) </script> </body>
|
template写法(最常见)
其实和前面一样,只是用template
标签包裹对应组件内容
<template id='cpn'> <div> <h2>我是标题</h2> <p>我是内容</p> </div> </template>
|
之后像前面那样注册挂载即可
2.4 组件可以访问Vue实例数据吗
组件时一个单独功能模块的封装:
- 这个模块有属于自己的HTML模板,也应该有属性自己的数据data
组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
我们先来测试一下组件中能不能直接访问Vue实例中的data:
<template id="myCpn"> <div>消息: {{ message }}</div> </template> <script> let app = new Vue({ el: '#app', data: { message: "hello world" }, components: { 'my-cpn': { template: 'myCpn' } } }) </script>
|
解析
组件去访问message,message定义在Vue,我们发现最终并没有显示结果
结论: 组件不能直接访问Vue实例中的data数据
我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变得非常臃肿
结论: Vue组件应该有自己保存数据的地方
2.5 组件中的data为什么必须是函数
(待补充)
2.6 父子组件间传值
在前面我们提到了子组件是不能引用父组件或者Vue实例的数据的
但是在开发中,往往一些数据确实需要从上层传递到下层
- 比如一个页面中,我们从服务器请求到了很多的数据
- 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下马的子组件进行展示
- 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
如何进行父子组件见进行通信呢?Vue官方提到:
- 通过props向子组件传递数据
- 通过事件向父组件发送消息
在下面代码中,我们将Vue实例当做父组件,并且其中包含子组件来简化代码
<div id="app"> <cpn :child-movies="movies" :child-message="message"></cpn> </div>
<template id="cpn"> <div> <p>{{childMovies}}</p> <h2>{{childMessage}}</h2> </div> </template>
<script src="./js/vue.js"></script> <script> <!-- 父传子使用props --> const cpn = { template: '#cpn', props: ['childMovies', 'childMessage'], data() { return {} } }
const app = new Vue({ el: '#app', data: { message: "你好啊", movies: ["海王", "海贼王", "上海贼王"] }, components: { cpn } }) </script>
|
props基本用法
在组件中,使用选项props来声明需要从父级接收到的数据
props的值有两种方式:
- 字符串数组,数组中的字符串就是传递时的名称
- 对象,对象可以设置传递时的类型,也可以设置默认值等
props数据验证
在前面的示例中,我们的props选项是使用一个数组;我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型验证时,就需要对象写法了
验证都支持哪些数据类型呢?
type
可以是下列原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
Symbol
你可以为 props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component', { props: { propA: Number, propB: [String, Number], propC: { type: String, required: true }, propD: { type: Number, default: 100 }, propE: { type: Object, default: function () { return { message: 'hello' } } }, propF: { validator: function (value) { return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } })
|
子级向父级传递事件
props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中,这个时候我们需要使用自定义事件来完成
什么时候需要自定义事件呢
- 当子组件需要向父组件传递数据时,就要用到自定义事件了
- 我们之前学习的v-on不仅可以用于监听DOM事件,也可以用于组件间自定义事件
自定义事件的流程
- 在子组件中,通过$emit()来触发事件
- 在父组件中,通过v-on来监听子组件事件
来看一个简单的例子:
<div id="app"> <cpn @item-click="cpnClick"></cpn> </div>
<template id="cpn"> <div> <button v-for="item in categories" @click="btnClick(item)"> {{item.id}}-{{item.name}} </button> </div> </template>
<script src="./js/vue.js"></script> <script> <!-- 父传子使用props --> const cpn = { template: '#cpn', data() { return { categories: [ { id: 1, name: "亚洲高清" }, { id: 2, name: "欧美经典" }, { id: 3, name: "国产自拍" }, { id: 4, name: "日语学习" } ] } }, methods: { btnClick(item) { console.log(item) this.$emit('item-click', item) } } }
const app = new Vue({ el: '#app', data: {
}, components: { cpn }, methods: { cpnClick(item) { console.log('button clicked') console.log(item) } } }) </script>
|
2.7 父子组件的访问方式
$children
有时候我们需要父组件直接访问子组件, 子组件直接访问父组件,或者是子组件访问根组件
- 父组件访问子组件: 使用$children 或 $refs
- 我们先来看下$children的访问
- this.$children是一个数组类型,它包含所有子组件对象
- 我们这里通过一个遍历,取出所有子组件的message状态
<div id="app"> <cpn></cpn> <cpn></cpn> <cpn ref="ccc"></cpn> # 给组件创建一个ref命名 <button @click="btnClick">按钮</button> </div>
<template id="cpn"> <div> <h2>我是子组件</h2> </div> </template>
<script src="./js/vue.js"></script> <script> <!-- 父传子使用props --> const cpn = { template: '#cpn', methods: { showMessage() { console.log('show message') } } }
const app = new Vue({ el: '#app', components: { cpn }, data: { message: '你好呀' }, methods: { btnClick() {
} } })
|
在日常的开发中一般使用refs方式较多
2.8 子组件访问父组件(或根组件)
直接看代码吧(一般很少用):
components: { ccpn: { template: '#cpn', methods: { btnClick() { console.log(this.$parent) console.log(this.$parent.name) console.log(this.$root) } } } }
|
2.9 Slot插槽
slot翻译为插槽
- 在生活中很多地方都有插槽,比如USB插槽等
- 插槽的目的是让我们原来的设备具备更多的扩展性
- 比如电脑的USB插槽可以让我们插入U盘、硬盘、手机等等
组件的插槽
- 组件的插槽也是为了让我妈封装的组件更加具有扩展性
- 让使用者可以决定组件内部的一些内容到底展示什么
例子:移动网站中的导航栏
- 移动开发中,几乎每个页面都有导航栏
- 导航栏我们必然会封装成一个插件,比如nav-bar组件
- 一旦有了这个组件,我们就可以在多个页面复用了
如何封装这类组件呢?
- 它们有很多区别,但也有很多共性
- 如果,我们每一个单独去封装一个组件显然不合适;比如每个页面都返回,这部分内容我们就要重复去封装
- 但是,如果我们封装成一个好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些事文字等等
具体如何封装呢?
核心思想是抽取共性,保留不同
- 最好的封装方式就是将共性抽取到组件总,将不同暴露为插槽
- 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容
- 是搜索框,还是文字,菜单,有调用者自己来决定
这就是我们为什么要学习组件插槽的原因
2.9.1 插槽的基本使用
插槽的基本使用非常简单:
<template id="cpn"> <div> <h2>我是组件</h2> <slot></slot> <p>我是组件的内容,哈哈哈</p> </div> </template>
|
在组件中预留<slot>
标签
<div id="app"> <cpn> <button>我是插槽中按钮</button> # 插槽中放入button </cpn> <cpn> <span>我是插槽中span</span> # 插槽中放入span </cpn> <cpn></cpn> </div>
|
插槽中通过插入不同的功能,实现不同的需求
如果插槽中有某个功能是经常重复使用的时候,可以给插槽设置默认值:
<slot><p>我是插槽中的默认内容</p></slot>
|
这样如果使用插槽时不覆盖,则显示的是默认内容
<cpn> <button>我是插槽中按钮</button> # 显示按钮 </cpn> <cpn> <span>我是插槽中span</span> # 显示span </cpn> <cpn></cpn> # 显示默认的<p>标签
|
2.9.2 具名插槽
有时候一个组件中不止一个插槽,可以通过给插槽命名的方式,来指定修改哪一个插槽的内容:
<div id="app"> <cpn> <span slot="left">左边被我改了</span> <span slot="right">右边被我改了</span> </cpn> </div>
<template id="cpn"> <div> <slot name="left"><p>我是插槽中左边的默认内容</p></slot> <slot name="center"><p>我是插槽中中间的默认内容</p></slot> <slot name="right"><p>我是插槽中右边的默认内容</p></slot> </div> </template>
|
如上,通过指定slot的name,可以修改指定插槽的内容,避免混乱。效果:
2.9.3 编译作用域
在进一步学习插槽之前,我们需要先理解一个概念:编译作用域
官方对于编译的作用域解析比较简单,我们通过一个例子来理解:
<div id="app"> <cpn v-show="isShow"></cpn> </div>
<template id="cpn"> <div> <h2>我能不能显示出来呢</h2> </div> </template>
<script src="./js/vue.js"></script> <script> <!-- 父传子使用props --> const cpn = { template: '#cpn', data() { return { isShow: false } } }
const app = new Vue({ el: '#app', components: { cpn }, data: { message: '你好呀', isShow: true } }) </script>
|
在Vue实例中的isShow为true,而子组件的isShow为false,那么组件最终是否会被渲染呢? 答案是最终可以被渲染出来,也就是说v-show使用的是Vue实例中的isShow属性,而非子组件中的isShow属性。
为什么呢?
官方给出了一条准则: 父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译
而我们在使用<cpn v-show="isShow"></cpn>
的时候,整个组件的使用过程相当于在父组件中出现的
那么它的作用域就是父组件,使用的属性也是父组件的属性
因此,isShow使用的是Vue实例中的属性,而不是子组件的属性
2.9.4 作用域插槽
作用域插槽是slot一个比较难以理解的点 ,官方文档说的也不十分清晰
这里,我们用一句话对其做一个总结,然后我们在后续的案例中来体会:
我们先提一个需求:
- 子组件中包括一组数据,比如:[‘JavaScript’, ‘Python’, ‘Go’, ‘Java’]
- 需要在多个界面展示:
- 某些界面是以水平方向展示的
- 某些界面是以列表形式展示的
- 某些界面直接展示一个数组
内容在子组件,希望父组件告诉我们如何展示,怎么办呢
<div id="app"> <cpn></cpn> <cpn> <template slot-scope="slot"> <span>{{slot.data.join(' - ')}}</span> </template> </cpn> </div>
<template id="cpn"> <div> <slot :data="language"> <ul> <li v-for="item in language">{{item}}</li> </ul> </slot> </div> </template>
<script src="./js/vue.js"></script> <script> const cpn = { template: '#cpn', data() { return { language: ['JavaScript', 'Python', 'Go', 'C'] } } }
|