这次的教程里,我们要把组件化进行到底!最近半年的几个项目中,都遇到了需要使用Toast或者Notification组件的情况。在目前已有的一些基于Vue.js开发的组件库,都没有找到太合适的,所以自己重头实现了一个。历经几个项目的磨练,这个提示组件的功能已经越来越完善,这次就分享一下组件以及其实现思路吧。
GitHub 仓库:https://github.com/Yuyz0112/vue-notie
Demo 地址:http://lab.myriptide.com/vue-notie/
深入组件化,组件的拆分、整合与复用
Vue.js的组件化可以说是其招牌特性之一,而在实际应用时,并非一味地追求组件颗粒越小越好,而是需要根据项目的实际需求,来分析自己需要什么级别的组件。
例如在一个SPA中,我可能有主页、文章列表页、文章页、个人中心页4个主要的视图,于是我将其分别对应的写成4个组件。
但是在实际编写的过程中,发现他们共用了同一套侧边栏,而侧边栏对应的代码也在4个组件中重复书写了4次。所以可以将侧边栏单独写成一个组件进行复用。
之后,我们可能发现可以复用的还有一些表单、按钮之类的内容我们都可以复用成组件。但实际上,我们也会发现过度的组件化会导致代码量上升、开发时间增加以及额外的数据传递等等。所以如果不打算制作一个完整的组件库,那么在实际项目中做到按需拆分、整合即可,不用过分的追求每个可复用的部分都写成单个组件。
为什么需要一个提示组件
因为alert大部分时间不能满足我们的需求啊。往往项目里需要一个类似于alert的东西,用美观、可定制的方式提示用户一些信息,因此这样一个提示组件很有必要。
同时,我们也不希望同一时间出现多个提示混淆用户,因此在设计上,我们将提示组件设定为具有唯一性,整个应用中各个视图调用的都是同一个提示组件。
Show me the code
接下来,由简入繁依次实现提示组件的各个功能。
基本功能
最基本的功能当然是触发后显示,并且能够以某种方式关闭。唯一需要自定义的部分,就是具体显示的内容。所以最开始组件长这样:
<template>
<div class="notification fixed"
v-if="show"
transition="slide">
<div class="delete"
@click="close()"></div>
<div class="content">
{{ options.content }}
</div>
</div>
</template>
<script>
export default {
props: {
options: {
type: Object,
default: () => {
return {}
}
},
show: {
type: Boolean,
default: false
}
},
methods: {
close () {
this.show = false
this.options = {}
}
}
}
</script>
<style scoped lang="sass">
.slide-transition
transition: all .3s ease
transform: translate3d(0, 0, 0)
.slide-enter,
.slide-leave
transform: translate3d(0, -100%, 0)
.delete
-moz-appearance: none
-webkit-appearance: none
background: rgba(51,51,51,0.2)
cursor: pointer
display: inline-block
height: 24px
position: relative
vertical-align: top
width: 24px
float: right
&:before,
&:after
background: #fff
content: ""
display: block
height: 2px
left: 50%
margin-left: -25%
margin-top: -1px
position: absolute
top: 50%
width: 50%
&:before
transform: rotate(45deg)
&:after
transform: rotate(-45deg)
&:hover
background: rgba(51,51,51,0.5)
.notification
width: 100%
line-height: 2
z-index: 3
position: fixed
top: 0
left: 0
.content
padding: .75rem 2rem
</style>
思路很简单,props传递两个数据,show用于控制显示,options传入包括内容在内的自定义内容。为了让提示的显示更加自然,添加了一个滑动进入和离开的transition。
注意:这里的关闭按钮是通过css实现的,如果在你的项目中有对应的icon,可以将其替换掉。
在此处,也可以使用slot来进行内容的传递,但考虑到之后还有别的参数需要传递至组件内,一次用一个统一的对象options进行传递。
自定义样式
通常提示的内容种类很多,有的是成功提示,有的是警告,有的则是报错。因此我们需要定义不同的样式以表达不同的内容。
方法很简单,在options中传入背景色和文字颜色两个参数,如果组件中检测到了传入的样式参数,就用其替换默认样式。
Vue.js在处理动态样式时非常灵活,为了让代码更清晰,我没有选择将动态样式内联,而是单独使用一个计算属性setStyle进行设定:
computed: {
setStyle () {
return {
color: this.options.textColor || '#fff',
background: this.options.backgroundColor || '#21e7b6'
}
}
}
这样一来,只要在options中一并传入textColor和backgroundColor两个属性,就可以轻松自定义提示样式了。
自动关闭
很多时候,我们希望提示在一定时间之后可以自动关闭,因此组件也需要扩展出一个自动关闭的模式。同样的,在“数据驱动”的思想下,我们应该提供一个数据,用来表明这个提示是否自动关闭。
options中的autoClose属性就是这个作用。同样的,自动关闭的延迟时间显然也要能够自定义,因此还一同添加了showTime这一属性。
自动关闭本身不太复杂,我们只需要使用setTimeout,定义一个计时器即可。
首先是监听提示组件的显示。
在这里,我通过watch监听options的变化来处罚计时器。由于我们已经定义了一个close方法用于关闭计时器,并且在关闭时重置了show和options的值,所以在options变化时,只需要判断options中的autoClose是否为true,就能知道是否需要启动计时器了。这里单独使用一个countdown方法来处理定时器相关的操作。
新增代码如下:
data () {
return {
timers: []
}
},
methods: {
countdown () {
if (this.options.autoClose) {
const t = setTimeout(() => {
this.close()
}, this.options.showTime || 3000)
this.timers.push(t)
}
}
},
watch: {
options () {
this.timers.forEach((timer) => {
window.clearTimeout(timer)
})
this.timers = []
this.countdown()
}
}
细心地你肯定会发现,这段代码中,有一些奇怪的处理。我们定义了一个空数组timers,并且每次开始一个计时器的时候,就把计时器存入数组中,而每次options变化时,我们也从timers中遍历所有计时器并取消,之后清空timers。
这个做法,主要是为了避免一个计时器还没有结束时,又开始一个新的提示所引发的提示被提前关闭的清空。举个例子,如果没有这样的处理,那么先发出一个自动关闭的提示,在其没自动关闭之前,就再发出一个新的提示。那么第一个提示的定时器依然会错误的关闭新提示。
这样的问题主要是由于我们所有的计时器都是在同一个组件中,本质上都是同一个提示,因此需要清除计时器,避免冲突。许多组件库中类似的功能组件,是采用每一条提示就新生成一个提示组件的方式来实现的。但是那样在多个提示连续出现时,就会出现堆叠在一起,又各自离开的情况。
之前的版本中,我的提示组件也采用了类似的设计方式,但是在最近的一个项目中,需要实现半透明的提示组件,就出现了堆叠后看不清提示文字的现象,才使用了现在新的模式。
进一步扩展
紧接着,我拓展了一个自动关闭模式下的倒计时条功能。思路上没有使用Vue.js的transition系统,而是采用了Css3本身的动画系统。在一个自动关闭的提示被初始化时,为计时条添加一个样式,效果是向X轴负方向移动100%,transition时间则通过计算属性对应设定。具体实现可以参考源代码,这里不多做赘述。
增强灵活性
最后则是让提示组件更灵活。有的时候,我们想展示的可能是可以自定义样式的文本、亦或是一个超链接甚至更多。而Vue.js实现起来不要太简单。我们只需要将组件中用于渲染的{{ options.content }}
变为{{{ options.content }}}
即可,对于3重花括号的模板,Vue.js会将其中的HTML标签按照正常内容渲染。
如此一来,我们就可以将任何HTML内容放入提示中了。当然一定要注意避免将用户输入的内容渲染到3重花括号的模板中,避免XSS攻击。
结合vuex
很多时候,我们会把提示组件引入到App.vue这个根组件中,但是发出提示的可能是组件树中的任何一个组件。如果不想代码中遍布各种dispatch和broadcast,那么引入vuex来进行管理是个很好的方案。
大致的思路如下:
// store.js
const state = {
show: false,
options: {
autoClose: false,
content: 'notice content'
}
}
const mutations = {
NEW_NOTICE (state, options) {
state.show = true
state.options = options
},
CLOSE_NOTICE (state) {
state.show = false
state.options = {}
}
}
// actions.js
export const newNotice = ({dispatch}, options) => {
dispatch('NEW_NOTICE', options)
}
export const closeNotice = ({dispatch}) => {
dispatch('CLOSE_NOTICE')
}
// Notification.vue
vuex: {
getters: {
show: state => state.show
options: state => state.options
},
actions: {
close: closeNotice
}
}
// 任意调用notice的组件
vuex: {
actions: {
notice: newNotice
}
}
引入vuex后,按上述代码进行配置,就可以在任意一处组件中,使用this.notice({options})
传递数据。不过由于vuex的单项数据流动特性,所有对state数据的操作都必须经过actions调用mutations实现,包括提示组件中的close方法也要替换成actions中的closeNotice方法。
综述
通过这个提示组件,我们更熟练的掌握了Vue.js的组件系统、数据传递、计算属性、transition动画等特性。另外此组件已经可以直接用于生产环境中,欢迎star、fork、pr。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。