Open miaowwwww opened 3 years ago
一般情况下我们在使用框架时(react、vue、angular)都是创建一个实例,然后所有的页面都写在#app一个容器内。这样可能会导致一些本改高复用,高解耦的弹窗类组件,在使用上变得麻烦/复杂。 本文尝试通过重新实例化Vue组件的方式,让脱离主视觉的弹窗类组件,大幅地降低组件和调用方的逻辑耦合。通过函数式的调用组件,极大的提高组件的可阅读性。同时满足开闭原则,对组件的二次开发也更容易
现有的弹窗组件,在组件复用、与父组件的控制耦合、父组件和弹窗组件的通信,都没有让人满意,存在更优解。
一般情况下我们实现一个弹窗组件
// 伪代码 const template = `<div> <Modal :visible="visible" :params1="params1" :params2="params2" @success="onSuccess" @close="onCloase" /> </div>`; import Modal from './Modal.vue'; import { Component, Prop, Vue } from "vue-property-decorator"; @Component export default class App extends Vue { visible = false; modalParams = {}; onOpen() { this.modalParams = {}; this.visible = true; } onSuccess() { // ... this.onClose(); }, onClose() { this.visible = false; this.modalParams = null; } }
缺点:
参考antd的Modal.info组件,其实我们完全可以在需要的使用的时候,直接创建dom元素,并实例化一个新的Vue实例
改造后使用弹窗类组件的方式
import Modal from './modal'; showModal() { const vm = Modal.instanceRender({ modalParams1, modalParams2 onCallback() { } }); }
优点:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
如何实现这样一个状态自治、方便使用的Modal组件
给组件创建一个静态方法InstanceRender,用于创建实例
class InstanceRenderClass extends Vue { static instanceRender(options: ComponentOptions<Vue>) { // 创建vue实例,可组件内固定部分的参数 const instance = new this({ el: document.createElement('div'), ...options, // data: {visible: true, params1: '' }, // i18n: i18n, // store: store, }); // 把实例添加到dom document.body.appendChild(instance.$el); // *如果全局唯一,可存贮实例,通过状态控制显示隐藏,减少创建实例成本* } }
export class InstanceRenderClass extends Vue { // 根据需要隐藏元素、销毁组件、移除dom元素、调用回调 instanceClose() { this.visible = false; this.$destroy(); this.$el.remove(); } }
基于DRY原则,对于下面两点进行抽象封装还是相当有必要的。
每一个函数式调用的组件都仅是增加了两个方法:InstanceRender & instanceClose 业务参数的输入、固定参数的输入
InstanceRender & instanceClose
import Vue, { VueConstructor } from 'vue'; import { VueClass } from 'vue-class-component/lib/declarations'; import { ComponentOptions } from 'vue/types/options'; interface IInstanceRender { instanceRender: (options: ComponentOptions<Vue>) => InstanceType<VueConstructor> } /** * 基础实现 * @param Component 想要渲染的目标组件 * @returns VueClass */ export function InstanceRender<VC extends VueClass<Vue>, NVC extends VC & IInstanceRender>( Component: NVC ): NVC { Component.instanceRender = function ( options: ComponentOptions<Vue> ) { const instance = new Component({ el: document.createElement('div'), ...options, // i18n: options.i18n, // store: options.store, // route: options.route, // data: options.data, }); document.body.appendChild(instance.$el); return instance; }; // 若需要特殊逻辑,可以在Component组件中重写实现 Component.prototype.instanceClose = function () { this.$destroy(); this.$el.remove(); }; return Component as NVC; }
通过装饰器的使用方式
/* 使用装饰器的方式调用 */ @InstanceRender @Component({ mixins: [lockBodyScrollMixin], data: () => ({ ctitle: "ctitle" }), }) export default class HelloWorldWithDecorator extends Vue { // 如果通过装饰器@的方式使用InstanceRender,则对 instanceRender进行声明是必须的。原因如下 // declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; static instanceRender: any; instanceClose: any; private handleClose() { this.instanceClose(); } }
基于方法的使用方式
/* 使用函数的方式调用 */ @Component({ data: () => ({ ctitle: "ctitle" }) }) class HelloWorld extends Vue { // declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; instanceClose: any; private handleClose() { this.instanceClose(); } created() { console.log('create in hellow'); } } export default InstanceRender(HelloWorld);
### 问题整理 **1. 在使用Decorator @的方式调用`HelloWorld.instanceRender`会触发TS的报错** > 原因是装饰器的实现就是原封不动的返回入参。 ```js declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
解决方法:
2. 关闭弹窗的实例方法instanceClose, 会触发TS报错 解决方法:在HelloWorld中给instanceClose声明。
instanceClose
3. 为什么不使用继承的方式给子类增加方法
export class InstanceRenderClass extends Vue { static instanceRender(options: ComponentOptions<Vue>) { } instanceClose() { } } @Component class HelloWorld extends InstanceRenderClass { // -- }
原因: 这是一种失败的方式,@Component 之后的组件中,不存在instanceRender方法.因为 vue-property-decorator 中的 @Component默认了直接父类就是Vue,因此他认为所有的属性都在当前的class中,实例化时就不会获取原型链上的静态属性。参考源代码可见。 如有兴趣可以尝试一下vue-class
@Component
instanceRender
vue-property-decorator
vue-class
import { Component, Vue } from "vue-property-decorator"; @Compnoent class HelloWorld extends Vue { // - }
/** * 对于用的上InstanceRender的组件,一般是fixed的全屏弹窗之类的,因此一般还需要展示之后禁止页面的滚动 * 希望InstanceRender纯粹一点就不给它增加参数加入其中了 */ export const lockBodyScrollMixin = { created() { document.body.style.overflow = "hidden"; }, beforeDestroy() { document.body.style.overflow = "initial"; }, }
最终回顾解决方案的时候会发现:原始问题的优先级并不高,而且整个过程并没有复杂度比较高的环节。但是通过一步一步解决下来,还是有触摸到自己的盲区,并且最后的成果还是相当有建设性的。
文中出现都是代码块。重在传递思路。 InstanceRender不仅仅适用于弹窗。而是任何想要高内聚,低耦合,又脱离主视觉的业务,都可以考虑使用。
文中出现都是代码块。重在传递思路。
InstanceRender不仅仅适用于弹窗。而是任何想要高内聚,低耦合,又脱离主视觉的业务,都可以考虑使用。
InstanceRender
写在前面
一般情况下我们在使用框架时(react、vue、angular)都是创建一个实例,然后所有的页面都写在#app一个容器内。这样可能会导致一些本改高复用,高解耦的弹窗类组件,在使用上变得麻烦/复杂。
本文尝试通过重新实例化Vue组件的方式,让脱离主视觉的弹窗类组件,大幅地降低组件和调用方的逻辑耦合。通过函数式的调用组件,极大的提高组件的可阅读性。同时满足开闭原则,对组件的二次开发也更容易
现状
现有的弹窗组件,在组件复用、与父组件的控制耦合、父组件和弹窗组件的通信,都没有让人满意,存在更优解。
一般情况下我们实现一个弹窗组件
缺点:
无法同时render多个modal实例优化
参考antd的Modal.info组件,其实我们完全可以在需要的使用的时候,直接创建dom元素,并实例化一个新的Vue实例
改造后使用弹窗类组件的方式
优点:
缺点:
实现
如何实现这样一个状态自治、方便使用的Modal组件
给组件创建一个静态方法InstanceRender,用于创建实例
抽象封装
基于DRY原则,对于下面两点进行抽象封装还是相当有必要的。
抽象的方法
通过装饰器的使用方式
基于方法的使用方式
解决方法:
2. 关闭弹窗的实例方法
instanceClose
, 会触发TS报错解决方法:在HelloWorld中给
instanceClose
声明。3. 为什么不使用继承的方式给子类增加方法
原因: 这是一种失败的方式,
@Component
之后的组件中,不存在instanceRender
方法.因为vue-property-decorator
中的@Component
默认了直接父类就是Vue,因此他认为所有的属性都在当前的class中,实例化时就不会获取原型链上的静态属性。参考源代码可见。 如有兴趣可以尝试一下vue-class
其他
instanceRender
静态方法,缓存实例等操作,可以有效提高重复渲染的效率。最后回顾一下发展历程
最终回顾解决方案的时候会发现:原始问题的优先级并不高,而且整个过程并没有复杂度比较高的环节。但是通过一步一步解决下来,还是有触摸到自己的盲区,并且最后的成果还是相当有建设性的。
PS