前言
因为本文冗长,有一些是考虑对于小白上手所需技能,需要直接考虑组件拆分思路,建议直接进入
三、1) 组件拆分原则
六、一般的组件化内容
因为组件的知识点过多,本文是用于闲(摸 )暇(鱼 )时间写的,具体某个功能的用法,强烈建议翻阅其他文章或官网
一、 技术关键字
vue2.0
二、何为组件
1.组件
组件(Component)是 Vue.js 最强大的功能之一。
组件可以扩展 HTML 元素,封装可重用的代码。
组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:
2.组件化
组件化,就是将可封装的样式、逻辑进行封装,保证同项目甚至同工程以至于整个公司前端架构的用户操作体感的统一,界面的感官一致,以及复杂逻辑高度抽象,减少错误。
组件化应⽤提⾼了开发效率,增强可维护性,更好的去解决软件上的⾼耦合、低内聚、⽆重⽤的三⼤代码问。
三、组件封装
1.组件封装原则
其实可以将每个组件视为大前端架构下的独立前端项目,亦或是微服务架构下的独立服务。其设计思路基本相通。
Ⅰ框架组件
框架组件:来源于《在前端架构-从入门到微前端》一书
壹)原子设计
①原子
即事物的基本组成部分。他是最小的单元,不能再往下细分,也就是基本的HTML标签,比如表单标签、输入、按钮。
②分子
即由原子聚合而成的粒子。在UI设计中,分子是由几个基本的HTML标签组成的简单组织,如移动端Web应用的底部导航栏。
③有机体
是由分子、原子或其他有机体组成的相对复杂的UI组分。它用于创建一些独立、可复用的内容,比如header、footer等。
④模板
顾名思义就是整合签名档元素,构建整体的布局,它将组件在这个布局的上下文中结合到一起,比如,一个博客包含header、footer及博客内容,三者可以构建一个基本外观。
⑤页面
用真实的内容(数据)展示出来的最终产品,它还能测试设计系统的弹性。
贰)维护与文档
这是一个架构级组件是否还“活着”的凭证
Ⅱ业务组件
壹)单一职责
即每个组件,应且只应去完成一个事情,页面说基于一个又一个小功能点进行的叠加。例如,table组件不应当参与分页逻辑,而应当只考虑渲染table功能,而搜索功能也应当与table组件与分页器脱离。确保每个组件的功能单一,职责明确。
贰)简化复杂应用
当一个页面逻辑过于复杂时,试着将逻辑进行拆解一个个小功能点。而页面本身,只关心怎么帮助组件进行通讯即可,不去干扰组件内部的逻辑,而组件内部的方法,“非请勿入”,只有组件明确邀请,才可以通过$refs进行操作
叁)灵活组合
当页面拆解之后,将会得到一大堆零件,此时可以思考,哪些零件是雷同的,可以进行合并,哪些组件的功能冗余,包含了一大堆职责,而哪些组件被父页面频繁访问内部属性?
肆)业务组件拆分总结
简简单单六个字:高内聚、低耦合
2.组件封装类型
本人按照个人习惯,将组件封装简单划分为两种UI与功能
《在前端架构-从入门到微前端》一书中
将组件分为三类:
基础UI组件:这是最小化的组件,他们不依赖于其他组件(博主注:可以参考elementUI源码、iview源码)
复合组件:由多个组件组成的组件,他们依赖于现有组件(博主注:参考ruoyi、花裤衩等源码)
业务组件:带有业务功能的大量重复使用的组件(博主注:参考公司自身封装的业务组件源码或项目页面组件代码)
1)UI式组件
顾名思义,UI式组件主要是对于项目内相同的样式进行封装,也是新手最易上手的封装思路,因为能够根据直观的可视化界面进行封装,上手较为简单。
但是UI一般每个项目都有些许差异,因此大部分UI组件为项目级或业务级组件,很少会被提升为框架架构即组件。
如下图,一般而言会将商品单封装成组件,进行样式统一管理。
2)逻辑功能性组件
封装逻辑功能型组件,一般是有一定经验的前端会考虑的封装思路,其主要思想不局限于UI的展示,一般还可能进行业务逻辑的封装,保证新人不会遗落或设置错误的前端逻辑
如上图,这是一个后台管理界面demo,一般新人处理翻页逻辑以及搜索重置逻辑时,功能点较多导致逻辑混乱,经常导致如下的问题:
- 翻页未触发更新接口
- 翻页后搜索项被重置,导致用户每次翻页后,需要重新输入搜索条件,否则二次翻页会回显全量数据
- 翻页设置的条数项目内不一致
- 用户输入搜索项,未点击搜索,点击翻页时数据条目错误(搜索项用户未确认,前端却错误地传给后端服务了)
- 如此等等 …
如此多的逻辑处理,即使是经验老道的前端去处理,每个页面都要写一次,也是繁琐。因此将整个搜索列表页进行封装,将翻页逻辑,搜索逻辑,显示逻辑等进行统一处理,保证了项目使用感受的统一性,也保证了新手进入项目时逻辑混乱导致项目崩盘
一般功能性组件因其思路较为健全,符合整体设计,较于UI组件会较多的提升为架构级别的组件应用,融入公司框架项目,方便后续开发使用
3.组件封装思路
组件如何去封装,如何去搭建是新人比较头疼的事情,以下为本人的设计思路,其实与组件封装原则是互通的
1)由上及下眼光
首先就是由上及下地去观察,先看项目,再看页面,再看局部,哪些样式是相似的,哪些逻辑是统一的
相似一般出现在for循环内,复制粘贴过程中
2)由下及上搭建
当发现到哪些内容可以封装时,要确保其已经是最小的单元,其所谓最小单元不是局限于样式,也是逻辑功能的最小单元,例如,列表table就可以是一个单独的组件,不用怕组件多,一般浏览器性能都可以支撑起来,当然,如果是对于性能有极致要求,就要平衡一下封装与效率了
3)积木式构建页面
当由下及上的搭建组件时,会得到一大堆分离的组件,当绘制页面时肯定不能将组件直接扔在页面上,因此,需要对于组件进行搭建,其搭建就如同积木一般,将所需的组件放在他应该所处的位置,并在多个组件的父页面上,进行组件间的通信,
切记:尽量不要把组件通信扔在某个单一组件内,因为其破坏了组件封装的原则,组件不能触碰他所未涉及的领域功能
如下图:此列表页面,组件通信逻辑应当在页面的index内,而不是table组件内
四、组件设计
上文均为组件化设计思维,主要焦点为如何进行组件化,组件拆分依据
下文主要是组件封装相关技术
1.数据传递
1)prop &&emit
propu与emit的组件通信是最为常见的使用方式,一般来说此类封装的组件通信能占50%以上的组件,或80%以上的业务组件封装。
Ⅰ.单向数据流
原文如下
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
需要注意的是,prop的通信方向仅可以父向子进行数据传递,emit仅进行子向父传递内容。
但是因vue2.0的数据监听方式以及浅拷贝问题,通过对象或数组是可以绕过检测,进行数据变更且不会抛出异常。此方法是绝对禁止的!
正常操作应当是子组件仅更新组件内部的相关数据,而不应该污染父页面的数据。对于数组或对象的数据的接收及处理,正确做法应当是深拷贝数组或对象后,扔进子组件的data内,可以在监听器内或生命周期内进行处理,此处不再赘述。
Ⅱ.父子传值
父子传值是较为常见的,即简单的prop &&emit应用,不做过多描述,以下为简单示例
壹)prop
子组件的prop绑定的值会接收父页面传来的值,当没有传值且未定义default时,将会默认undefined。
父页面:
<children :test="123456" />
子组件:
props:{
test:Number
}
以下为经验之谈
[Vue warn]: Invalid prop: type check failed for prop "error". Expected Number with value NaN, got String with value "error".
看到类似错误基本上就是数据类型出问题了
①类型
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise //或任何其他构造函数
}
②验证
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
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
}
}
}
贰)emit
this.
e
m
i
t
(
k
e
y
,
p
a
r
a
m
s
)
e
m
i
t
传
递
的
数
据
会
被
emit(key,params) emit传递的数据会被
emit(key,params)emit传递的数据会被on(key,function)接收,且触发对应的function,注意:emit的key与on的key一致才可被触发
一般较多用于子组件传递参数给父页面
父页面:
<children @testEmit="testFun" />
export default {
methods:{
testFun(params){
console.log(params)
}
}
}
子组件:
export default {
methods:{
demo(){
this.$emit("testEmit","TEST")//当然,不仅仅可以传一个字符串,其参数长度,数据类型无限制
}
}
}
Ⅲ.穿透
穿透一般用于父页面与子组件的组件进行通信,其prop与emit无需在子组件内进行接收与传导。
主要使用了v-bind=“$attrs"以及v-on=”$listeners"
壹)$attrs
主要接收父页面传递,子组件却未在prop接收的值,是这些值的集合
贰)$listeners
主要将父页面的事件监听器,传递到更深的组件内
叁)用法
父页面:
<children @testEmit="testFun" :testProp="testProp" />
export default {
data() {
testProp:"testTextToGrandson"
},
methods:{
testFun(params){
console.log(params)
}
}
}
子组件:
<Grandson v-bind="$attrs" v-on="$listeners" />
export default {}
孙组件
export default {
props:{
test:Number
},
methods:{
demo() {
this.$emit("testEmit","TEST")//当然,不仅仅可以传一个字符串,其参数长度,数据类型无限制
}
}
}
Ⅳ.双向绑定语法糖
主要是v-model语法糖的使用:
如果想要通过v-model来进行值的传递与更新,需要如下写法
父页面:
<children v-model="test" />
export default {
data() {
test:"testText"
}
}
子组件:
export default {
props:{
value:String
},
methods:{
demo(){
this.$emit("input","updateText")
}
}
}
当父页面的v-model传入子组件时,用value接收,input更新时,也会同时更新v-model,而且不会触发vue的禁止反向更新的警告,因为这是被允许的
2)inject && provide
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
当组件的层次结构比较深时,亦或是搭建大型组件库时,可以使用inject && provide进行值的传递,但是一般业务不建议使用,因为其会增加阅读代码逻辑的复杂性(不好跟踪数据),部分情景无法监听数据变化.
父页面:
export default {
provide() {
return {
foo: this.foo,
content: this.content
}
},
data() {
return {
content: 'hello world'
}
},
methods: {
foo() {
console.log("我是父组件")
}
}
};
子组件
export default {
name: "Son",
inject: ['content','foo'],
created() {
this.foo();
}
};
3)bus
中央事件总线bus,可以作为一个简单的组件传递数据,用于解决跨级和兄弟组件通信问题,本质上是使用一个空的Vue实例作为中央事件总线。
一般业务谨慎使用,尤其是多人维护项目,可能会出现数据跟踪丢失,数据污染,多次触发等情景
bus.js
import Vue from 'vue'
const Bus = new Vue()
......
Vue.prototype.$bus = new Bus();
组件1/子组件(触发组件)
this.$bus.$emit('busLog', "https://blog.csdn.net/weixin_44599143/article/details/124680508")
组件2/父组件(接收组件)
this.$bus.$on('busLog', text=> {
console.log(text)
});
使用后记得通过$off卸载监听或者通过$once等方法控制
4)ref && parent && children && root
其实本质上是将各个组件、父级的环境进行引入,进行操作其内部的函数、数据
一般来说,使用ref进行操作即可,不建议使用其他三种方法。
基本用法就是
this.$refs.组件名
this.$parent
this.$children
this.$root
最常见的用法就是父页面调用子组件相关函数,进行控制或数据校验
5)Hook
hook是子组件暴露自身的生命周期给父页面进行操作的一个方式,目前没有实际运用,不做展开讲解
<children
@hook:mounted="childrenMounted"
@hook:beforeUpdated="childrenBeforeUpdated"
@hook:updated="childrenUpdated"
/>
2.插槽
插槽就是在子组件内,暴露一部分空间,将此空间交给父级页面
需要注意,插槽在渲染的过程会被顶替,因此挂载在 标签上面的class或style将失效
子组件
<div>
当前作用域在:<slot />
</div>
父页面
<children>父页面</children>
渲染在页面时就是
当前作用域在:父页面
1)匿名插槽
匿名插槽就是没有命名的基本用法,同上面例子,不做赘述
2)具名插槽
具名插槽主要是用在子组件有多个插槽的使用场景,此时用匿名插槽,将不能够很好的匹配插槽的位置,当使用name时,插槽能够自动追踪自己应该在的位置
子组件
<div>
<slot name="test"/>:<slot name="test2"/>
</div>
父页面
<children>
<span slot="test2">
父页面
</span>
<span slot="test2">
当前作用域在
</span>
</children>
渲染在页面时就是
当前作用域在:父页面
而非
父页面:当前作用域在
每个slot都会追踪自己的name
3)作用域插槽
顾名思义 当父页面需要引用子组件某个局部变量时,将会使用作用域插槽,此功能主要用于列表渲染时的个性化参数设置,父页面在插槽内获取每行数据,进行差异化操作等
子组件
<div>
<slot test="当前在" test2="子组件"/>
</div>
父页面
<children>
<span slot-scope="test" slot-scope="test2">
{{test}}{{test2}}
<div>当前作用域在:父页面</div>
</span>
</children>
当前在子组件
当前作用域在:父页面
4)插槽默认内容
当插槽没有在父页面声明时,可以选择默认渲染一些内容
子组件
<div>
当前作用域在:<slot>子组件</slot>
</div>
父页面
<children />
渲染在页面时就是
当前作用域在:子组件
3.动态组件 标签component
动态组件主要是用在组件会根据业务实时变化的场景下,如果使用v-if未免显得冗余,使用动态组件可以更加方便的进行页面搭建
<component is="el-input" v-model="input"></component>
这个组件经过vue处理,会等价于
<el-input v-model="input"></el-input>
4.函数式组件
函数式组件,关键字就是functional
函数式组件没有this指向以及生命周期,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法,也就不需要组件自身的状态去占用浏览器资源
functional学习成本较高不建议小白一上来就用,本文只做简单demo描述,具体使用建议查询其他资料或官网
1)template
模板functional主要是处理一些样式上的封装,
子组件
<template functional>
<slot />
{{props.test}}
</template>
父页面
<children test="函数式组件">这当然是</children>
渲染在页面时就是
这当然是函数式组件
2)script
script一般是通过rander函数进行组件的封装
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
vnodes.push(<i class={icon}/>)
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
在 render里不仅可以使用vnodesList 也可以使用h函数进行处理
export default {
name: 'TableExpand',
functional: true,
props: {
test: String
},
render: (h, ctx){
return h('div',{style:'color:red;'},h('div',test))
}
};
5. keep-alive
keep-alive会将失活的组件缓存在vue环境里,当失活的组件被重新激活时,其内部的状态将会延续失活前的状态
<keep-alive>
<children></children>
</keep-alive>
请注意
触发此机制必须组件内部声明了name属性
export default {
name: 'children'
}
请注意
keep-alive的生命周期与正常组件的create等有些许差异,他有自己的唤醒,失活生命周期,具体请查阅官网
五、组件的二次封装
1.mixins混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
混入仅能合并script标签内的内容
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello("这是混入")
},
methods: {
hello(text) {
console.log('混入')
console.log(text)
}
}
}
当想要使用时
import myMixin from 'myMixin'
export default {
mixins:[myMixin],
created: function () {
this.hello('这是组件')
},
methods: {
hello(text) {
console.log('组件')
console.log(text)
}
}
};
混入数组,因为他可以使用多个混入对象
函数式数据(生命周期钩子等),先执行混入,再执行组件的数据
而对象数据(监听器,计算属性,methods等),组件数据会覆盖混入的数据
2.extends继承
继承主要是对某个组件进行继承,相当于将整个组件copy过来进行改写
如下代码,就是本人通过继承手段将删除逻辑进行封装操作
(中介者设计模式,方便业务全局修改)
<script>
import {Button} from 'element-ui'
export default {
props: {
plain:{
type:Boolean,
default:true
},
type:{
type:String,
default:"primary"
}
},
extends: Button,
};
</script>
或者改写已有组件的内部逻辑(将:value@check-change绑定改写为v-model语法糖)
(装饰者模式,方便进行快速开发)
<script>
import {Tree} from 'element-ui'
export default {
props: {
value: {
type: Array,
default: () => []
}
},
extends: Tree,
watch: {
value:{
immediate:true,
handler(val){
this.$nextTick(()=>{
this.setCheckedKeys(val);
})
}
}
},
created() {
this.$on('check-change',()=>{
this.$emit("input",this.getCheckedKeys())
});
}
};
</script>
六、一般的组件化内容
本节主要对于常见的组件化进行归纳(《在前端架构-从入门到微前端》)
1.表单控制类
自动完成、输入框、单选框、选择器、滑动输入条 等
2.导航类
菜单、侧边导航、工具栏等
3.布局类
卡片、标签页、树形选择器、栅格列表、步骤条等
4.按钮和指示器类
按钮、图标、进度条等
5.弹窗及对话窗类
底部弹出框、对话框、提示框、表单输入框等
6.数据表格类
分页器、排序表单类等