持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
前言
在使用 element-ui
dialog
组件的时候,需要通过一个布尔值去控制 dialog
的显示与隐藏,那么这里就会有一些问题存在,例如弹框关闭后,需要清空数据,否则下次显示弹框时,会保留上次的数据;在例如如果页面中拥有多个弹框,使用布尔值控制就显得繁琐。为了解决这个问题,我们就需要设计一个更加便捷的弹框使用方式。
分析
想要一个更加的便捷的使用方式,我们想到了 message
, confirm
等组件的使用方式,以 message
为例,其使用方式时这样的
this.$message('内容', '提示', options)
这种调用方式就十分简单,那么我们就可以把 dialog
改成这种命令式的方式去调用,从而提升开发体验。
根据这个核心的需要我们可以很快得出我们需要做的事情:
graph TD
命令式Dialog调用 --> 需要提供全局调用API
命令式Dialog调用 --> 需要提供dialog模板内容的渲染
命令式Dialog调用 --> 需要提供dialog参数的传递
需要提供全局调用API --> 显示dialog
需要提供全局调用API --> 关闭dialog
显示dialog --> this.$dialog
关闭dialog --> this.$closeDialog
需要提供dialog模板内容的渲染 --> 传递一个组件
需要提供dialog参数的传递 --> 传递一个对象
传递一个组件 --> 根元素是el-dialog
传递一个组件 --> 根元素是其它任何元素
Vue.extend()
要使用全局组件,就需要使用Vue.extend
。Vue.extend
属于 Vue2
的全局 API
,是vue
的一个构造器,继承自vue
,他创建一个“子类”,在实际业务开发中我们很少使用,因为相比常用的 Vue.component
写法使用 extend
步骤要更加繁琐一些。但是在一些独立组件开发场景中,Vue.extend
+ $mount
这对组合是我们需要去关注的。
全局组件
// main.vue
<template>
<div>
<p>{{firstName}} {{lastName}} == {{alias}}</p>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'Jone',
lastName: 'Brus',
alias: 'JB'
}
},
};
</script>
import main from './main.js'
var Profile = Vue.extend(main)
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#container')
全局数据仓库
在一些特殊场景使用 Vue.extend
,可以当作一个全局数据仓库,例如 element-ui
的 table
组件就用到了这个。
那么使用这个好处是什么呢?
例如有一个页面,这个页面有很多个组件,如果进行采用传统方式,多个父子组件的传参,使用 props
和 emit
,就会显得很麻烦,那么就可以使用 Vue.extend
来简化操作,减少代码量,把Vue.extend
当作一个全局数据仓库来使用,你可以配合watch,computed,method,mixins
等方法
🙋🌰
在 mounted
中模拟请求,给 a,b,c,d
4个属性赋值,a
组件需要 a,b,c
3个属性,b
组件需要 a,b,c,d
4个属性,a
组件有 changeA
和 changeB
事件分别用来改变 a
和 b
的值,b
组件有 changeB
和 changeC
事件分别用来改变 c
和 d
的值,但是需要分别加上 a
和 b
的值。
未使用 Vue.extend
(93 line)
<template>
<div>
<a-component :a="a" :b="b" :c="c" @changeA="changeA" @changeB="changeB" />
<b-component :a="a" :b="b" :c="c" :d="d" @changeC="changeC" @changeD="changeD" />
</div>
</template>
<script>
export default {
data() {
return {
a: 0,
b: 0,
c: 0,
d: 0,
};
},
mounted() {
// 模拟请求
setTimeout(() => {
this.a = 10;
this.b = 20;
this.c = 30;
this.d = 40;
}, 100);
},
methods: {
changeA(a) {
this.a = a;
},
changeB(b) {
this.b = b;
},
changeC(c) {
this.c = c;
},
changeD(d) {
this.d = d;
},
},
};
</script>
a-component
<template>
<div>
<p @click="changeA">a: {{ a }}</p>
<p @click="changeB">b: {{ b }}</p>
<p>c: {{ c }}</p>
</div>
</template>
<script>
export default {
props: {
a: Number,
b: Number,
c: Number,
},
data() {},
methods: {
changeA() {
this.$emit('changeA', 100);
},
changeB() {
this.$emit('changeB', 200);
},
},
};
</script>
b-component
<template>
<div>
<p>a: {{ a }}</p>
<p>b: {{ b }}</p>
<p @click="changeC">c: {{ c }}</p>
<p @click="changeD">d: {{ d }}</p>
</div>
</template>
<script>
export default {
props: {
a: Number,
b: Number,
c: Number,
d: Number,
},
data() {},
methods: {
changeC() {
this.$emit('changeC', 300 + this.a);
},
changeD() {
this.$emit('changeD', 400 + this.b);
},
},
};
</script>
使用 Vue.extend
(87 line)
// watcher.js
import Vue from 'vue';
export default Vue.extend({
data() {
return {
a: 0,
b: 0,
c: 0,
d: 0,
};
},
computed: {},
watch: {},
methods: {
changeC() {
this.c = 300 + this.a;
},
changeD() {
this.d = 400 + this.b;
},
},
});
// store.js
import Store from './watcher';
let store = {};
if (!store.demo) {
store.demo = new Store();
}
export default store;
<template>
<div>
<a-component />
<b-component />
</div>
</template>
<script>
import store from './store'
export default {
data() {
return {};
},
mounted() {
// 模拟请求
setTimeout(() => {
store.demo.a = 10;
store.demo.b = 20;
store.demo.c = 30;
store.demo.d = 40;
}, 100);
},
methods: {},
};
</script>
a-component
<template>
<div>
<p @click="changeA">a: {{ demo.a }}</p>
<p @click="changeB">b: {{ demo.b }}</p>
<p>c: {{ demo.c }}</p>
</div>
</template>
<script>
import store from './store'
export default {
computed: {
demo() {
return store.demo
}
},
methods: {
changeA() {
store.demo.a = 100;
// this.demo.a = 100;
},
changeB() {
store.demo.b = 200;
},
},
};
</script>
b-component
<template>
<div>
<p>a: {{ demo.a }}</p>
<p>b: {{ demo.b }}</p>
<p @click="demo.changeC">c: {{ demo.c }}</p>
<p @click="demo.changeD">d: {{ demo.d }}</p>
</div>
</template>
<script>
import store from './store'
export default {
computed: {
demo() {
return store.demo
}
},
methods: {},
};
</script>
显示dialog
在上面的分析中我们可以把调用dialog
显示的API
的用法先设计好,例如这样
this.$dialog(component, isAnyDialog=false)(props)
.then(data => {})
.catch(data => {})
参数 | 类型 | 说明 |
---|---|---|
component | VueComponent | 弹框内容组件 |
isAnyDialog | boolean | false: component的根组件是 el-dialog ; true: 让所有非 element-dialog 组件都可以变成弹框 |
props | 对象 | 传递给弹框内容组件的参数,当 isAnyDialog 为 true 时,可以传递 el-dialog 组件的默认属性 |
关闭dialog
关闭弹框时调用全局关闭API
this.$closeDialog(state)
参数 | 类型 | 说明 |
---|---|---|
state | boolean | 状态,true: this.$dialog执行then,否则执行catch |
详细代码
我们首先创建一个 store
,用来存放弹框组件和传递给弹框组件的 props
;接着创建一个容器用来挂载这个弹框;如果不是任意弹窗则直接使用传过来的 component
,如果是任意弹窗则 component
就变成我们封装的弹框组件 main.vue
,并且把传入的属性和传入组件存储下来;然后在 main.vue
中使用 component
标签动态加载组件。
设计好 API
就可以实现具体的代码了
// watcher.js
import Vue from 'vue';
export default Vue.extend({
data() {
return {
propsData: {},
component: null,
};
},
computed: {},
watch: {},
methods: {},
});
// store.js
import Store from './watcher';
let store = {};
if (!store.dialog) {
store.dialog = new Store();
}
export default store;
// dialog.js
import Main from './main.vue';
import store from './store';
const openDialog = (Vue, component, isAnyDialog = false) => {
let componentTmp = component;
// 创建容器
const div = document.createElement('div');
// 承载弹框挂在的位置
const el = document.createElement('div');
div.appendChild(el);
document.body.appendChild(div);
if (!component) {
throw new Error('component 组件不能为空');
}
// 如果是 true,则使用 Main 组件
if (isAnyDialog) {
component = Main;
}
// 创建组件类
const ComponentConstructor = Vue.extend(component);
return (propsData = {}, parent = undefined) => {
if (propsData.component) {
throw new Error('this.$dialog 的属性 component 属于内置属性,不能传递 component 属性,入需要传递组件,请使用其它参数名');
}
if (isAnyDialog) {
// 用 dialog store 存储一下
store.dialog.propsData = propsData;
store.dialog.component = componentTmp;
}
let instance = new ComponentConstructor({
// 使用 propsData 对象传递参数,子组件在 props 中可以接收到
propsData,
parent,
}).$mount(el);
// 销毁
const destroyDialog = () => {
if (instance && div.parentNode) {
instance.$destroy();
instance = null;
div.parentNode && div.parentNode.removeChild(div);
}
};
// visible控制
if (instance['visible'] !== undefined) {
// 监听 visible 属性的变化,如果 false 就销毁这个弹框组件,并且从 dom 中移除
instance.$watch('visible', val => {
!val && destroyDialog();
});
// 当组件触发式,显示弹框
Vue.nextTick(() => (instance['visible'] = true));
}
return new Promise((resolve, reject) => {
instance.$once('done', data => {
destroyDialog();
resolve(data);
});
instance.$once('cancel', data => {
destroyDialog();
reject(data);
});
});
};
};
// index.js 代码
import openDialog from './dialog';
function install(Vue) {
if (install.installed) {
return;
}
install.installed = true;
// 挂载
Vue.prototype.$dialog = (comp, isAnyDialog) => openDialog(Vue, comp, isAnyDialog);
// 添加 $closeDialog API
Vue.mixin({
methods: {
$closeDialog(isDone = false, ...args) {
this.$emit(isDone ? 'done' : 'cancel', ...args);
},
},
});
}
// auto plugin install
let GlobalVue = null;
if (typeof window !== 'undefined') {
GlobalVue = window.Vue;
} else if (typeof global !== 'undefined') {
GlobalVue = global.vue;
}
if (GlobalVue) {
GlobalVue.use({
install,
});
}
// export default
export default {
install,
};
main.vue
<template>
<el-dialog :visible.sync="visible" :before-close="handleClose" v-bind="store.dialog.propsData" :width="width">
<component v-if="store.dialog.component" ref="component" v-bind:is="store.dialog.component" v-bind="store.dialog.propsData" title=""> </component>
</el-dialog>
</template>
el-dialog
上就绑定了所有的 propsData
,这样只要 propsData
里面有 对应的 el-dialog
属性就会自动绑定上去,component
也是如此。
结语
vue.extend
的使用非常强大,上文中只是说明了其中一部分作用,除此之外,还可以对某个组件继承,从而实现一些的新的功能,例如 el-table
使用配合 cell-dblclick
就可以在不使用 v-if
的情况下实现单元格的输入框和文本切换,又或者把一个组件变成一个高阶组件,有兴趣的小伙伴可以自行研究。