Vue.js 2.0 学习笔记(三)组件化开发
一、组件化开发
组件化开发:根据封装的思想,把页面上可复用的 UI 解构封装为组件,方便项目的开发和维护
组件化开发的思想:将复杂的问题拆分成很多小问题。若我们将页面中所有的逻辑处理全部放在一起,处理起来会非常复杂,且不利于后续的管理和扩展。若我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护都会变得容易
1.1 vue中的组件中的三个组成部分
每个.vue组件都由 3 部分构成,分别是:
- template:组件的模板结构,注意:在template中只能有一个根节点
- script:组件的 JavaScript行为
- style:组件的样式
注意:.vue组件 中 script 里面的 data 节点不能指向对象,组件里面的 data 必须是个函数
1.2 组件中的 methods 方法中的 this
要点:在组件中,this就是当前组件的实例对象
1.3 组件使用的基本步骤
组件的使用的三个步骤:(Vue 2.x开始实际上就不用,只是是基础)
- 创建组件构造器:调用Vue.extend()
- 注册组件:调用Vue.component()
- 使用组件:在Vue实例作用范围内使用
(1)Vue.extend()
- 调用Vue.extend()创建的是一个组件构造器
- 通常在创建构造器时,传入的template代表我们自定义组件的模板,该模板就是在使用组件的地方,要显示的HTML代码
(2)Vue.component()
- 将刚才的组件构造器注册为一个组件,并给他起一个组件的标签名称
- 传递的参数:注册组件的标签名和组件构造器
(3)使用组件
组件必须挂载在某个Vue实例下,否则起不生效
如今使用组件的三个步骤:(现在主流使用的)
-
导入组件:使用 import 导入需要的组件
import Login from '@/components/Login.vue'
-
注册组件:使用 components 节点注册组件
export default{ components:{ //右边的Login 对应import 中的组件名,左边的字符串对应使用便签时的标签名 //'Login': Login Login //当key 和 value的值相同时,可以简写 } }
-
使用组件:以标签形式在Vue实例作用范围内使用
<Login></Login>
1.4 全局组件和局部组件
// 注册组件(全局组件,意味着可以在多个Vue实例下使用)
import cpnC from '@/components/cpnC.vue'
// 参数1:字符串格式,表示组件的“注册名称”
// 参数2:需要被全局注册的那个组件
Vue.component('cpn',cpnC); //全局组件
const app = new Vue({
el:'#app',
data: {
message:'你好'
},
components: {
//cpn使用组件时的标签名
cpn: cpnC //局部组件
}
})
1.5 父组件和子组件
// 创建第一个组件构造器(子组件)
const cpnC1 = Vue.extend({
template:`
<div>
<h2>xxxx</h2>
<p>hhhh</p>
</div>
`
});
// 创建第二个组件构造器(父组件)
const cpnC2 = Vue.extend({
template:`
<div>
<h2>xxxx</h2>
<p>hhhh</p>
<cpn1></cpn1>
</div>
`,
components: {
cpn1: cpnC1
}
});
// root组件
const app = new Vue({
el:"#app",
data: {
message: '你好啊'
},
components: {
cpn2: cpnC2
}
})
1.6 语法糖(重要,之前的写法现在不用了)
1.6.1 注册全局组件
Vue.component('cpn1',{
template:`
<div>
<h2>xxxx</h2>
<p>hhhh</p>
</div>
`
});
注意:cpn1用单引号或双引号包裹的
1.6.2 注册局部组件
const app = new Vue({
el:"#app",
data: {
message: '你好啊'
},
components: {
'cpn2': {
template:`
<div>
<h2>xxxx</h2>
<p>hhhh</p>
</div>
`
}
}
})
注意:cpn2用单引号或双引号包裹的
1.7 组件模板的分离写法
<!-- 1.script标签,注意类型必须是text/x-template -->
<script type="text/x-template" id="cpn">
<div>
<h2>xxxx</h2>
<p>hhhh</p>
</div>
</script>
// 注册全局组件
<script>
Vue.component('cpn', {
template: '#cpn'
})
</script>
1.8 组件访问vue的数据
- 组件是一个单独功能模块的封装,这个模块有个属于自己的HTML,也应该有属性自己的数据data
- 组件不能直接访问Vue实例中的data
1.8.1 组件自己数据的存放
- 组件对象也有data属性(也可以有methods等属性),但该data属性必须是一个函数,即data(),原因:函数返回自己的对象,相互之间不会互相影响。
- 该函数返回一个对象,对象内部保存着数据
<div id="app">
<my-cpn></my-cpn>
</div>
<template id="myCpn">
<div>消息:{
{message}}</div>
</template>
<script>
const app = new Vue({
el:"#app",
components: {
'my-cpn': {
template: "#myCpn",
data() {
return {
message: 'hello world'
}
}
}
}
})
</script>
1.9 组件间的样式冲突问题
默认情况下,卸载.vue组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题,导致样式冲突的根本原因:
- 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的index.html 页面进行呈现的
- 每个组件中的样式,都会影响整个index.html 页面中的 DOM 元素
解决方案:在 style 节点中 使用 scoped 属性
1.9.1 scoped的底层原理
当在.vue 的 style节点中加上 scoped 属性后,样式局部化,只在当前的.vue文件中生效,不会产生冲突,底层原理是使用 scoped 后,vue为该文件中的每个html 标签都添加了 data-v 的自定义属性,以便在样式中使用属性选择器来解决样式冲突
1.9.2 使用deep修改自组件的样式
作用:能够在父组件中改变子组件的样式
应用场景:当使用第三方组件库(如:vant)时,如果有修改第三方组件库默认样式的需求,需要用到 /deep/
/deep/ h5 {
color: pink;
}
1.10 vue组件的实例对象
浏览器是不认识.vue 文件的,需要依赖 packa.json -> devDependencies 中的 vue-template-compiler 编译解析为 js 文件在传给浏览器执行
二、组件之间的数据共享
2.1 父子组件之间的通信
背景:我们知道组件实例的作用域是孤立的。这意味着不能(也不应该) 在子组件的模板内直接引用父组件的数据或是Vue实例数据的。
但在开发中,往往会有一些数据需要从上层传递到下层的,如:在页面中,从服务器请求到了很多数据,其中一部分数据并非是整个大组件来展示的,而是需要下面的子组件来展示。这时并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
2.1.1 父子组件通信的方法
- 父组件通过 props 向子组件传递数据
- 子组件通过事件 events 向父组件发送信息
- 总结: props down, events up
2.1.2 props基本用法(父组件向子组件传递信息)
props 是组件的自定义属性,在封装通用组件时,合理的使用 props 可以极大的提高组件的复用性,语法格式如下:
export default {
//组件自定义属性
props: ['自定义属性a','自定义属性b'...],
//组件的私有属性
data() {
return{
}
}
}
父组件向子组件传递数据分为两种方式:动态和静态
1. 静态props
子组件要显式地用 props
选项声明它期待获得的数据,静态 Props 通过为子组件在父组件中的占位符添加特性的方式来达到传值的目的
<div id="app">
<child-cpn :message="message"></child-cpn>
</div>
<template id="childCpn">
<div>消息:{
{message}}</div>
</template>
<script>
const app = new Vue({
el:"#app",
data: {
message: 'hello'
},
components: {
'child-cpn': {
template: "#childCpn",
props: ['message']
}
}
})
</script>
props传递的过程:
- Vue实例中初始化data
- 子组件中初始化props
<child-cpn :message="message"></child-cpn>
通过:message="message"将data中的数据传给了props(双引号中的message才是绑定的数据变量)- 将props中的数据显示在子组件中
2. 动态props
在模板中,要动态地绑定父组件的数据到子模板的 props,与绑定到任何普通的HTML特性相类似,就是用 v-bind
。 每当父组件的数据变化时,该变化也会传导给子组件
3. props值的两种表达形式
- 字符串数组:数组中的字符串就是传递时的名称,无法设置默认值
const cpn = {
template: '#cpn',
props:['cmovies','cmessage']
}
- 对象:对象可以设置传递时的类型,也可以设置默认值等
const cpn = {
template: '#cpn',
props: {
// 1.类型限制
// cmovies: Array,
// cmessage: String
//2.或提供一些默认值
cmessage: {
type: String,
default: 'hhhhh',
required: true //意味着必须传值,否则报错
}
// 类型是对象或数组时,默认值必须是一个函数
cmovies: {
type: Array,
default() {
return []
}
}
}
}
要点:
- props的值是只读的,不要直接修改 props 中的值,会报错
- props 中的数据,可以直接在模板结构中使用
4. props中的命名约定
- 对于props声明的属性来说,在父级HTML模板中,属性名需要使用中划线写法,不支持驼峰
var parentNode = {
template: `
<div class="parent">
<child my-message="aaa"></child>
<child :my-message="message"></child>
</div>`,
components: {
'child': childNode
}
};
- 子级props属性声明时,使用小驼峰或者中划线写法都可以;而子级模板使用从父级传来的变量时,需要使用对应的小驼峰写法
var childNode = {
template: '<div>{
{myMessage}}</div>', //子级模板使用从父级传来的变量:使用小驼峰写法
props:['myMessage'] // 子级props属性声明时,使用小驼峰或者中划线写法都可以
// 或者 props:['my-message']
}
5. props验证
对 props 进行类型等验证时,需要用对象写法
验证支持的数据类型有:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
当有自定义的构造函数时,也支持自定义的类型
Vue.component('my-component', {
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 ['success','warning','danger'].indexOf(value) !== -1;
}
}
}
})
2.1.3 子传父
方法:通过自定义事件来完成
自定义事件的流程:
- 在子组件中,通过 $emit() 来触发事件
- 在父组件中,通过 v-on (@)来监听子组件事件
<!-- 父组件模板 -->
<div id="app">
<cpn @itemClick="cpnClick"></cpn> // 监听到子组件事件:itemClick后触发cpnClick事件
</div>
<!-- 子组件模板 -->
<template id="childCpn">
<div>
<button v-for="item in cactegories"
@click="btnClick(item)">
{
{item.name}}
</button>
</div>
</template>
<script>
// 子组件
const cpn = {
template: '#cpn',
data() {
return {
categories:[
{
id: 'aaa', name: '1'},
{
id: 'bbb', name: '2'},
{
id: 'ccc', name: '3'},
{
id: 'ddd', name: '4'}
]
}
},
methods: {
// 子组件的自定义事件
btnClick(item) {
// 发射事件
this.$emit('item-click',item)
}
}
}
// 父组件
const app = new Vue({
el:"#app",
data: {
message: 'hello'
},
components: {
cpn
},
methods: {
//父组件接收事件
cpnClick(item) {
console.log('cpnClick');
}
}
})
</script>
案例:实现两个按钮+1和-1,点击后修改counter。操作过程在子组件中完成,展示交给父组件,就需要把子组件中的counter传给父组件的某个属性,如total
<div id="app">
<cpn :number1="num1"
:number2="num2"
@num1change="num1change"
@num2change="num2change">
</cpn>
</div>
<!-- 子组件模板 -->
<template id="cpn">
<div>
<h2>props:{
{number1}}</h2>
<h2>data:{
{dnumber1}}</h2>
<input type="text" v-model="dnumber1">
<h2>props:{
{number2}}</h2>
<h2>data:{
{dnumber2}}</h2>
<input type="text" v-model="dnumber2">
</div>
</template>
<script>
// 子组件
const cpn = {
template: '#cpn',
data() {
return {
dnumber1:this.number1,
dnumber2:this.number2
}
},
methods: {
// 子组件的自定义事件
num1Input(event) {
//将input中的value赋值到dnumber中
this.dnumber1 = event.target.value;
this.$emit('num1change',this.dnumber1)
this.dnumber2 = this.dnumber1 *100;
this.$emit('num2change',this.dnumber2)
}
num2Input(event) {
//将input中的value赋值到dnumber中
this.dnumber2 = event.target.value;
this.$emit('num2change',this.dnumber2)
this.dnumber2 = this.dnumber2 /100;
this.$emit('num1change',this.dnumber1)
}
}
}
// 或者用watch监听属性的变化
const cpn = {
template: '#cpn',
data() {
return {
dnumber1:this.number1,
dnumber2:this.number2
}
},
watch: {
dnumber1(newvalue) {
this.dnumber2 = newvalue * 100;
this.$emit('num1change',newvalue);
},
dnumber2(newvalue) {
this.dnumber1 = newvalue / 100;
this.$emit('num2change',newvalue);
}
}
}
// 父组件
const app = new Vue({
el:"#app",
data: {
num1:1,
num2:0
},
components: {
cpn: {
template: '#cpn',
props: {
number1: Number,
number2: Number
}
}
},
methods: {
num1change(value) {
this.num1 = parseFloat(value)
},
num2change(value) {
this.num2 = parseFloat(value)
}
}
})
</script>
注意:不要直接去绑定num1,num2 来改变值,改变数据时写一个值
2.2 父子组件的访问方式
- 父组件访问子组件:使用
$children
或$refs
- 子组件访问父组件:使用
$parent
(访问根组件时可用$root
)
2.2.1 父组件访问子组件
this.$children 是一个数组类型,包含所有子组件对象
2.2.2 子组件访问父组件
<template id="cpn">
<div>
<h2>hhh</h2>
<button @click="btnClick"></button>
</div>
</template>
<script>
const app = new Vue({
el:"#app",
data:{
message:'hhh'
}
components: {
cpn: {
template: '#cpn',
data() {
return {
name:'我是cpn组件的name'
}
},
components: {
ccpn: {
template: "#ccpn",
methods: {
btnClick() {
//1. 访问父组件$parent
console.log(this.$parent);
console.log(this.$parent.name);
//2. 访问根组件$root
console.log(this.$root);
console.log(this.$root.message);
}
}
}
}
}
}
})
</script>
2.3 兄弟组件之间的通信
vue 2.x 中,兄弟组件之间数据共享的方案是:eventBus
eventBus使用步骤:
-
创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象
-
在数据发送方,调用 bus.$emit(‘事件名称’,要发送的数据) 方法触发自定义事件
-
在数据接收方,调用 bus.$on(’事件名称‘,事件处理函数)方法注册一个自定义事件
三、组件的生命周期和生命周期函数
3.1 生命周期和生命周期函数的概念
可参考笔记(二)中的生命周期
- 生命周期:指一个组件从 创建 -> 运行 -> 销毁 的整个阶段,强调一个时间段
- 生命周期函数:是由vue框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行,强调时间点
四、ref 引用
背景:在vue中,不需要操作DOM是数据驱动视图,若在 vue 项目中,需要操作DOM 了,需要拿到页面上某个 DOM 元素的引用,该怎么办?
4.1 什么是 ref 引用
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用
每个 vue 组件实例上,都包含一个 r e f s ( refs( refs(开头的都是 vue 的内置对象)对象,里面存储着对应的 DOM 元素或组件的引用,默认情况下,组件的$refs 指向一个空对象
<template>
<div>
<h1 ref="myh1">App 根组件</h1>
<button @click="showThis"></button>
</div>
</template>
<script>
export default {
methods: {
showThis() {
this.$refs.myh1.style.color = 'red';
}
}
}
</script>
4.2 使用 ref 引用组件
<template>
<div>
<h1 ref="myh1">App 根组件</h1>
<button @click="showThis">打印this</button>
<button @click="onReset">重置 Left 组件中的 count 值为 0</button>
<div class="box">
<!-- 渲染 Left 子组件 -->
<Left ref="comLeft"></Left>
</div>
</div>
</template>
<script>
import Left from '@/components/Left.vue'
export default {
methods: {
showThis() {
this.$refs.myh1.style.color = 'red';
},
onReset() {
// resetCount() 和 count 分别是 Left 组件的方法和数据
this.$refs.comLeft.resetCount();
// this.$refs.comLeft.count = 0; (或者)
}
},
components: {
Left
}
}
</script>
4.3 this.$nextTick
作用:将代码延迟到 DOM 元素重新渲染完毕之后再执行
首先来看个案例,需求:
- 实现输入框和展示按钮的互斥切换,有输入框时不展示按钮,有按钮时不展示输入框
- 展示输入框时,自动聚焦
- 输入框失去焦点时自动隐藏,展示按钮
<template>
<div>
<input type="text" v-if="inputVisible" @blur="showButton" ref="iptRef">
<button v-else @click="showInput">展示输入框</button>
</div>
</template>
<script>
export default {
data() {
return {
inputVisible: false
}
},
methods: {
//点击按钮,展示输入框
showInput() {
this.inputVisible = true;
//让展示出来的文本框,自动获得焦点
//此时只是数据更新了,结构还未渲染,所以页面上呈现的还是按钮,也就拿不到输入框的引用
// this.$refs.iptRef.focus(); //无效的,会报错,this.$refs.iptRef为undefined
},
showButton() {
this.inputVisible = false;
}
}
}
</script>
this.$refs.iptRef.focus(); 并没有生效,还报错了,原因:此时只是数据更新了,结构还未渲染,所以页面上呈现的还是按钮,也就拿不到输入框的引用
解决以上问题的方案:利用 this.$nextTick(callback) 方法
<script>
export default {
data() {
return {
inputVisible: false
}
},
methods: {
//点击按钮,展示输入框
showInput() {
this.inputVisible = true;
// 将代码延迟到 DOM 元素重新渲染完毕之后再执行
this.$nextTick(() => {
this.$ref.iptRef.focus();
})
},
showButton() {
this.inputVisible = false;
}
}
}
</script>
注意:上面的问题不能使用 updated() 方法,原因:点击一次按钮,inputVisible 变为true 展示输入框,数据变化,触发 updated()获取焦点,再点击一次,该隐藏输入框显示按钮,但此时数据也发生了变化,触发 updated()获取焦点,但此时已经没有了输入框所以会报错