共计 4955 个字符,预计需要花费 13 分钟才能阅读完成。
不甘做轮子的搬运工!!!
记录一个实习菜鸟写图片预览组件的艰辛道路~
elementUI 很多组件中使用了指令模式和服务模式,比如:loding、message…
以下以 loading 组件为例:
指令模式:
<template>
<div :v-loading.fullscreen=”true”> 全屏覆盖 </div>
</template>
服务模式:
const loading = this.$loading({
lock: true,
text: ‘Loading’,
spinner: ‘el-icon-loading’,
background: ‘rgba(0, 0, 0, 0.7)’
});
跟大多数萌新一样,啥是服务?!
先看看 elmentUI 的目录结构:
打开 node_modules 目录,找到其下 elementUI 目录:
element-ui\src\index.js 文件中有一大坨组件注册信息, 重点找到我们要找的 loading…
// …
// directive 指令装载
Vue.use(Loading.directive)
// prototype 服务装载
Vue.prototype.$loading = Loading.service
// …
Vue.use() 这个指令是 Vue 用来安装插件的,如果传入的参数是一个对象,则该对象要提供一个 install 方法,如果是一个函数,则该函数被视为 install 方法,在 install 方法调用时,会将 Vue 作为参数传入。
开始叭!
先看看 loading/index.js 文件中是什么鬼?
// 引入指令文件和服务文件,directive 为指令模式文件,index.js 为服务模式文件
import directive from ‘./src/directive’;
import service from ‘./src/index’;
export default {
//install 方法注册组件,不在赘述 install 的用法,star-pic-list 图片预览组件文章中已经介绍过
install(Vue) {
Vue.use(directive);
// 在 vue 的原型对象上注册一个 $loading 的对象,这个 $loading 非常眼熟,看上面服务模式的使用,用到了 this.$loading,源头找到了
Vue.prototype.$loading = service;
},
// 引入的 directive 文件
directive,
// 引入的 index.js 文件
service
};
v-loading 指令解析
篇幅太长,其中我们只取 fullscreen 修饰词。
// 引入 .vue 文件
import Vue from ‘vue’
// 引入 loading.vue 基础文件,里面包含的是组件的基础结构,如 html 结构,loading 显示的页面结构都在这里面
import Loading from ‘./loading.vue’
// 后面重点讲解 extend() 构造器
// Vue.extend() 是 vue 构造器,它返回的是一个扩展实例构造器,也就是预设了部分选项的 Vue 实例构造器,
// mask 字面意思是面具,掩饰,可以猜出来,这个通过 Vue.extend(Loading) 返回构造器应该是用于我们 loading 加载时的遮罩层用的
// loading 就是预设选项,就像 vue 示例中,有 components,name,data,methods… 好像有点明白了
const Mask = Vue.extend(Loading)
const loadingDirective = {}
// 还记得 Vue.use() 的使用方法么?若传入的是对象,该对象需要一个 install 属性
loadingDirective.install = Vue => {
// toggleLoading 方法看名字就是切换 loading 显示和隐藏的嘛~
const toggleLoading = (el, binding) => {
// 若绑定值为 truthy 则插入 loading 元素
// binding 值是一个对象,有指令名、指令的绑定值、modifiers 修饰符对象等等等等,具体的可以去了解自定义指令相关内容
if (binding.value) {//binding.value 是绑定的指令值
if (binding.modifiers.fullscreen) {还记得我们插入的指令吗?:v-loading.fullscreen=”true” , .fullscreen 就是修饰符
insertDom(document.body, el, binding) //insertDom 看名字就知道是插入新的元素
}
// visible 是 loading.vue data 里面定义的值
} else {
el.instance.visible = false
}
}
const insertDom = (parent, el, binding) => {
// loading 设为可见
el.instance.visible = true
// appendChild 添加的元素若为同一个,则不会重复添加
parent.appendChild(el.mask)
}
// 在此注册 directive 指令
Vue.directive(‘loading’, {
bind: function(el, binding, vnode) {
// 创建一个子组件,这里和 new Vue(options) 类似
// 返回一个组件实例
const mask = new Mask({
el: document.createElement(‘div’),
// 有些人看到这里会迷惑,为什么这个 data 不按照 Vue 官方建议传函数进去呢?
// 其实这里两者皆可
// 稍微做一点延展好了,在 Vue 源码里面,data 是延迟求值的
// 贴一点 Vue 源码上来
// return function mergedInstanceDataFn() {
// let instanceData = typeof childVal === ‘function’
// ? childVal.call(vm, vm)
// : childVal;
// let defaultData = typeof parentVal === ‘function’
// ? parentVal.call(vm, vm)
// : parentVal;
// if (instanceData) {
// return mergeData(instanceData, defaultData)
// } else {
// return defaultData
// }
// }
// instanceData 就是我们现在传入的 data: {}
// defaultData 就是我们 loading.vue 里面的 data() {}
// 看了这段代码应该就不难理解为什么可以传对象进去了
data: {
fullscreen: !!binding.modifiers.fullscreen
}
})
// 将创建的子类挂载到 el 上
// 在 directive 的文档中建议
// 应该保证除了 el 之外其他参数(binding、vnode)都是只读的
el.instance = mask
// 挂载 dom
// bind 只会调用一次,在 bind 的时候给 el.mask 赋值,因此 el.mask 所指的为同一个 dom 元素
el.mask = mask.$el
// 若 binding 的值为 truthy 运行 toogleLoading
binding.value && toggleLoading(el, binding)
},
update: function(el, binding) {
// 若旧不等于新值得时候(一般都是由 true 切换为 false 的时候)
if (binding.oldValue !== binding.value) {
// 切换显示或消失
toggleLoading(el, binding)
}
},
unbind: function(el, binding) {
// 当组件 unbind 的时候,执行组件销毁
el.instance && el.instance.$destroy()
}
})
}
export default loadingDirective
关于 extend()更多内容请参考这里,非常通熟易懂!
loading 服务方式调用原理
直接看源码:
import Vue from ‘vue’
import loadingVue from ‘./loading.vue’
// 和指令模式一样,创建实例构造器
const LoadingConstructor = Vue.extend(loadingVue)
// 定义变量,若使用的是全屏 loading 那就要保证全局的 loading 只有一个
let fullscreenLoading
// 这里可以看到和指令模式不同的地方
// 在调用了 close 之后就会移除该元素并销毁组件
LoadingConstructor.prototype.close = function() {
setTimeout(() => {
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
this.$destroy()
}, 3000)
}
const Loading = (options = {}) => {
// 若调用 loading 的时候传入了 fullscreen 并且 fullscreenLoading 不为 falsy
// fullscreenLoading 只会在下面赋值,并且指向了 loading 实例
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
// 这里就不用说了吧,和指令中是一样的
let instance = new LoadingConstructor({
el: document.createElement(‘div’),
data: options
})
let parent = document.body
// 直接添加元素
parent.appendChild(instance.$el)
// 将其设置为可见
// 另外,写到这里的时候我查阅了相关的资料
// 自己以前一直理解 nextTick 是在 dom 元素更新完毕之后再执行回调
// 但是发现可能并不是这么回事,后续我会继续研究
// 如果干货足够的话我会写一篇关于 nextTick ui-render microtask macrotask 的文章
Vue.nextTick(() => {
instance.visible = true
})
// 若传入了 fullscreen 参数,则将实例存储
if (options.fullscreen) {
fullscreenLoading = instance
}
// 返回实例,方便之后能够调用原型上的 close() 方法
return instance
}
export default Loading
现学现用 - 写一个点击图片预览的组件试试看
目录结构:
directive.js 是指令模式文件,index.js 是服务模式文件,star-pic-preview.vue 是基础单文件,包含了基础的 html 结构
先看指令模式:
使用:
我直接使用了指令,并没有传参,因为功能简单,默认参数就是 false
<img
src=”http://img5.imgtn.bdimg.com/it/u=3300305952,1328708913&fm=26&gp=0.jpg”
v-pic-preview
>
效果:
点击出现遮罩层,图片居中显示预览
服务模式:
使用:
<img
src=”http://img4q.duitang.com/uploads/item/201502/22/20150222191447_jdBYa.thumb.700_0.jpeg”
@click=”openImagePreview2″
>
methods: {
// 服务方式
openImagePreview2(e) {
// 如果只传图片
this.$picPreview(e.target.src);
// 如果传复杂对象,可以配置遮罩层的背景颜色等 …
// this.$picPreview({
// background: ‘rgba(0, 0, 0, 0.7)’,
// imageUrl: e.target.src,
// });
},
}
效果如上
源码请参考 github 地址:源码地址