写代码有时候就和弹簧加工同样。一个看似简单的物品,加工起来未必简单css
最近在作项目的时候,看到有两个功能同样,可是交互,样式不同的需求,为了图方便维护,就封装了组件,发现一个看似简单的组件,若是要封装得通用些,要考虑的东西其实也很多。前端
该文章只是举例说明能够从哪些点入手,增长组件的通用性。以及提供一些封装的思路。说起的组件仍然与项目需求有挺大的关系,差很少是针对项目的定制开发,在其余项目上可能还不能开箱即用,要使用的话,还须要对组件进行修改。vue
这个组件看着就很简单,一下就写好了git
出于篇幅的考虑,css ,以及一些不关联的 js 代码就不提供了,须要看源码能够移步: 文章例子源码:HandleButtonOld,项目完整代码:项目代码github
HandleButtonOld.vuebash
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index}"
@click="switchCur(item,index)"
>
<ec-text v-if="item.fileType==='text'" :src="item.fileUrl"></ec-text>
<video :src="item.fileUrl" v-if="item.fileType==='video'"></video>
<audio :src="item.fileUrl" controls="controls" v-if="item.fileType==='audio'"></audio>
<ec-image :src="item.fileUrl" v-if="item.fileType==='image'" />
<ul class="customer-form-view-action-box">
<li class="iconfont icon-icon-cus-edit" @click.stop="handleEvent('edit',index)"></li>
<li
class="iconfont icon-icon-cus-up"
@click.stop="handleEvent('up',index)"
v-if="index!==0"
></li>
<li
class="iconfont icon-icon-cus-down"
@click.stop="handleEvent('down',index)"
v-if="index!==value.length-1"
></li>
<li class="iconfont icon-icon-cus-del" @click.stop="handleEvent('delete',index)"></li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'HandleButton',
componentName: 'HandleButton',
props: {
value: {
type: Array,
default () {
return []
}
}
},
data () {
return {
nowClickIndex: ''
}
},
mounted () {},
methods: {
handleEvent (type, index) {
let _list = JSON.parse(JSON.stringify(this.value))
let _nowItem = _list[index]
switch (type) {
case 'up':
this.nowClickIndex--
_list.splice(index, 1)
_list.splice(index - 1, 0, _nowItem)
break
case 'down':
this.nowClickIndex++
_list.splice(index, 1)
_list.splice(index + 1, 0, _nowItem)
break
case 'delete':
_list.splice(index, 1)
}
this.$emit('input', _list)
this.$emit(type, _nowItem, index)
},
switchCur (item, index) {
this.nowClickIndex = index
}
}
}
</script>
<style lang="scss" scoped>
// 略
</style>
复制代码
组件用起来也很简单,简单一行代码就出来了微信
<handle-button-old v-model="sortData"/>
ide
sortData函数
sortData: [
{
fileType: 'text',
content: '前端开发',
index: 2,
size: 12
},
{
fileNmae: '251bb6d882024b11a6051d604ac51fc3.jpeg',
fileType: 'image',
fileUrl:
'https://file-cdn-china.wechatify.net/marketing/sms/mms_material/53ce422f14e516af0eb9a5c7251cc1ca.jpeg',
index: 3,
size: 101109,
fileName: '53ce422f14e516af0eb9a5c7251cc1ca.jpeg'
},
{
fileType: 'text',
content: '守候',
index: 5,
size: 12
}
]
复制代码
可是若是页面上又有这样一个需求,功能同样,样式排版不同,好比下图这样优化
而后组件就没法使用了。
这个时候,确定不是复制一个文件,改下样式再写一个组件,只能把原来的组件改得通用些,能适合更多需求。
遇到这样的需求,很是不建议复制一个文件,再写一个组件。若是下次再有这种状况,又要再复制一个文件,再写一个组件。就可能会致使组件文件很是多,影响维护
让组件更通用些,能适合更多需求,主要就是要把常常会变的因素抽取出来,交给用户自定义,至于有哪些地方能够改进优化?下面就简单列举一下
首页,看到两个需求,排版样式和显示字段就不同了。不知道之后第三种,第四种排版样式,也不知道会显示什么字段。因此这里最好是他操做按钮抽出来,做为组件封装,至于怎么排版,显示什么字段,组件无论,只须要提供一个 slot 由用户自定义。
HandleButtonOld.vue
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index}"
@click="switchCur(index)"
>
<!-- 提供slot -->
<slot :data="item"></slot>
<ul class="customer-form-view-action-box">
<!--重复代码略-->
</ul>
</div>
</div>
</template>
复制代码
页面调用
<handle-button-old v-model="sortData">
<!--提供 slot-scope 须要什么字段以及排版能够自定义-->
<div slot-scope="item" class="view-item">
<span v-if="item.data.fileType==='text'">{{item.data.content}}123</span>
<video :src="item.data.fileUrl" v-if="item.data.fileType==='video'"></video>
<audio :src="item.data.fileUrl" controls="controls" v-if="item.data.fileType==='audio'"></audio>
<img :src="item.data.fileUrl" v-if="item.data.fileType==='image'" />
</div>
</handle-button-old>
复制代码
来到这里,看一下选中的效果,
除了显示几个操做按钮以外,还有一个蓝色的边框线,可是不一样需求,选中效果多是不同的,好比有一个地方要用灰色双实线,再有一个地方要用白色实现,边距增长 30px 等等。没法猜想下一次用这个组件的时候,选中样式是什么。因此选中样式不能在 handle-button-old 内部写死或者判断,只能让用户自定义。咱们能提供的,就是给一个字段,告诉用户哪一项是当前选中的。
HandleButtonOld.vue
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index}"
@click="switchCur(item,index)"
>
<!--对 item 进行封装-->
<slot :data="getItem(item,index)"></slot>
//代码略
</div>
</div>
</template>
<script>
export default {
//代码略
methods: {
getItem (item, index) {
// 把索引($index) 和 当前是否选中($select) 字段合并到 item 里面
//这里是顺便把索引传过去了,是为了之后的不时之需,这里不展开讲
return Object.assign({}, item, { $index: index, $select: this.nowClickIndex === index })
}
//代码略
}
}
</script>
复制代码
页面调用
<!--根据 $select 判断是否添加 cur 这个 class-->
<handle-button-old v-model="sortData">
<div slot-scope="item" class="view-item" :class="{'cur':item.data.$select}">
//代码略
</div>
</handle-button-old>
<style lang="scss">
.view-item {
padding: 10px;
border:4px dashed transparent;
&.cur{
border:4px double #ccc;
}
}
</style>
复制代码
这样就可让用户自定义选中的样式了
再看一下两个需求的样式
首先看到按钮的位置和方向是不同的。按钮的位置,能够给默认值,但也要让用户能够自定义。要肯定按钮的定位,则 handle-button-old 组件须要提供 top,right,bottom,left,四个参数。为了方便定位,除了能够设置具体像素,百分比以外,还要支持用户输入'center',方便用户设置垂直或者水平居中。
按钮的方向就须要提供 direction 参数,用户输入 horizontal 就垂直显示,输入 vertical 就水平显示
handle-button-old
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index}"
@click="switchCur(item,index)"
>
<slot :data="getItem(item,index)"></slot>
<!--绑定style,以及根据direction 设置 class,设置ul的样式-->
<ul class="customer-form-view-action-box"
:style="ulPosition"
:class="{'handle-vertical':direction==='vertical'}"
>
//代码略
</ul>
</div>
</div>
</template>
<script>
export default {
//代码略
props: {
//代码略
top: {
type: [String, Number],
default: '0'
},
bottom: {
type: [String, Number],
default: 'auto'
},
left: {
type: [String, Number],
default: 'auto'
},
right: {
type: [String, Number],
default: '-26px'
},
direction: {
type: String,
default: 'horizontal'
}
},
computed: {
ulPosition () {
let obj = {
left: this.left,
right: this.right,
top: this.top,
bottom: this.bottom
}
let _x = '0'
let _y = '0'
if (this.top === 'center' || this.bottom === 'center') {
obj.top = '50%'
obj.bottom = 'auto'
_y = '-50%'
obj.transform = `translate(${_x},${_y})`
}
if (this.left === 'center' || this.right === 'center') {
obj.left = '50%'
obj.right = 'auto'
_x = '-50%'
obj.transform = `translate(${_x},${_y})`
}
return obj
}
}
}
</script>
<style lang="scss" scoped>
.ec-handle--item {
position: relative;
ul {
position: absolute;
right: -26px;
top: 0;
display: none;
line-height: 24px;
&.handle-vertical {
li {
display: inline-block;
vertical-align: top;
}
}
}
}
</style>
复制代码
页面调用
<!--设置按钮的位置和方向-->
<handle-button-old
v-model="sortData"
direction="vertical"
right="6px"
bottom="center"
>
<div slot-scope="item" class="handle-item">
//代码略
</div>
</handle-button-old>
export default {
data () {
return {
iconByFileType: {
text: 'icon-wenben',
image: 'icon-tupian1',
video: 'icon-shipin',
audio: 'icon-yinpin',
link: 'icon-duanlian'
},
//代码略
}
},
//代码略
methods: {
formatSize (val) {
if (val === 0) {
return '0B'
}
let sizeObj = {
MB: 1048576,
KB: 1024,
B: 1
}
val = +val
for (let key in sizeObj) {
if (val >= sizeObj[key]) {
return +(val / sizeObj[key]).toFixed(2) + key
}
}
},
//代码略
}
}
</script>
<style lang="scss" scoped>
//代码略
</style>
复制代码
这样效果就实现了
想必你们已经看到问题了,【3-2】最后看到的结果是只有其中一项是有操做按钮的,而【3-2】一开始,看到的需求是全部的结果都要显示出来。那么这里就要设置一个 display 属性了,设置操做按钮的显示方式。目前提供三个值'default'-选中的项显示,'visible'-全部项显示,'none'-不显示。
handle-button-old
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index || display==='visible'}"
@click="switchCur(item,index)"
>
<slot :data="getItem(item,index)"></slot>
<ul class="customer-form-view-action-box"
:style="ulPosition"
v-if="display!=='none'"
:class="{'handle-vertical':direction==='vertical'}"
>
//代码略
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
display: {
type: [String],
default: 'default'
},
//代码略
},
//代码略
}
</script>
复制代码
页面调用
<handle-button-old
v-model="sortData"
direction="vertical"
right="6px"
display="visible"
bottom="center"
>
//代码略
</handle-button-old>
复制代码
这样就能实现了
不少人在开发上会遇到一些需求,特别是在执行好比删除,清空等“危险操做”以前,要给一个弹窗或者其余方式的提醒,让用户谨慎操做。而这个组件的操做按钮,也有多是“危险操做”。因此须要让用户能够自定义操做前的回调。
拿文章说起的 handle-button-old 组件来讲,若是需求是“删除”按钮前须要给提醒弹窗,其余的按钮直接操做。
handle-button-old
<template>
<!--代码略-->
</template>
<script>
export default {
props: {
beforeDelete: {
type: Function
},
beforeUp: {
type: Function
},
beforeDown: {
type: Function
},
beforeEdit: {
type: Function
}
},
data () {
return {
nowClickIndex: '',
eventType: '',
curHandleIndex: ''
}
},
methods: {
/**
* @description 执行事件
*/
handle () {
let _list = this.value
let _nowItem = _list[this.curHandleIndex]
switch (this.eventType) {
case 'up':
this.nowClickIndex--
_list.splice(this.curHandleIndex, 1)
_list.splice(this.curHandleIndex - 1, 0, _nowItem)
break
case 'down':
this.nowClickIndex++
_list.splice(this.curHandleIndex, 1)
_list.splice(this.curHandleIndex + 1, 0, _nowItem)
break
case 'delete':
_list.splice(this.curHandleIndex, 1)
}
this.$emit('input', _list)
this.$forceUpdate()
this.$emit(this.eventType, _nowItem, this.curHandleIndex)
},
/**
* @description 处理事件
*/
handleEvent (eventType, item, index) {
// 记录事件类型
this.eventType = eventType
// 记录当前操做项的索引
this.curHandleIndex = index
let _type = eventType.substr(0, 1).toUpperCase() + eventType.substr(1)
if (typeof this[`before${_type}`] === 'function') {
// 把当前操做的函数,当前项,索引做为参数,传给调用函数
this[`before${_type}`](this.handle, item, index)
} else {
this.handle()
}
},
}
// 代码略
}
</script>
复制代码
页面调用
<template>
<handle-button-old
v-model="sortData"
direction="vertical"
right="6px"
display="visible"
bottom="center"
:beforeDelete="handleBefore"
>
<!--代码略-->
</handle-button-old>
</template>
<script>
methods: {
/**
* @description 操做前的回调
* @augments done - 用于执行操做
* @augments item - 当前项
* @augments index - 当前索引
*/
handleBefore (done, item, index) {
// 点击确认才进行操做,点击取消不作处理
this.$confirm('确认进行删除操做?')
.then(() => {
done()
})
.catch(() => {})
}
}
</script>
复制代码
好比有需求,点击切换选中的时候,须要拿当前项的数据,作为请求的参数。实现这个需求,只须要在 handle-button-old 组件里面须要提供一个自定义事件便可
handle-button-old
methods:{
switchCur (item, index) {
this.nowClickIndex = index
//触发自定义事件
this.$emit('change', item, index)
}
}
复制代码
页面调用
<handle-button-old
style="margin-bottom:500px;"
v-model="sortData"
direction="vertical"
right="6px"
display="visible"
bottom="center"
@change="handleChange"
>
</handle-button-old>
复制代码
可能你们在一早已经发现了这个问题,若是选中了某一项,出现了下面状况。
可是若是需求是取消选中呢?那就作不到了。从代码逻辑上来说,只要选中了,就要选中一项,没办法取消。因此,在 3-5 的 switchCur 函数就须要判断一下,若是点击的是当前项,就取消选中
handle-button-old
methods:{
switchCur (item, index) {
if (this.display === 'visible') {
return
}
// 若是点击的是当前项,就取消选中
this.nowClickIndex = this.nowClickIndex !== index ? index : ''
this.$emit('change', item, index)
}
}
复制代码
在上面的图片能够看到,按钮要么是横向排列,要么是竖向排列。若是哪天需求以为按钮太占位置,须要折叠显示按钮,这个也很简单就能够兼容了,给 handle-button-old 加个 type 参数判断下要根据什么方式显示就能够了。
handle-button-old
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index || display==='visible'}"
@click="switchCur(item,index)"
>
<slot :data="getItem(item,index)"></slot>
<!--若是不是 dropdown 类型以及 dispaly 不为 none-->
<ul
class="customer-form-view-action-box"
:style="ulPosition"
v-if="type!=='dropdown'&&display!=='none'"
:class="{'handle-vertical':direction==='vertical'}"
>
<!--代码略-->
</ul>
<!--若是是dropdown类型-->
<el-dropdown v-else-if="type==='dropdown'" class="customer-form-view-action-box" :style="ulPosition" style="position:absolute;">
<span class="el-dropdown-link">
操做<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu>
<el-dropdown-item><div @click.stop="handleEvent('edit',item,index)">编辑</div></el-dropdown-item>
<el-dropdown-item v-if="index!==0"><div @click.stop="handleEvent('up',item,index)">上移</div></el-dropdown-item>
<el-dropdown-item v-if="index!==value.length-1"><div @click.stop="handleEvent('down',item,index)">下移</div></el-dropdown-item>
<el-dropdown-item><div @click.stop="handleEvent('delete',item,index)">删除</div></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
export default {
name: 'HandleButton',
componentName: 'HandleButton',
props: {
type: {
type: String
}
// 代码略
}
// 代码略
}
复制代码
页面调用
type="dropdown" 以后,direction 参数会不起效
<!--type="dropdown" 折叠显示操做按钮,不然为平铺并列显示-->
<handle-button-old
style="margin-bottom:500px;"
v-model="sortData"
direction="vertical"
type="dropdown"
right="6px"
display="visible"
bottom="center"
:beforeDelete="handleBefore"
>
<div slot-scope="item" class="handle-item">
<div class="message-item___box">
<span class="message-item___icon iconfont" :class="iconByFileType[item.data.fileType]"></span>
</div>
<div class="message-item___info">
<p v-if="item.data.fileType==='text'">
<span>{{item.data.content}}</span>
</p>
<p v-else>{{item.data.fileName}}</p>
<span class="message-item___info_size">{{formatSize(item.data.size)}}</span>
</div>
</div>
</handle-button-old>
复制代码
回到这个场景,可能你们在开发的时候已经想到了,要出现操做按钮,必需要点击某一项才会出现。但不少时候的需求,须要鼠标 放上去的时候就显示操做按钮,不须要点击。要实现这个,只须要添加 一个 trigger 参数,triggle 默认为 'click'-点击出现,'hover'-鼠标放上去出现
<template>
<div class="ec-handle">
<div
class="ec-handle--item"
v-for="(item,index) in value"
:key="index"
:class="{'cur':nowClickIndex===index || display==='visible'}"
@click="switchCur(item,index,'click')"
@mouseenter="switchCur(item,index,'hover')"
@mouseleave="handleMouseLeave"
>
<!--代码略-->
</div>
</div>
</template>
<script>
export default {
props: {
// 代码略
triggle: {
type: String,
default: 'click'
}
},
methods: {
// 加上 eventType 参数区分当前触发的事件
switchCur (item, index, eventType) {
if (this.display === 'visible') {
return
}
//若是当前触发事件与 triggle 不一样,则不执行操做
if (eventType !== this.triggle) {
return
}
this.nowClickIndex = this.nowClickIndex !== index ? index : ''
this.$emit('change', item, index)
},
handleMouseLeave () {
// 若是triggle 为 hover ,鼠标移除的时候,取消选中当前项
if (this.triggle === 'hover') {
this.nowClickIndex = ''
}
}
}
// 代码略
}
</script>
复制代码
页面调用
<handle-button-old v-model="sortData" triggle="hover">
<!--代码略-->
</handle-button-old>
复制代码
首次 handle-button-old 这个组件为例,列举了一些改进优化的功能。若是想折腾,仍是有很多功能能够折腾的,好比按钮的样式(图标的颜色、形状、背景颜色、大小等等)、间距,自定义按钮等。至于要不要折腾,就看需求有没有必要了,具体状况,具体分析。文章的这个,仍是很简单的一个组件,若是是复杂的组件,须要优化的点可能就更多了。
封装组件的时候,若是一开始对组件的要求比较单一,或者时间比较紧急,也能够先封装个有基本功能,能知足需求的组件。以后若是发现组件不能知足业务需求了,再进行改进和优化也不迟。
-------------------------华丽的分割线--------------------
想了解更多,和我交流,请添加我微信。或者关注个人微信公众号:守候书阁