一种vue函数式组件的实现思路

本文首发于:github.com/bigo-fronte... 欢迎关注、转载。

写在前面

一般情况下我们在使用框架时(react、vue、angular)都是创建一个实例,然后所有的页面都写在#app一个容器内。这样可能会导致一些本改高复用,高解耦的弹窗类组件,在使用上变得麻烦/复杂。
本文尝试通过重新实例化Vue组件的方式,让脱离主视觉的弹窗类组件,大幅地降低组件和调用方的逻辑耦合。通过函数式的调用组件,极大的提高组件的可阅读性。同时满足开闭原则,对组件的二次开发也更容易

现状

现有的弹窗组件,在组件复用、与父组件的控制耦合、父组件和弹窗组件的通信,都没有让人满意,存在更优解。

一般情况下我们实现一个弹窗组件

js 复制代码
// 伪代码
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;
    }
}

缺点:

  • 耦合度增加:父组件需要存储对应的变量,注册相应的回调函数
  • 复用代码量增加:Modal想要使用的时候,需要重新存贮变量,注册回调
  • 嵌套调用增加复杂度:当多个地方使用同一个Modal组件时,把Modal放到最外层可以减少重复代码,但是在传参时却又得一层一层往上传(不使用使用状态机情况下)
  • 无法同时render多个modal实例

优化

参考antd的Modal.info组件,其实我们完全可以在需要的使用的时候,直接创建dom元素,并实例化一个新的Vue实例

改造后使用弹窗类组件的方式

js 复制代码
import Modal from './modal';
showModal() {
    const vm = Modal.instanceRender({
        modalParams1,
        modalParams2
        onCallback() {
            
        }
    });
}

优点:

  • 解耦:通过显性的prop,大幅度的降低消费者和生产者之间的耦合
  • 弹窗组件的状态自治:例如visible、handleClose
  • 减少父组件的代码量,随处引入随处使用
  • 可多次实例化,实例间可互不干涉
  • 返回VM实例对象,依然在父组件的管控之中(使用入参,基本就不需要VM了)
  • 可阅读:Modal的所有入参都只能通过prop一次传入,避免了变异,降低复杂度
  • 降低学习成本:当你做出一个组件,想要给别人使用的时候。只要写好options入参就可以了。没有套路、没有黑幕。其他部分都可以当成黑盒子。

缺点:

  • 因为是新的Vue实例,主Vue实例原有的基础数据无法继承(例如:i18n,store,等实例属性,实例方法),都需要重新赋值。
  • 如果与调用方需要强耦合,会增加沟通成本(这种建议就不要用instanceRender了)
  • ts兼容问题。使用@装饰器方式,无法增加新字段,还不如函数式调用 declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

实现

如何实现这样一个状态自治、方便使用的Modal组件

  • 给组件创建一个静态方法InstanceRender,用于创建实例
js 复制代码
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);
    
        // *如果全局唯一,可存贮实例,通过状态控制显示隐藏,减少创建实例成本*
    }
}
  • 给组件创建一个关闭的实例方法,用于内部关闭
js 复制代码
export class InstanceRenderClass extends Vue {
  // 根据需要隐藏元素、销毁组件、移除dom元素、调用回调
  instanceClose() {
    this.visible = false;
    this.$destroy();
    this.$el.remove();
  }
}
  • 组件之间的通信。包含两部分:业务状态、vue基础数据(如:i18n等实例属性&方法)
    • 通过options.data传参。建议!单向传参可降低耦合
    • 通过桥梁。项目使用vuex后,状态都在store中。把store传给新的vue实例。

抽象封装

基于DRY原则,对于下面两点进行抽象封装还是相当有必要的。

  1. 每一个函数式调用的组件都仅是增加了两个方法:InstanceRender & instanceClose
  2. 业务参数的输入、固定参数的输入

抽象的方法

js 复制代码
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;
}
  • 通过装饰器的使用方式
js 复制代码
/* 使用装饰器的方式调用 */
@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();
  }
}
  • 基于方法的使用方式
js 复制代码
/* 使用函数的方式调用 */
@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;

解决方法:

  • 使用@,并在HellowWorld组件中声明 instanceRender,
  • 通过函数的方式调用。函数调用的方式会正确给Component增加静态属性

2. 关闭弹窗的实例方法instanceClose, 会触发TS报错

解决方法:在HelloWorld中给instanceClose声明。

3. 为什么不使用继承的方式给子类增加方法

js 复制代码
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

js 复制代码
import { Component, Vue } from "vue-property-decorator";
@Compnoent
class HelloWorld extends Vue {
    // -
}

其他

  1. 使用InstanceRender的场景,一般是弹窗(规则,创建,详情,confirm)。都是一些fixed的位置,因此你可能会需要禁止body滚动。
js 复制代码
/**
 * 对于用的上InstanceRender的组件,一般是fixed的全屏弹窗之类的,因此一般还需要展示之后禁止页面的滚动
 * 希望InstanceRender纯粹一点就不给它增加参数加入其中了
 */
export const lockBodyScrollMixin = {
    created() {
        document.body.style.overflow = "hidden";
    },
    beforeDestroy() {
        document.body.style.overflow = "initial";
    },
}
  1. 通过修改instanceRender静态方法,缓存实例等操作,可以有效提高重复渲染的效率。
  2. 组件中可能会出现数据字典等基础请求,设置缓存是很有必要的(或者直接传参)

最后回顾一下发展历程

  1. 发现问题:使用弹窗类组件,需要声明多个与调用方无关的变量、方法。并且多页面使用需要多次声明。
  2. 寻找方向:参考了经常会使用的antd:Moda.info(),直接阅读源码
  3. 方案:给组件重新实例化的方式,实现状态自治
  4. 优化:封装成@InstanceRender,并解决遇到的问题
  5. feature...

最终回顾解决方案的时候会发现:原始问题的优先级并不高,而且整个过程并没有复杂度比较高的环节。但是通过一步一步解决下来,还是有触摸到自己的盲区,并且最后的成果还是相当有建设性的。

PS

文中出现都是代码块。重在传递思路。
InstanceRender不仅仅适用于弹窗。而是任何想要高内聚,低耦合,又脱离主视觉的业务,都可以考虑使用。

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。

相关推荐
你挚爱的强哥3 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
susu10830189114 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
天天进步20156 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员7 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
想自律的露西西★8 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳8 小时前
vue3:瀑布流
前端·javascript·vue.js
程序媛-徐师姐9 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
余道各努力,千里自同风10 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave10 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习