组件基础
组件基础:
- 了解组件的概念
- 掌握组件的定义
- 了解组件的组织方式
- 掌握组件通信
组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。
通过 Element 感受组件的威力
Element 是基于 Vue 开发的一个知名的第三方组件库,它能帮助我们更加快速的构建应用。
安装:
npm i element-ui
Hello World:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
<div id="app">
<el-button @click="visible = true">按钮</el-button>
<el-dialog :visible.sync="visible" title="Hello world">
<p>欢迎使用 Element</p>
</el-dialog>
</div>
</body>
<!-- 先引入 Vue -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data: function() {
return { visible: false }
}
})
</script>
</html>
使用组件
组件的定义方式分为两种,全局定义和局部定义:
- 全局组件定义在全局,在任意组件中都可以直接使用
- 局部组件定义在组件内部,只能在当前组件使用
- 建议把通用的组件定义在全局,把不通用的组件定义在局部
全局注册
注册:
Vue.component('my-component', {
template: '<div>A custom component!</div>'
});
// 创建根实例
new Vue({
el: '#example'
});
在模板中使用组件:
<div id="example">
<my-component></my-component>
</div>
渲染结果:
<div id="example">
<div>A custom component!</div>
</div>
局部注册
你不必把每个组件都注册到全局。你可以通过某个 Vue 实例/组件的实例选项 components
注册仅在其作用域中可用的组件:
注册:
new Vue({
// ...
components: {
// <my-component> 将只在父组件模板中可用
'my-component': {
template: '<div>A custom component!</div>'
}
}
});
使用:
<div id="example">
<div>A custom component!</div>
</div>
组件的模板
- DOM 模板
- 字符串模板
- .vue 单文件组件中的
template
模板
组件的 data
必须是函数
构造 Vue 实例时传入的各种选项大多数都可以在组件里使用。只有一个例外:data 必须是函数。
Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
data: function () {
return { counter: 0 }
}
});
new Vue({
el: '#example-2'
});
组件的作用域是独立的
- 组件无法访问外部作用域成员
- 外部作用域也无法访问组件内部成员
组件组合
组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。然而,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。
在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。看看它们是怎么工作的。
组件化构建 TodoMVC
组件通信
在树形结构里面,组件之间有几种典型的关系:父子关系、兄弟关系、没有直接关系。
相应地,组件之间有以下几种典型的通讯方案:
- 直接的父子关系
- 直接的父子关系
- 没有直接关系
- 利用 cookie 和 localstorage 进行通讯。
- 利用 session 进行通讯。
- 父传子 Props Down
子通知父亲 Events Up
通过 ref 父亲直接访问子组件
- 给子组件起个 ref
- 然后在父组件中通过
this.$refs.子组件ref名
- 子组件可以在内部通过
this.$parent
直接访问父组件 - 非父子关系
- 事件通信 Events Bus
- Global Bus
- 集中式状态管理 Vuex
父子组件通信:Props Down
1. 在父组件中通过子组件标签属性传递数据
<child message="hello!"></child>
2. 在子组件显式地用 props
选项声明它预期的数据并使用
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 也可以在模板中使用
// 同样也可以在 vm 实例中通过 this.message 来使用
template: '<span>{{ message }}</span>'
});
camelCase vs. kebab-case
HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名)。
Vue.component('child', {
// 在 JavaScript 中使用 camelCase
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
});
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>
如果你使用字符串模板,则没有这些限制。
动态 Prop
与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind
来动态地将 prop
绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
你也可以使用 v-bind 的缩写语法:
<child :my-message="parentMsg"></child>
字面量语法 vs 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>
因为它是一个字面量 prop,它的值是字符串 “1” 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:
<!-- 传递真正的数值 -->
<comp v-bind:some-prop="1"></comp>
单向数据流
Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
在两种情况下,我们很容易忍不住想去修改 prop 中数据:
- Prop 作为初始值传入后,子组件想把它当作局部数据来用
- Prop 作为原始数据传入,由子组件处理成其它数据输出
对这两种情况,正确的应对方式是:
1. 定义一个局部变量,并用 prop 的值初始化它:
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
2. 定义一个计算属性,处理 prop 的值并返回:
// ...
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
},
!> 注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
Prop 验证
我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。
要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:
Vue.component('example', {
props: {
// 基础类型检测 (`null` 指允许任何类型)
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 value > 10
}
}
}
});
type
可以是下面原生构造器:
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
type 也可以是一个自定义构造器函数,使用 instanceof 检测。
当 prop 验证失败,Vue 会抛出警告 (如果使用的是开发版本)。
注意 prop 会在组件实例创建之前进行校验,所以在 default 或 validator 函数里,诸如 data、computed 或 methods 等实例属性还无法使用。
父子组件通信:Events Up
我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。
1. 在子组件中调用 $emit()
方法发布一个事件
Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
// 发布一个名字叫 increment 的事件
this.$emit('increment')
}
},
});
2. 在父组件中提供一个子组件内部发布的事件处理函数
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
});
3. 在使用子组件的模板的标签上订阅子组件内部发布的事件
<div id="counter-event-example">
<p>{{ total }}</p>
<!--
订阅子组件内部发布的 increment 事件
当子组件内部 $commit('increment') 发布的时候,就会调用到父组件中的 incrementTotal 方法
-->
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
给组件绑定原生事件
有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 v-on
的修饰符 .native
。例如:
<my-component v-on:click.native="doTheThing"></my-component>
.sync
修饰符
在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。
就是当一个子组件改变了一个带 .sync 的 prop 的值时,这个变化也会同步到父组件中所绑定的值。
在使用子组件的时候加上 .sync
修饰符:
<comp :foo.sync="bar"></comp>
在子组件内部更新 foo
的值时,显示的触发一个更新事件:
this.$emit('update:foo', newValue);
非父子组件通信:Event Bus
有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:
var bus = new Vue();
// 触发组件 A 中的事件
bus.$emit('id-selected', 1);
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
});
专业组件通信大杀器:Vuex
在复杂的情况下,我们应该考虑使用专门的 状态管理模式。