前言
为什么要有组件化、模块化
现在的网站在公司业务发展的过程中体积越来越庞大,其中堆叠了大量的业务逻辑代码,不同业务模块的代码相互调用,相互嵌套,代码之间的耦合性越来越高,调用逻辑会越来越混乱。
当某个功能需要升级的时候,往往牵一发而动全身,不管是在 DOM 上面,还是 js逻辑 层面,都会让我们需要大量的精力去兼顾旧的代码的修改以及调整,带来工作量的增加。
总的来说,大量的代码堆叠使得可读性、可维护性、不同工程师的协同非常的差,再加之业务越来越复杂,我们需要一种方式让其变得简单。
化繁为简
一个页面的繁琐,化为每个块独立的单独功能,而每个单独的块组合、嵌套,协同工作,就是组件化的思想。
我们可以把一个页面想象成一辆车,组件就是每个独立的零件,他们自身发挥自己的作用,组合起来就是一辆车。
我们可以很大程度的降低系统各个功能的耦合性,并且提高了功能内部的聚合性。这使得我们的可读性、可维护性大大的增加,耦合性的降低,也降低了我们开发的复杂性,提升了开发效率。我们就可以多点时间做自己喜欢的事情辣!
设计组件要遵循一个原则:一个组件只专注做一件事,且把这件事做好。(用钢炼的话来说:一就是全,全就是一)
下面我们就来学习 Vue 的组件化开发吧~
组件是什么
组件 跟我们平时写的 js 函数一样,具有
- 封装性
- 复用性
- 单一职责
他可以扩展HTML元素,封装可重用的HTML代码,我们可以将组件看作自定义的HTML元素。
组件注册
全局注册
- 通过
component指令直接注册一个组件 - 第一个参数为 组件名,第二个参数就是我们在
new Vue内写的 options,包括 data 等 component内暴漏一个字段template用于写html结构- 我们可以把组件当成一个 标签 引入
- 值得注意的是,我们习惯性用大驼峰命名法命名组件
- 而在 标签 的书写上,我们需要用烤肉串的命名规则(html对大小写不敏感)
<div id="app">
<component-a></component-a>
</div>
Vue.component("ComponentA", {
template: `<div>ComponentA</div>`
});
const app = new Vue({
el: '#app'
});
- 我们也可以通过字段
template引入- 这个时候我们可以直接用大驼峰命名,没有命名限制
- 使用
template亦会使得目标内的东西被template的内容取代
<div id="app">
我要被干掉了~
</div>
Vue.component("ComponentA", {
template: `<div>ComponentA</div>`
})
const app = new Vue({
el: '#app',
template: `<div>
{{msg}}
<ComponentA></ComponentA>
</div>`,
data: {
msg: 'hello world'
}
});
局部注册
- 我们可以声明一个对象,通过字段
components引入组件使用
<div id="app"></div>
const ComponentB = {
template: `<div>
ComponentB
{{msg}}
<button @click="handleClick">add</button>
</div>`,
data() {
return {
msg: 0
}
},
methods: {
handleClick() {
this.msg++
}
},
}
Vue.component("ComponentA", {
components: {
ComponentB
},
template: `<div>
ComponentA
<ComponentB></ComponentB>
</div>`
})
const app = new Vue({
el: '#app',
template: `<div>
{{msg}}
<ComponentA></ComponentA>
<ComponentA></ComponentA>
</div>`,
data: {
msg: 'hello world'
}
});
注意点
- 组件必须有根节点(Vue3可不需要)
- data 必须是个函数(因为组件可复用,使用函数把他们解耦开)
组件生命周期
什么是生命周期
组件的生命周期就是组件从 挂载前 到 卸载后 的一些钩子,也可以称为 hooks,就是普通的函数
为什么要用生命周期
我们常常有需求,挂载这个组件前要请求数据,又或者添加组件后需要启用全局(window)下的函数,例如定时器,也需要知道组件什么时候卸载,这个时候,我们需要用到组件的生命周期,我们可以把它想象为内置的 组件监听事件
生命周期是怎样的
- 主要由四套,create、mount、updata、destroy,每套有之前(before)、之后(ed) 组成
- 通过下面的小案例,我们可以清晰的看到生命周期的执行顺序是怎样的,如何工作的
- 组件会在
mounted即 挂载后 添加到视图里面 => 我们能看到他了
Vue.component("ComponentA", {
template: `<div>
ComponentA
{{count}}
<button @click="handleAdd">add</button>
</div>`,
data() {
return { count: 0 }
},
methods: {
handleAdd() {
this.count++
}
},
beforeCreate() {
console.log('创建前 - a');
},
created() {
console.log('创建后 - a');
},
beforeMount() {
console.log('挂载前 - a');
},
mounted() {
console.log('挂载后 - a');
},
beforeUpdate() {
console.log('更新前 - a');
},
updated() {
console.log('更新后 - a');
},
beforeDestroy() {
console.log('卸载前 - a');
},
destroyed() {
console.log('卸载后 - a');
}
})
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA v-if="handleShow"></ComponentA>
<button @click="handleShow=!handleShow">挂载/卸载</button>
</div>`,
data: {
handleShow: true
},
beforeCreate() {
console.log('创建前');
},
created() {
console.log('创建后');
},
beforeMount() {
console.log('挂载前');
},
mounted() {
console.log('挂载后');
}
});
组件间的通信
上面我们说了,组件相当于 js函数一样,那么在组件里面,我们是如何进行传参的?又如何返回参数的呢?换句话说,组件之间,如何协同工作呢?下面我们来说说组件间的通信
props
- 我们需要把参数传入组件时,用props进行传参
- 通过字段
props接收参数,引用组件时通过attribute传入参数 props可定义为 数组 或者 对象 的方式- 类比
typeScript,props也可以通过 对象的关键字段 限制类型,还能自定义规则 - 限制类型和规则,目的是为了多人协同工作时,防止乱传参数,减少对接成本
Vue.component("ComponentA", {
// 方式1
props: ['title'],
// 方式2
props: {
title: {
// 定义类型
type: String,
// 定义默认值
default: 'title-default',
// 定义是否必须传值
required: true
},
student: {
// 定义类型
type: Object,
// 自定义规则
validator(val) {
console.log(val);
// return true 通过, return false 不通过
return val.name === '小明'
}
}
},
template: `<div>
ComponentA
<div>{{title}}</div>
</div>`
})
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA title="123" :student="{name:'小明'}"></ComponentA>
</div>`
});
$emit
emit可以理解为 js函数 的 output,是用于组件发出值- 调用
this.$emit,第一个参数即是外层接收事件的 名字,用事件方式接收
Vue.component("ComponentA", {
template: `<div>
<button @click="handleClick">ComponentA</button>
</div>`,
methods: {
handleClick() {
this.$emit('change', '点击了')
}
}
});
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA @change="handleChange"></ComponentA>
</div>`,
methods: {
handleChange(val) {
console.log(val);
}
}
});
v-model
- 使用
v-model在组件传值,需要在组件内生命一个props - 一个组件上的
v-model默认会利用名为value的 prop 和名为input的事件 - 为了避免像单选框、复选框等类型的输入控件可能会将
valueattribute用于不同目的,可用model修改默认名
Vue.component("ComponentA", {
// model 用于修改默认关键字
model: {
prop: 'milk',
event: 'change'
},
// 默认为value
// props: ['value'],
props: ['milk'],
template: `<div>
<!-- {{value}} -->
{{milk}}
<button @click="handleClick">ComponentA</button>
</div>`,
data() {
return {
countA: 0
}
},
methods: {
handleClick() {
// 默认为 input
// this.$emit('input', ++this.countA)
this.$emit('change', ++this.countA)
}
}
});
const app = new Vue({
el: '#app',
template: `<div>
{{count}}
<!-- 这里的 count 将会传入 props 的 milk 内,当组件内 $emit change 触发并带回新值时,count 将会被更新 -->
<ComponentA v-model="count"></ComponentA>
</div>`,
data: {
count: 0
}
});
.sync
- 上面的
v-model可以使得传入的 count 与组件进行绑定,通过$emit更改 count,但是组件内需要一个中间值 countA 进行传输,那么如果我们需要绑定的值很多,我们可不可以在 组件内 直接修改props使得外部的 count 发生改变呢? - 直接修改
props是不可以的,因为Vue是 单向数据流 ,即数据只能单向传导,这么做是为了数据不乱套,维护数据的统一性 - 我们应该怎么做呢?
- 我们可以直接使用
$emit的方法,发送新的值出去,在外部改变- 这样写起来很麻烦,需要绕一个圈:先传值 => 改变值 => 传输出去 => 外部再改变值
- 会让我们的代码变得繁琐,业务多会出现这种逻辑,大量的堆叠起来,维护性和阅读性也会变差,而且组件之间不灵活,耦合性很强
Vue.component("ComponentA", {
props: ['count'],
template: `<div>
{{count}}
<button @click="handleClick">ComponentA</button>
</div>`,
methods: {
handleClick() {
this.$emit("upDateCount", this.count + 1)
}
}
});
const app = new Vue({
el: '#app',
template: `<div>
{{count}}
<ComponentA :count="count" @upDateCount="upDateCount"></ComponentA>
</div>`,
data: {
count: 0
},
methods: {
upDateCount(val) {
console.log(val);
this.count = val;
}
}
});
- 外部定义传值的时候,也定义一个修改这个值的方法(跟React逻辑一样)
- 这样看起来很不错,耦合性降低,一个值对应了读,写
- 但是这样我还是觉得不方便,我看着还是不爽
Vue.component("ComponentA", {
props: ['count', 'upDateCount'],
template: `<div>
{{count}}
<button @click="handleClick">ComponentA</button>
</div>`,
methods: {
handleClick() {
this.upDateCount(this.count + 1);
}
}
});
const app = new Vue({
el: '#app',
template: `<div>
{{count}}
<ComponentA :count="count" :upDateCount="upDateCount"></ComponentA>
</div>`,
data: {
count: 0
},
methods: {
upDateCount(val) {
this.count = val;
}
}
});
- sync
- 这是
Vue内置的语法糖 - 它可以大量的缩减我们的代码,使得我们心情愉悦(划掉),减少代码量,增加阅读性
- 这是
Vue.component("ComponentA", {
props: ['count'],
template: `<div>
{{count}}
<button @click="handleClick">ComponentA</button>
</div>`,
methods: {
handleClick() {
// 发出:update:propsName(接收的attribute)
this.$emit('update:childCount', this.count + 1)
}
}
});
const app = new Vue({
el: '#app',
template: `<div>
{{parentCount}}
<!-- 接收:propsName(接收的attribute).sync:data -->
<ComponentA :count="parentCount" :childCount.sync="parentCount"></ComponentA>
</div>`,
data: {
parentCount: 0
}
});
slot插槽
- 组件的传输,除了
props还有slot插槽 slot内可写默认的值- 插槽就像打孔,在组件内打孔,用外部传进去的东西填充
- 插槽可有多个,单个的
name默认为default,当你想定义多个时 你得给他起个名字 - 插槽有自己的作用域,如果想在调用组件时可以调用组件内的参数,可以通过
slot的attribute以对象的形式发出
Vue.component("ComponentA", {
template: `<div>
<!-- name 为 header 的 插槽 -->
<p><slot name="header">headerDefault</slot></p>
<!-- 默认插槽 的 两种写法 -->
<slot>default</slot>
<slot name="default">default</slot>
<!-- name 为 footer 的 插槽 -->
<!-- 插槽的作用域,把 childMsg 和 123 通过 attribute 传出去 -->
<p><slot name="footer" :msg="childMsg" num="123">footerDefault</slot></p>
</div>`,
data() {
return {
childMsg: 'childMsg'
}
}
})
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA>
<!-- name 为 header 的 插槽 -->
<template v-slot:header>{{parentMsg}}</template>
<!-- 默认插槽 的 两种写法 -->
slot1
<template v-slot:default>slot2</template>
<!-- name 为 footer 的 插槽 -->
<!-- 作用域插槽传出的 data 为 对象 -->
<template v-slot:footer="data">{{data.msg}} -- {{data.num}}</template>
</ComponentA>
</div>`,
data: {
parentMsg: 'parentMsg'
}
});
- 附加示例:作用域插槽的使用,内置 for 循环,外部调用
Vue.component("ComponentA", {
template: `<div>
<ul>
<li v-for="item in list"><slot :item="item"></slot></li>
</ul>
</div>`,
data() {
return {
list: [
{ name: '橘子', num: 12 },
{ name: '苹果', num: 0 },
{ name: '香蕉', num: 3 },
]
}
}
})
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA>
<-- 这里 item 是对象,通过{}解构 -->
<template v-slot="{item}">
{{item.name}} - {{item.num}}
<span v-if="!item.num">卖的真好,么得了</span>
</template>
</ComponentA>
</div>`
});
mixin混入
mixin是为了 组件的 复用而出现的- 他就是一个普通对象,在组件内暴露的
mixins引入后将会被使用 mixin对象内所有的参数 跟组件完全一样(包括mixins)**mixin有缺陷- 依赖关系模糊,找不到 对应的 来源
- 因为
mixin里面可以套用mixin,我们很难在嵌套的关系中找到对应我们需要的 fn
- 因为
- 命名冲突 A、B methods命名 可能一样,被顶替掉
- 依赖关系模糊,找不到 对应的 来源
mixin在Vue3被compositionAPI取缔
const mixinB = {
mounted() {
console.log('hei~ I\'m mixinB');
}
}
const mixinA = {
mixins: [mixinB],
template: `<div>ComponentA</div>`,
mounted() {
console.log('hei~ I\'m mixinA');
}
}
Vue.component("ComponentA", {
mixins: [mixinA]
});
Vue.component("ComponentB", {
mixins: [mixinA],
template: `<div>ComponentB</div>`
});
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA></ComponentA>
<ComponentB></ComponentB>
</div>`
});
ref
- 我们有的时候有可能需要到真实的
DOM元素进行一些操作,例如input框获取焦点之类的 - 这个时候我们可以通过使用 特殊的
attribute找到DOM
Vue.component("ComponentA", {
template: `<div>
ComponentA
<!-- attribute为ref -->
<input ref="ref"/>
</div>`,
mounted() {
// 通过 this.$refs 找到标记的 ref
console.log(this.$refs.ref);
this.$refs.ref.focus();
}
})
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA></ComponentA>
</div>`
});
自定义指令
- 除了上面的
ref之外,我们还可以通过 自定义指令 拿到真实的DOM,还有更加丰富的 配置项,可以让我们自定义我们需要的 Vue指令 - Vue3 更新了
directive内的生命周期指令,使得指令跟组件生命周期一样方便我们记,以后有空写自定义指令的时候再详细解读
Vue.directive('focus', {
// el 是 真实的 DOM
// binding 是 配置项,包括了参数,修饰语句等
// vnode 是 Vue 的虚拟 DOM
// bind 会在指令第一次绑定到元素时调用
bind(el, binding, vnode) {
console.log(el);
console.log(binding);
console.log(vnode);
console.log('bind');
},
// inserted 会在被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
inserted(el, binding) {
console.log('inserted');
el.focus();
},
// 还有其他的生命周期 update、componentUpdated、unbind 等可在官网了解
});
Vue.component("ComponentA", {
template: `<div>
ComponentA
<!-- .a.b 及 132 + 123 可在 binding 内获取 -->
<input v-focus.a.b="132 + 123" />
</div>`
});
const app = new Vue({
el: '#app',
template: `<div>
<ComponentA></ComponentA>
</div>`
});