bigo-frontend / blog

👨🏻‍💻👩🏻‍💻 bigo前端技术博客
https://juejin.cn/user/4450420286057022/posts
MIT License
129 stars 9 forks source link

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

Open miaowwwww opened 3 years ago

miaowwwww commented 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() {

        }
    });
}

优点:

缺点:

实现

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

抽象封装

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

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

抽象的方法

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;
}

### 问题整理 
**1. 在使用Decorator @的方式调用`HelloWorld.instanceRender`会触发TS的报错** 
> 原因是装饰器的实现就是原封不动的返回入参。 

```js
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

解决方法:

2. 关闭弹窗的实例方法instanceClose, 会触发TS报错
解决方法:在HelloWorld中给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

import { Component, Vue } from "vue-property-decorator";
@Compnoent
class HelloWorld extends Vue {
    // -
}

其他

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

最后回顾一下发展历程

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

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

PS

文中出现都是代码块。重在传递思路。

InstanceRender不仅仅适用于弹窗。而是任何想要高内聚,低耦合,又脱离主视觉的业务,都可以考虑使用。