什么是组件化
-
将一个复杂的问题, 拆解为很多个可以处理的小问题, 再将其放到整体当中,大问题就可以迎刃而解 其实就是 动态规划问题
-
组件化也是类似的思想
- 如果将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展, 不利于维护
- 如果将一个页面拆分成一个个效地功能块, 每个功能块完成属于自己的部分的独立功能, 那么之后整个页面的管理和维护就变得非常容易了
-
组件化是Vue.js中的重要思想, 它提供了一种抽象, 让我们开发出一个个独立的可以复用的小组件来组成我们的页面
-
任何的应用都可以被抽象成一个组件树
-
组件化思想的应用
- 有了组件化的思想, 在以后的开发中就要充分地利用组件化
- 尽可能地将页面拆分成一个个小的, 可以复用的组件
- 这样可以方便代码的管理和维护,代码的扩展性也会大大增强
注册组件的基本步骤
-
组件的使用分成三个步骤
- 创建组件构造器
- 调用Vue.extend()方法创建组件构造器
- 注册组件
- 调用Vue.component()方法注册组件
- 使用组件
- 在Vue实例的作用范围内使用组件
- 创建组件构造器
-
注意: 前两个步骤代码的编写要写在 创建 new Vue() 之前
注册组件步骤解析
-
- Vue.extend()
- 调用Vue.extend() 创建的是一个组件构造器
- 在创建组件构造器的时候, 传入template 代表自定义组件的模板
- 改模板就是在使用到组件的地方, 要显示的html代码
- 事实上,这种写法在Vue2.x的文档中几乎看不到了,都是使用语法糖
-
- Vue.component()
- 调用Vue.component() 是将创建的组件构造器注册为一个组件, 并给它一个标签名称
- Vue.component() 需要传递两个参数: 1. 注册组件的标签名 2. 组件构造器
-
- 组件必须挂载在某个Vue实例中, 否则不会生效
<div id="app">
<!-- 3. 使用组件 -->
<my-cpn></my-cpn>
</div>
<script>
// 1. 创建组件构造器
const cpn = Vue.extend({
template: `
<div>
<h1>我是标题</h1>
<p>我是内容, 你好你好</p>
<p>我是内容, 你好你好</p>
<p>我是内容, 你好你好</p>
</div>
`
})
// 2. 注册组件
Vue.component('my-cpn', cpn)
// 以上两步要写在 new Vue实例之前
const app = new Vue({
el: "#app",
data: {}
});
</script>
全局组件和局部组件 (开发中常用的是局部组件)
- 全局组件就是在全局注册的组件, 全局组件可以在多个Vue实例中使用
- 局部组件就是在Vue实例对象中注册的组件, 局部组件只能在当前的Vue实例下面使用, 在其他的Vue实例下使用会报错
- 局部组件是在Vue实例下的一个叫 components 的 option下注册的, components值是一个对象那个, 里面写入 组件名(就是在html中使用的标签名) : 组件构造器 即可注册组件
- 注意, 组件构造器和全局注册组件的代码要写在 创建Vue实例的代码之前
<div id="app1">
<mycpn1></mycpn1>
<mycpn2></mycpn2>
</div>
<div id="app2">
<mycpn1></mycpn1>
<!-- 局部的组件 在这里无效 而且会报错-->
<!-- <mycpn2></mycpn2> -->
</div>
<script>
// 1. 创建组件构造器
const cpn1 = Vue.extend({
template: `
<div>
<h2>我是标题111</h2>
<p>我是内容111, 你好你好</p>
</div>`
})
const cpn2 = Vue.extend({
template: `
<div>
<h2>我是标题222</h2>
<p>我是内容222, 你好你好</p>
</div>`
})
// 2. 注册组件 (全局组件)
// 全局组件可以在多个Vue实例下使用
Vue.component('mycpn1', cpn1)
const app1 = new Vue({
el: "#app1",
// 在实例化的Vue中注册组件就是 局部组件, 局部组件只能在当前的Vue实例中使用, 不能再其他的Vue实例中使用
// 在 components option中定义局部组件
components: {
mycpn2: cpn2 // 组件名(就是在html中使用的标签名) : 组件构造器
}
})
const app2 = new Vue({
el: "#app2"
})
</script>
Vue组件的父组件和子组件
- 子组件的注册组件代码可以写在父组件的组件构造器的名为components的option中, 子组件的注册完成后, 子组件的标签名就可以写入父组件的template中
- 简单来说, 就是组件在哪里注册就只能在哪里使用, 浏览器解析组件标签的时候, 会先从当前的组件构造器中的components中查找是否有注册该标签名, 如果找到了就开始渲染, 如果没有找到就在全局的Vue.component中查找, 如果找到就渲染, 如果都没有找到的话就会报错
- 注意, 子组件的组件构造器要写在 父组件的组件构造器之前, 否则按照浏览器解析代码的顺序, 会找不到子组件, 就会报错
<div id="app">
<fathercpn></fathercpn>
</div>
<script>
// 组件1
const son = Vue.extend({
template: `<div>
<h2>我是子标题</h2>
<p>我是子内容,呵呵呵呵</p>
</div>`
})
// 组件2
// 注意, 子组件的组件构造器要写在父组件的组件构造器之前
const father = Vue.extend({
template: `<div>
<h2>我是父标题</h2>
<p>我是父内容,哈哈哈哈</p>
在这里使用子组件
<soncpn></soncpn>
</div>`,
// 子组件可以在父组件的内部注册
// 注册后就可以在父组件的template中使用
components: {
soncpn: son
}
})
// 这个app可以看作是root组件(根组件)
const app = new Vue({
el: "#app",
components: {
fathercpn: father
}
})
</script>
注册组件的语法糖
- 传统注册组件的方式, 有些繁琐
- Vue为了见过这个过程, 提供了注册的语法糖
- 主要是省去了调用Vue.extend()的这个步骤, 可以将一个对象当作参数传入component中
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
</div>
<script>
// 注册组件的语法糖
// 1. 全局注册组件的语法糖
// 将组件构造器中调用Vue.extend方法的步骤省略, 而是直接将一个对象代替传入Vue.component中
Vue.component('cpn1', {
template: `<div>
<h2>我是标题111</h2>
<p>我是内容111, 你好你好</p>
</div>`
})
const app = new Vue({
el: "#app",
components: {
// 2. 局部注册组件的语法糖
// 跟全局注册组件的语法糖差不多, 也是直接将一个对象传入
cpn2: {
template: `<div>
<h2>我是标题222</h2>
<p>我是内容222, 你好你好</p>
</div>`
}
}
})
</script>
组件模板的分离写法
- template模板如果写在js代码中不优雅不好看
- 将其中的HTML代码分离出来,然后挂载到对应的组件上
- Vue提供了两种方案来定义HTML模块内容
- script标签
- 注意script标签需要定义type类型为 text/x-template
- 并且定义id属性来绑定组件的标签名
- template标签
- 定义id属性来绑定组件的标签名
- script标签
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
</div>
<!-- 组件模板的分离写法 -->
<!-- 1. script标签写法 -->
<script type="text/x-template" id="cpn1">
<div>
<h2>我是标题111</h2>
<p>我是内容,哈哈哈哈哈</p>
</div>
</script>
<!-- 2. template标签写法 -->
<template id="cpn2">
<div>
<h2>我是标题222</h2>
<p>我是内容,呵呵呵呵呵</p>
</div>
</template>
<script>
// 将定义的模板的id绑定到标签名中
Vue.component('cpn1', {
template: "#cpn1"
})
Vue.component('cpn2', {
template: "#cpn2"
})
const app = new Vue({
el: "#app"
})
</script>
组件中的数据存放问题
- 组件可以访问Vue中的数据吗?
- 不可以
- 组件是一个单独功能模块的封装
- 这个模块有属于自己的HTML模板, 也会有属于自己的数据data
- 组件中的数据是保存在自身的一个data属性中的(组件也可以有自己的methods等等option,以后会用到)
- 这个data属性必须是一个函数
- 函数返回一个对象, 对象内部保存数据
<div id="app">
<cpn1></cpn1>
</div>
<template id="cpn1">
<div>
<h2>我是标题</h2>
<!-- 组件保存的数据也是用mustache语法引用 -->
<p>我是内容,{{message}}</p>
</div>
</template>
<script>
Vue.component('cpn1', {
template: "#cpn1",
// 组件中的数据要存放在组件的注册的data中, 而且这个data必须是一个函数, 函数返回一个对象, 对象内写入数据
data() {
// 返回一个对象
return {
// 对象内写入数据
message: "你好你好"
}
}
})
const app = new Vue({
el: "#app"
})
</script>
组件中的data为什么一定要是一个对象
- 就是为了防止多次引用组件的时候, 组件和组件之间共用一个data, 造成变量泄露
- Vue已经考虑了这个问题, 所以这里必须写函数, 利用函数的作用域, 成为一个闭包, 防止变量泄露
<body>
<div id="app">
<counter></counter>
<counter></counter>
<counter></counter>
</div>
<template id="counter">
<div>
<h2>当前计数: {{num}}</h2>
<button @click="decrement">-</button>
<button @click="increment">+</button>
</div>
</template>
<script>
// 1. 注册组件
Vue.component('counter', {
template: "#counter",
// 这里为什么要是一个函数?
// 就是为了防止多次引用组件的时候, 组件和组件之间共用一个data, 造成变量泄露
// Vue已经考虑了这个问题, 所以这里必须写函数, 利用函数的作用域, 成为一个闭包, 防止变量泄露
data() {
return {
num: 0
}
},
methods: {
increment() {
this.num++
},
decrement() {
this.num--
}
}
})
const app = new Vue({
el: "#app"
})
</script>
组件通讯-父组件向子组件传递数据
- 在Vue中子组件是不可以直接引用父组件或者Vue实例中的数据的
- 但是在开发中, 往往一些数据需要从上层传递到下层
- 比如在一个页面中, 从服务器请求到很多数据, 包括大组件的数据和小组件的数据, 他们都存储在大组件的data中
- 其中一部分数据, 并非是整个页面的大组件来展示的, 而是通过子组件来展示
- 这时, 并不会让子组件再次发送一个网络请求, 这样会大大加大服务器的压力的, 此时会让大组件(父组件)将苏剧传递给小组件(子组件)
- 父组件向子组件传递数据的方法
- 通过props向子组件传递数据
- props的写法也分有数组写法和对象写法 (我们一般用对象写法)
- 对象的写法可以设置 传入的数据类型, 是否必须传入, 默认值等等
- 对象的写法中, 如果type的值为0的时候, 则可以传入任何类型的值
- 通过props向子组件传递数据
<div id="app">
<!-- 传递数据要子组件的标签中用 v-bind 链接数据 -->
<cpn :sonmessage="message" :sonmovies="movies"></cpn>
</div>
<template id="cpn">
<div>
<!-- 在通过子组件标签链接数据后, 就可以在子组件的html模板中用mustache语法使用数据 -->
<h2>{{sonmessage}}</h2>
<ul>
<li v-for="item in sonmovies">{{item}}</li>
</ul>
</div>
</template>
<script>
// 这里是子组件
const cpn = {
template: "#cpn",
// 子组件向父组件拿数据, 用props
// 这里用数组形式, 数组里面传入新的数据名
// props: ["sonmessage", "sonmovies"]
// 这里使用对象形式, 对象形式可以设置传入的数据的类型, 默认值, 是否必须传入等等
props: {
// 简单地设置数据类型
/* sonmessage : String,
sonmovies: Array */
// 传入一个对象可以有更多的操作
sonmessage: {
// 设置数据的类型
type: String,
// 设置是否必须传入
required: true, // true表示必须传入, false表示不是必须传入
// 设置默认值
default: "你好我是默认值"
},
sonmovies: {
type: Array,
// 注意如果传入的数据类型是数组或者对象的话, 设置默认值default必须是一个函数, 函数返回一个默认的数据
default () {
return ["我是默认的数据", "我也是默认的数据"]
}
}
}
}
const app = new Vue({
el: "#app",
data: {
// 这里是父组件的数据
message: "你好你好",
movies: ["海王", "海贼王", "海尔兄弟"]
},
components : {
cpn
}
})
</script>
父组件向子组件传递数据时props中的驼峰标识
- 因为HTML代码时不区分大小写的, 所以不能使用驼峰命名法, 而JS代码时严格区分大小写的, 所以就会导致这个问题
- 记住在HTML代码中用短线命名, 在JS代码中使用驼峰命名 就OK啦~
<div id="app">
<!-- 注意这里不能使用驼峰命名法了, 要将props中的驼峰命名的转换为短线命名 -->
<cpn :son-message="message" :son-person="person"></cpn>
</div>
<template id="cpn">
<div>
<!-- 这里使用的数据, 要与props中的数据名一致 -->
<h2>{{sonMessage}}</h2>
<p>{{sonPerson}}</p>
</div>
</template>
<script>
const cpn = {
template: "#cpn",
props: {
sonMessage: {
type: String,
default: "你好你好"
},
sonPerson: {
type: Object,
default () {
return {}
}
}
}
}
const app = new Vue({
el: "#app",
data: {
message: "我是传入的数据",
person: {
name: "xiaoLam",
age: 22
}
},
components: {
cpn
}
})
</script>
子组件向父组件传递数据
- 需要使用自定义事件传递
- 什么时候需要自定义事件呢?
- 当子组件需要向父组件传递数据的时候, 就要用到自定义事件了
- v-on不仅仅可以监听DOM事件, 也可以用域监听组件间的自定义事件
- 自定义事件的流程
- 在子组件中, 通过$emit()来发射数据
- $emit() 中有两个参数, 第一个参数是自定义事件的名字, 第二个参数是需要发送的数据
- 在父组件中, 通过v-on来监听自定义事件, 接收数据
- 在子组件中, 通过$emit()来发射数据
<!-- 需求: 计数器 -->
<!-- 操作在子组件中完成 -->
<!-- 展示交给父组件完成 -->
<!-- 父组件模版 -->
<div id="app">
<p>{{num}}</p>
<!-- 在这里用v-on监听接收子组件发射数据的自定义事件 -->
<btn @num-de="decreNum" @num-in="increNum"></btn>
</div>
<!-- 子组件模板 -->
<template id="btn">
<div>
<!-- 设置点击后触发发射数据事件 -->
<button @click="decrement">-</button>
<button @click="increment">+</button>
</div>
</template>
<script>
// 子组件模块
const btn = {
template: "#btn",
methods: {
// 用$emit()发射数据
decrement() {
this.$emit("num-de")
},
increment() {
this.$emit("num-in")
}
}
}
// 父组件模块
const app = new Vue({
el: "#app",
data: {
num: 0
},
components: {
btn
},
methods: {
// 处理接收来的数据
increNum() {
this.num++
},
decreNum() {
this.num--
}
}
})
</script>
子组件接收来自父组件的数据, 通过子组件修改父组件的数据案例
- 注意!!! 子组件不要直接修改props中来自父组件的数据, 会报错的
- 正确做法是, 子组件修改子组件中data的数据, 然后将data中的数据通过自定义事件发送给父组件, 父组件处理接收的数据, 修改父组件自身的数据
- 以下的代码请注意看注释
- 以下代码实现的过程为, 从父组件通过props向子组件发送数据, 子组件接收数据, 子组件设置自己的data数据, 子组件修改自己的data数据, 子组件通过自定义事件向父组件发送自己的data数据, 父组件接收数据, 父组件修改自己的data数据, 同步修改了props向子组件发送的数据
<body>
<!-- 根组件模板 -->
<div id="app">
<son :sonnum1="num1" :sonnum2="num2" @parchangenum1="parentnum1" @parchangenum2="parentnum2"></son>
</div>
<!-- 子组件模板 -->
<template id="son">
<div>
<!-- 注意嗷!!! 子组件里不要直接修改props中来自父组件的数据 -->
<!-- <input type="number" v-model="num1"> -->
<input type="number" :value="num1" @input="changenum1">
<!-- props 是接收的来自父组件的数据 -->
<h2>props : {{sonnum1}}</h2>
<!-- data 是子组件自身的数据 -->
<h2>data : {{num1}}</h2>
<!-- <input type="number" v-model="num2"> -->
<input type="number" :value="num2" @input="changenum2">
<h2>props : {{sonnum2}}</h2>
<h2>data : {{num2}}</h2>
</div>
</template>
<script>
const son = {
template: "#son",
// 接收来自根组件的数据
props: {
sonnum1: {
// 在做这个案例的时候, 发现了一个很有趣的现象, 如果将这里的type的值设置为 0 , 那么这个sonnum1 就可以接收任何类型的数据
type: Number
},
sonnum2: {
type: Number
}
},
// 子组件的data必须是一个函数, 防止变量泄露
data() {
return {
num1: this.sonnum1,
num2: this.sonnum2
}
},
methods: {
changenum1(event) {
// 处理num1, 然后向根组件发送
this.num1 = event.target.value;
this.$emit("parchangenum1", this.num1);
// 处理num2, 然后向根组件发送
this.num2 = this.num1 * 100;
this.$emit("parchangenum2", this.num2);
},
changenum2(event) {
this.num2 = event.target.value;
this.$emit("parchangenum2", this.num2)
this.num1 = this.num2 / 100;
this.$emit("parchangenum1", this.num1)
}
}
}
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
components: {
son
},
methods: {
// 父组件处理从子组件接收来的数据
parentnum1(num) {
this.num1 = parseFloat(num)
},
parentnum2(num) {
this.num2 = parseFloat(num)
}
}
})
</script>
</body>
组件访问-父组件访问子组件, 通过$children 和 $refs
- $children 是一个数组, 里面包含的的是父组件中包含的所有子组件
- 想要通过children访问某一个特定的子组件只能通过数组的下标来访问(这样的方法很不灵活), 所以children访问子组件的方法很少用
- $refs 是一个对象, 对象里面包含的是在父组件中 有ref属性注册的子组件, 没有ref属性的子组件不会被包含在内; 如果没有子组件设置ref属性, $refs就是一个空对象
- 想要通过refs访问某一个特定的子组件, 需要在子组件的ref属性设置值, 通过这个设置值来访问这个子组件
<body>
<div id="app">
<son ref="son1"></son>
<son></son>
<son></son>
<!-- 通过这个按钮来触发事件 -->
<button @click="btnClick">按钮</button>
</div>
<template id="son">
<div>
<h2>我是子组件</h2>
</div>
</template>
<script>
const son = {
template: "#son",
data() {
return {
name: "子组件数据"
}
},
methods: {
showMessage() {
console.log("子组件方法");
}
}
}
const app = new Vue({
el: "#app",
components: {
son
},
methods: {
btnClick() {
// 1. 通过children获取所有的子组件
// 获取的是所有的子组件组成的一个数组
// 想要获取某个特定的子组件, 只能使用数组的下标获取(这样很不灵活, 所以使用children获取子组件的方法很少用)
/* console.log(this.$children);
this.$children[0].showMessage(); // 通过$children 使用某个下标的子组件的方法
console.log(this.$children[0].name); // 通过$children 获取某个下标的子组件的数据 */
// 2. 通过$refs 获取某个特定的子组件
console.log(this.$refs); // $refs 获得的是一个对象, 里面包含通过标签属性ref注册的子组件 可以通过属性ref的值来获取特定的一个子组件
this.$refs.son1.showMessage(); //通过refs调用ref值为son1的子组件的方法
console.log(this.$refs.son1.name); // 通过refs获取ref值为son1的子组件中的数据
}
}
})
</script>
</body>
组件访问-子组件访问父组件, 通过$parent 和 $root
- $parent 可以访问当前子组件上一级的父组件
- $root 可以访问当前子组件的最上级根组件
- 这两个方法不是很常用, 因为Vue最大的优点是能够组件化分离, 如果组件用了$parent 或者 $root 方法的话就大大地减低了Vue的组件性
<body>
<div id="app">
<son></son>
</div>
<template id="son">
<div>
<h2>我是son组件</h2>
<button @click="sonBtnClick">我是son按钮</button>
<sonchild></sonchild>
</div>
</template>
<template id="sonchild">
<div>
<h2>我是sonchild组件</h2>
<button @click="sonChildBtnClick">我是sonchild按钮</button>
</div>
</template>
<script>
const app = new Vue({
el: "#app",
data: {
message: "我是根组件的数据"
},
components: {
son: {
template: "#son",
data() {
return {
message: "我是son组件的数据"
}
},
methods: {
sonBtnClick() {
// 通过parent访问父组件
console.log(this.$parent);
// 通过parent访问父组件的数据
console.log(this.$parent.message);
}
},
components: {
sonchild: {
template: "#sonchild",
methods: {
sonChildBtnClick() {
// 通过 $root 访问根组件
console.log(this.$root);
console.log(this.$root.message);
// 通过$parent 访问父组件
console.log(this.$parent);
console.log(this.$parent.message);
}
}
}
}
}
}
})
</script>
</body>