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

file

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

写在前面

一般情况下我们在使用框架时(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;}
}

缺点:

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

优化

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

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

import Modal from './modal';
showModal() {const vm = Modal.instanceRender({modalParams1,modalParams2onCallback() {}});
}

优点:

  • 解耦:通过显性的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,用于创建实例
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,});// 把实例添加到domdocument.body.appendChild(instance.$el);// *如果全局唯一,可存贮实例,通过状态控制显示隐藏,减少创建实例成本*}
}
  • 给组件创建一个关闭的实例方法,用于内部关闭
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. 业务参数的输入、固定参数的输入

抽象的方法

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的报错

原因是装饰器的实现就是原封不动的返回入参。

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

解决方法:

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

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";},
}
  1. 通过修改instanceRender静态方法,缓存实例等操作,可以有效提高重复渲染的效率。
  2. 组件中可能会出现数据字典等基础请求,设置缓存是很有必要的(或者直接传参)

最后回顾一下发展历程

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

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

PS

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

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

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

我是bigo前端,下期见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/509578.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++入门】C++关键字 | 命名空间 | C++的输入输出

目录 0.C与C 1.C的关键字 2.命名空间 2.1域 2.2C中命名冲突问题 2.3命名空间定义 2.4命名空间使用 2.5命令空间的展开&头文件的展开 3.C的输入&输出 3.1cout&cin 3.1<<流插入运算符 3.2>>流提取运算符 0.C与C C是在C的基础之上&#xff…

设计模式(十四)中介者模式

请直接看原文: 原文链接:设计模式&#xff08;十四&#xff09;中介者模式_设计模式之中介模式-CSDN博客 -------------------------------------------------------------------------------------------------------------------------------- 前言 写了很多篇设计模式的…

【JavaEE进阶】 Linux常用命令

文章目录 &#x1f343;前言&#x1f334;ls 与 pwd&#x1f6a9;ls&#x1f6a9;pwd &#x1f38d;cd&#x1f6a9;认识Linux目录结构 &#x1f340;touch与cat&#x1f6a9;touch&#x1f6a9;cat &#x1f332;mkdir与rm&#x1f6a9;mkdir&#x1f6a9;rm &#x1f384;cp与…

企业必备监管工具:让管理更简单,效率倍增!

微信作为当前广泛使用的沟通工具&#xff0c;成为企业监管的重要对象。因此&#xff0c;使用微信管理系统成为企业必备的监管工具之一。下面就给大家分享微信管理系统的监管功能&#xff0c;让大家的管理更简单、更高效&#xff01; 1、敏感词监控 设置完成后&#xff0c;一旦…

卡莱尔:现在的马刺显然跟之前不一样了 他们近期还击败过雷霆

直播吧指定地址&#xff1a;www.wfzkbzj.com 3月4日讯 步行者今日105-117不敌马刺&#xff0c;赛后&#xff0c;步行者主帅卡莱尔接受媒体采访。 谈及马刺&#xff0c;卡莱尔说道&#xff1a;“现在的马刺显然跟我们之前交手的马刺已经不一样了&#xff0c;他们近期还击败过雷…

【Python笔记-设计模式】状态模式

一、说明 状态模式是一种行为设计模式&#xff0c;用于解决对象在不同状态下具有不同行为 (一) 解决问题 在对象行为根据对象状态而改变时&#xff0c;规避使用大量的条件语句来判断对象的状态&#xff0c;提高系统可维护性 (二) 使用场景 当对象的行为取决于其状态&#…

栈与队列力扣经典例题20. 有效的括号1047. 删除字符串中的所有相邻重复项150. 逆波兰表达式求值

对于栈与队列&#xff0c;我们首先要搞清楚&#xff0c;栈是先入后出&#xff0c;而队列是先入先出&#xff0c;利用这个特性&#xff0c;我们来判断题目用什么STL容器&#xff0c;便于我们去解决问题 20. 有效的括号 这道题&#xff0c;首先我们要知道哪些情况&#xff0c;是会…

需求评审会常见的5大核心问题

需求评审会是项目管理过程中的一个重要环节&#xff0c;其核心问题的顺利讨论和评审&#xff0c;对项目来说非常重要。其有助于项目成员对需求理解达成共识&#xff0c;明确需求的内容、目标和预期结果&#xff0c;尽早发现需求不合理之处&#xff0c;从而能够及时调整和完善&a…

2024六大创业营销趋势,普通人创业新风向!

2024年越来越多的人选择创业&#xff0c;从龙年春节前后&#xff0c;创投圈就开始探讨关于2024的创业新风向&#xff0c;从各个热点&#xff0c;各大品牌&#xff0c;春晚等等方面洞察2024创业趋势&#xff0c;以下总结的6大创业营销趋势&#xff0c;跟着大品牌押宝&#xff0c…

2024034期传足14场胜负前瞻

2024034期售止时间为3月4日&#xff08;周一&#xff09;22点00分&#xff0c;敬请留意&#xff1a; 图片 本期深盘多&#xff0c;1.5以下赔率3场&#xff0c;1.5-2.0赔率5场&#xff0c;其他场次是平半盘、平盘。本期14场整体难度中等。以下为基础盘前瞻&#xff0c;大家可根据…

Springboot配置MySQL数据库

Springboot配置MySQL数据库 一、创建springboot项目&#xff0c;并添加如下依赖 <dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope> </dependency>二、在applica…

【Linux】进程间通信之共享内存

文章目录 引入共享内存的原理共享内存的相关接口shmget()shmat()shmdt()shmctl() 共享内存的简单使用共享内存的特点 引入 进程间通信&#xff0c;顾名思义就是一个进程和另一个进程之间进行对话&#xff0c;以此完成数据传输、资源共享、通知事件或进程控制等。 众所周知&am…