本文首发于个人博客。欢迎点击以得到更好的阅读效果~javascript
以前在写fj-service-system的时候,遇到了一些问题。那就是我有些组件,好比Dialog
、Message
这样的组件,是引入三方组件库,好比element-ui
这样的,仍是本身实现一个?虽然它们有按需引入的功能,可是总体风格和个人整个系统不搭。因而就能够考虑本身手动实现这些简单的组件了。html
一般咱们看Vue的一些文章的时候,咱们能看到的一般是讲Vue单文件组件化开发页面的。单一组件开发的文章相对就较少了。我在作fj-service-system项目的时候,发现其实单一组件开发也是颇有意思的。能够写写记录下来。由于写的不是什么ui框架,因此也只是一个记录,没有github仓库,权且看代码吧。vue
主要讲三种方式调用组件:java
v-model
或者.sync
显式控制组件显示隐藏在写组件的时候不少写法、灵感来自于element-ui,感谢。git
我习惯把这个东西叫作对话框,实际上还有叫作modal(弹窗)组件的叫法。其实就是在页面里,弹出一个小窗口,这个小窗口里的内容能够定制。一般能够用来作登陆功能的对话框。github
这种组件就很适合经过v-model
或者.sync
的方式来显式的控制出现和消失。它能够直接写在页面里,而后经过data去控制——这也是最符合Vue的设计思路的组件。element-ui
为此咱们能够写一个组件就叫作Dialog.vue
api
<template>
<div class="dialog">
<div class="dialog__wrapper" v-if="visble" @clcik="closeModal">
<div class="dialog">
<div class="dialog__header">
<div class="dialog__title">{{ title }}</div>
</div>
<div class="dialog__body">
<slot></slot>
</div>
<div class="dialog__footer">
<slot name="footer"></slot>
</div>
</div>
</div>
<div class="modal" v-show="visible"></div>
</div>
</template>
<script> export default { name: 'dialog', props: { title: String, visible: { type: Boolean, default: false } }, methods: { close() { this.$emit('update:visible', false) // 传递关闭事件 }, closeModal(e) { if (this.visible) { document.querySelector('.dialog').contains(e.target) ? '' : this.close(); // 判断点击的落点在不在dialog对话框内,若是在对话框外就调用this.close()方法关闭对话框 } } } } </script>
复制代码
CSS什么的就不写了,跟组件自己关系比较小。不过值得注意的是,上面的dialog__wrapper
这个class也是全屏的,透明的,主要用于获取点击事件并锁定点击的位置,经过DOM的Node.contains()
方法来判断点击的位置是否是dialog自己,若是是点击到了dialog外面,好比半透明的modal
层那么就派发关闭事件,把dialog给关闭掉。app
当咱们在外部要调用的时候,就能够以下调用:框架
<template>
<div class="xxx">
<dialog :visible.sync="visible"></dialog>
<button @click="openDialog"></button>
</div>
</template>
<script> import Dialog from 'Dialog' export default { components: { Dialog }, data() { return { visible: false } }, methods: { openDialog() { this.visible = true // 经过data显式控制dialog } } } </script>
复制代码
为了Dialog开启和关闭好看点,你可试着加上<transition></transition>
组件配合上过渡效果,简单的一点过渡动效也将会很好看。
这个组件相似于element-ui
的message(消息提示)。它吸引个人最大的地方在于,它不是经过显式的在页面里写好组件的html结构经过v-model去调用的,而是经过在js里经过形如this.$message()
这样的方法调用的。这种方法虽然跟Vue的数据驱动的思想有所违背。不过不得不说在某些状况下真的特别方便。
对于Notice这种组件,一次只要提示几个文字,给用户简单的消息提示就好了。提示的信息多是多变的,甚至能够出现叠加的提示。若是经过第一种方式去调用,事先就得写好html结构,这无疑是麻烦的作法,并且没法预知有多少消息提示框。而经过js的方法调用的话,只须要考虑不一样状况调用的文字、类型不一样就能够了。
而以前的作法都是写一个Vue文件,而后经过components
属性引入页面,显式写入标签调用的。那么如何将组件经过js的方法去调用呢?
这里的关键是Vue的extend
方法。
文档里并无详细给出extend
能这么用,只是做为须要手动mount
的一个Vue的组件构造器说明了一下而已。
经过查看element-ui
的源码,才算是理解了如何实现上述的功能。
首先依然是建立一个Notice.vue
的文件
<template>
<div class="notice">
<div class="content">
{{ content }}
</div>
</div>
</template>
<script> export default { name: 'notice', data () { return { visible: false, content: '', duration: 3000 } }, methods: { setTimer() { setTimeout(() => { this.close() // 3000ms以后调用关闭方法 }, this.duration) }, close() { this.visible = false setTimeout(() => { this.$destroy(true) this.$el.parentNode.removeChild(this.$el) // 从DOM里将这个组件移除 }, 500) } }, mounted() { this.setTimer() // 挂载的时候就开始计时,3000ms后消失 } } </script>
复制代码
上面写的东西跟普通的一个单文件Vue组件没有什么太大的区别。不过区别就在于,没有props了,那么是如何经过外部来控制这个组件的显隐呢?
因此还须要一个js文件来接管这个组件,并调用extend
方法。同目录下能够建立一个index.js
的文件。
import Vue from 'vue'
const NoticeConstructor = Vue.extend(require('./Notice.vue').default) // 直接将Vue组件做为Vue.extend的参数。新版vue-loader升级后要求加上`.default`,感谢评论指出~
let nId = 1
const Notice = (content) => {
let id = 'notice-' + nId++
const NoticeInstance = new NoticeConstructor({
data: {
content: content
}
}) // 实例化一个带有content内容的Notice
NoticeInstance.id = id
NoticeInstance.vm = NoticeInstance.$mount() // 挂载可是并未插入dom,是一个完整的Vue实例
NoticeInstance.vm.visible = true
NoticeInstance.dom = NoticeInstance.vm.$el
document.body.appendChild(NoticeInstance.dom) // 将dom插入body
NoticeInstance.dom.style.zIndex = nId + 1001 // 后插入的Notice组件z-index加一,保证能盖在以前的上面
return NoticeInstance.vm
}
export default {
install: Vue => {
Vue.prototype.$notice = Notice // 将Notice组件暴露出去,并挂载在Vue的prototype上
}
}
复制代码
这个文件里咱们能看到经过NoticeConstructor
咱们可以经过js的方式去控制一个组件的各类属性。最后咱们把它注册进Vue的prototype上,这样咱们就能够在页面内部使用形如this.$notice()
方法了,能够方便调用这个组件来写作出简单的通知提示效果了。
固然别忘了这个至关于一个Vue的插件,因此须要去主js里调用一下Vue.use()
方法:
// main.js
// ...
import Notice from 'notice/index.js'
Vue.use(Notice)
// ...
复制代码
在看element-ui
的时候,我也发现了一个颇有意思的组件,是Loading
,用于给一些须要加载数据等待的组件套上一层加载中的样式的。这个loading的调用方式,最方便的就是经过v-loading
这个指令,经过赋值的true/false
来控制Loading层的显隐。这样的调用方法固然也是很方便的。并且能够选择整个页面Loading或者某个组件Loading。这样的开发体验天然是很好的。
其实跟Notice的思路差很少,不过由于涉及到directive
,因此在逻辑上会相对复杂一点。
平时若是不涉及Vue的directive
的开发,多是不会接触到modifiers
、binding
等概念。参考文档
简单说下,形如:v-loading.fullscreen="true"
这句话,v-loading
就是directive
,fullscreen
就是它的modifier
,true
就是binding
的value
值。因此,就是经过这样简单的一句话实现全屏的loading效果,而且当没有fullscreen
修饰符的时候就是对拥有该指令的元素进行loading
效果。组件经过binding
的value
值来控制loading
的开启和关闭。(相似于v-model
的效果)
其实loading也是一个实际的DOM节点,只不过要把它作成一个方便的指令还不是特别容易。
首先咱们须要写一下loading
的Vue组件。新建一个Loading.vue
文件
<template>
<transition name="loading" @after-leave="handleAfterLeave">
<div v-show="visible" class="loading-mask" :class={'fullscreen': fullscreen}>
<div class="loading">
...
</div>
<div class="loading-text" v-if="text">
{{ text }}
</div>
</div>
</transition>
</template>
<script> export default { name: 'loading', data () { return { visible: true, fullscreen: true, text: null } }, methods: { handleAfterLeave() { this.$emit('after-leave'); } } } </script>
<style> .loading-mask{ position: absolute; // 非全屏模式下,position是absolute z-index: 10000; background-color: rgba(255,235,215, .8); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-mask.fullscreen{ position: fixed; // 全屏模式下,position是fixed } // ... </style>
复制代码
Loading关键是实现两个效果:
absolute
的话,就将其修改成relatvie
,并插入当前元素下。此时Loading的position就会相对于当前元素进行绝对定位了。因此在当前目录下建立一个index.js
的文件,用来声明咱们的directive
的逻辑。
import Vue from 'vue'
const LoadingConstructor = Vue.extend(require('./Loading.vue').default) // 新版vue-loader升级后要求加上`.default`,感谢评论指出~
export default {
install: Vue => {
Vue.directive('loading', { // 指令的关键
bind: (el, binding) => {
const loading = new LoadingConstructor({ // 实例化一个loading
el: document.createElement('div'),
data: {
text: el.getAttribute('loading-text'), // 经过loading-text属性获取loading的文字
fullscreen: !!binding.modifiers.fullscreen
}
})
el.instance = loading; // el.instance是个Vue实例
el.loading = loading.$el; // el.loading的DOM元素是loading.$el
el.loadingStyle = {};
toggleLoading(el, binding);
},
update: (el, binding) => {
el.instance.setText(el.getAttribute('loading-text'))
if(binding.oldValue !== binding.value) {
toggleLoading(el, binding)
}
},
unbind: (el, binding) => { // 解绑
if(el.domInserted) {
if(binding.modifiers.fullscreen) {
document.body.removeChild(el.loading);
}else {
el.loading &&
el.loading.parentNode &&
el.loading.parentNode.removeChild(el.loading);
}
}
}
})
const toggleLoading = (el, binding) => { // 用于控制Loading的出现与消失
if(binding.value) {
Vue.nextTick(() => {
if (binding.modifiers.fullscreen) { // 若是是全屏
el.originalPosition = document.body.style.position;
el.originalOverflow = document.body.style.overflow;
insertDom(document.body, el, binding); // 插入dom
} else {
el.originalPosition = el.style.position;
insertDom(el, el, binding); // 若是非全屏,插入元素自身
}
})
} else {
if (el.domVisible) {
el.instance.$on('after-leave', () => {
el.domVisible = false;
if (binding.modifiers.fullscreen && el.originalOverflow !== 'hidden') {
document.body.style.overflow = el.originalOverflow;
}
if (binding.modifiers.fullscreen) {
document.body.style.position = el.originalPosition;
} else {
el.style.position = el.originalPosition;
}
});
el.instance.visible = false;
}
}
}
const insertDom = (parent, el, binding) => { // 插入dom的逻辑
if(!el.domVisible) {
Object.keys(el.loadingStyle).forEach(property => {
el.loading.style[property] = el.loadingStyle[property];
});
if(el.originalPosition !== 'absolute') {
parent.style.position = 'relative'
}
if (binding.modifiers.fullscreen) {
parent.style.overflow = 'hidden'
}
el.domVisible = true;
parent.appendChild(el.loading) // 插入的是el.loading而不是el自己
Vue.nextTick(() => {
el.instance.visible = true;
});
el.domInserted = true;
}
}
}
}
复制代码
一样,写完整个逻辑,咱们须要将其注册到项目里的Vue下:
// main.js
// ...
import Loading from 'loading/index.js'
Vue.use(Loading)
// ...
复制代码
至此咱们已经可使用形如
<div v-loading.fullscreen="loading" loading-text="正在加载中">
复制代码
这样的方式来实现调用一个loading组件了。
在用Vue写咱们的项目的时候,不论是写页面仍是写形如这样的功能型组件,其实都是一件颇有意思的事情。本文介绍的三种调用组件的方式,也是根据实际状况出发而实际操做、实现的。不一样的组件经过不一样的方式去调用,方便了开发人员,也能更好地对代码进行维护。固然也许还有其余的方式,我并无了解,也欢迎你们在评论里指出!
最后再次感谢element-ui的源码给予的极大启发。