插件化-组件抽象 | 前端实践选题

85 阅读8分钟

插件化

一个组件的功能是多样的,或者说是由一个基本的框架然后添加多个小的功能实现的。但是我们在日常搭建组件的时候,会将所有的已经确定的需求全部写在组件内部。如下图:

image-20241107225133852.png 那我们来思考一下,这样是否是一个好的开发模式?今天要实现123这个三个功能,我一股脑全写在一个js文件或者说一个类里面,对于当天的我以及当天的任务需求来说是没问题的。但是为什么说是“我”和“当天”呢?假如一周后,是另外一个同学拿到了这份代码,可能看到的是这样的场景

image-20241107225607931.png 小杰:功能2在哪里实现?新的需求是删掉功能2,我删掉的代码会不会影响到其他功能?新功能我要写到哪里?

没错,即使不换位思考,其实个人开发也会遇到上面小杰的情况:核心就是多个功能的代码混杂在一起,删除添加麻烦。现在我就来介绍一下插件化的编程思想:

解耦

  1. 将控制元素抽取成插件
  2. 插件与组件之间通过注入依赖的方式建立联系

在正式介绍代码实现的之前,请让我用非常通俗的话解释一下上面两种方式的区别:

前者将所有的功能都杂合在一起,就像一个封闭的仪器:对内而言,所有的功能都相互依赖(即使不是真正的依赖,开发人员也会下意识的认为相互依赖),增加新的或者删除旧的功能都需要十分小心;对外而言,因为所有的功能都“藏”在一起内部,外面的人员也很难分析各个功能的实现,也不可能轻易的增加删除某一个功能。

后者插件化,像一个多接口的设备:对内而言,设备里面只有一些非常简单的数据(比如我是谁,我在那...)和基础功能部件(输出自身的信息,启动关闭设备),内部已经是一个非常简单的结构,也不怎么需要维护,如果需要增加功能只需要提供无限多的接口,利用设备本身的信息功能拓展自身功能即可,如果要删除直接把对应插入的功能拔开就可以了;对外而言,我能看到几个插在设备上的线,对插入的设备进行标注(函数名、函数备注等)外面的人就可以非常容易的判断那个线对应哪个功能,如果不要某一个功能了,直接拔线。想要添加功能,把新功能的线往里一插,就可以使用了

image-20241107233530250.png 插件化最核心的思想就是解耦,将控制元素抽取成插件,然后通过插件与组件之间依赖注入的方式进行联系。上文中一个个可以通过插线方式连接的功能/设备就是我们抽取出来的插件,而外设和本体之间的联系需要”插头“来建立。这个“插入”的过程就是组件的组册,注册之后的组件。

代码演示

页面Html

简单敲一个页面,显示一个广告页和一个元素盒。其中广告的display:none;,我们将广告显示权交给对应的插件:默认不显示,你需要这个功能再让它显示出来。(实际广告的透明度为1不透明,为了演示我才让它透明且显示)

为了代码的可读性,我们使用外联式引入Js代码

image-20241111110353597.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>一个不断完善的盒子</title>
</head>
<style>
    * {
        padding: 0;
        margin: 0;
        transition: all 0.7s;
        box-sizing: border-box; /* 避免 padding 和 border 影响尺寸 */
    }

    html, body {
        width: 100%;
        height: 100%;
        overflow: hidden; /* 防止滚动条 */
    }

    #Container {
        width: 100%;
        height: 100vh;
        background-color: #E1BEE7;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    div {
        width: fit-content;
        height: fit-content;
        padding: 24px;
        background-color: hsla(210, 90%, 50%, 0.7);
        border-radius: 10px;
    }

    #Container .redClass {
        background-color: hsla(0, 80%, 50%);
    }

    #slogan {
        display: none;
        width: 778px;
        height: 600px;
        position: absolute;
        background-color: hsla(21, 90%, 50%);
    }

</style>
<body>
    <div id="Container">
       <div id="targetDiv" >Hello!I am a Div.</div>
        <div id="slogan">
            <h2>我是广告 点击关闭我</h2>
        </div>
    </div>
</body>
<!--选择使用哪一种实现方式-->
<!--<script src="InjectedJs.js"></script>-->
<script src="NormalJs.js"></script>
</html>

NormalJs.js

我们最基础也是最近常实现一个组件的方法,都是像瀑布一样的开发。下面实现的功能都是一些非常简单的功能。

相对来说,各个功能也分得比较明确,但在实际学习过程中,肯定不会那么分明,比如:人们习惯把变量定义写在前面....。如果我需要删除某一个功能,其实是很麻烦的,如果不小心删除错误,可能会影响其他的功能甚至影响整个组件的使用。

image-20241111112249449.png

let tDiv = document.querySelector('#targetDiv');

// 实现功能一:页面加载五秒钟之后切换颜色颜色(添加样式类)
let changeToClass = "redClass" //添加样式的className
setTimeout(()=>{
    tDiv.classList.add(changeToClass);
    console.log("已经过了五秒钟啦~~");
},5000)

//功能二:添加广告的功能
let sloganDiv = document.querySelector('#slogan');
sloganDiv.style.display = 'block';
sloganDiv.onclick = () => {
    console.log("广告关闭")
    sloganDiv.style.display = 'none';
}

//功能三:点击实现复制targetDiv里面的内容到粘贴板
let copyText = "Hello World"
tDiv.addEventListener('click', e => {
    alert('clicked!');
    console.log(copyText)
    navigator.clipboard.writeText(copyText).then(function() {
        console.log('内容已复制到剪贴板');
    }).catch(function(err) {
        console.error('复制失败: ', err);
    });
})

InjectedJs.js

组件抽象

在讲注入式注册组件之前,还需要说明一个组件的抽象,说白了就是把一个组件写成一个类class,把其中的一个个功能抽象成方法。如果掌握组件的抽象,其实就可以将代码优化得很好了,逻辑清晰,代码结构明确。但是对于功能的增删以及会产生非常大代码量的构造函数,单单有组件抽象的思想还是有一定的缺陷。具体大家可以试一下,只用组件抽象实现上面的三个功能。

插入化

本次我们将最外面的盒子id:Container作为一个组件,里面的功能抽象成三个插件。分别是:PluginDivClass,feature1, feature2,feature3。至于我们如何实例化对象,并且注入式注册我们所需要的功能:创建实例的时候输入需要绑定的id,然后调用实例的注册插件的方法,将需要插入的函数写进去。

//创建实例
let myDiv = new PluginDivClass("#Container");
//注册需要实现的插件/功能
myDiv.registerPlugins(feature1, feature2,feature3);
//如果要删除feature2,直接不注册feature即可,方便快捷
myDiv.registerPlugins(feature1,feature3);
//如果要添加一个feature4也是很方便
myDiv.registerPlugins(feature1,feature2,feature3,feature4);

说完如何使用注入式注册组件,我们再将如何实现就比较明确了。首先我们我需要创建一个组件类PluginDivClass,这个类的构造函数可以绑定DOM元素,同时PluginDivClass类有一个方法可以注册传入的方法;然后就是实现这个三个功能的函数...

PluginDivClass

构造函数传入的id就是对应DOM的选择器,将成员container绑定到对应的DOM;注册方法:我们使用剩余参数语法...plugins,可以不限制传入的注册的组件数量,传入的函数都在plugins中。遍历所有注册的函数,调用这些函数,同时把实例本身指向的this传递到函数内部,相当于遍历的每一个函数都能获取到实例对象的数据。

class PluginDivClass{
    //初始化数据,传入ID就是元素的获取ID
    constructor(id){
        this.container = document.querySelector(id);
    }
    //注入式注册插件/功能,传入的plugins是一个个函数,每一个函数都传入实例对象this
    registerPlugins(...plugins){
        plugins.forEach(plugin => plugin(this));
    }
}

feature

我们就拿一个feature实现来解释。功能实现逻辑和Normal.js里面的实现是一样的,不一样的是,该函数接收的是一个组件实例化对象,其中的div.container就是使用querySelector获取到的标签(div.container = document.querySelector(id);)

// 实现功能一:页面加载五秒钟之后切换颜色颜色(添加样式类)
// 创建一个函数,传入的参数就是,实例对象this
function feature1(div){
    //div.container就是使用querySelector获取到的标签
    let targetDiv = div.container.querySelector('#targetDiv');
    let changeToClass = "redClass" //添加样式的className
    setTimeout(()=>{
        targetDiv.classList.add(changeToClass);
        console.log("已经过了五秒钟啦~~");
    },5000)

}

总结

插件化就是将一个组件抽象成一个组件类,将需要实现的功能抽象成一个个插件,使用注入式注册的方式,将插件注册到组件内部。便于增加或者删除功能的组件搭建思想

完整代码

class PluginDivClass{
    //初始化数据,传入ID就是元素的获取ID
    constructor(id){
        this.container = document.querySelector(id);
    }
    //注入式注册插件/功能,传入的plugins是一个个函数,每一个函数都传入实例对象this
    registerPlugins(...plugins){
        plugins.forEach(plugin => plugin(this));
    }
}

// 实现功能一:页面加载五秒钟之后切换颜色颜色(添加样式类)
// 创建一个函数,传入的参数就是,实例对象this
function feature1(div){
    //div.container就是使用querySelector获取到的标签
    let targetDiv = div.container.querySelector('#targetDiv');
    let changeToClass = "redClass" //添加样式的className
    setTimeout(()=>{
        targetDiv.classList.add(changeToClass);
        console.log("已经过了五秒钟啦~~");
    },5000)

}

//功能二:添加广告的功能
function feature2(div){
    let _container = div.container
    let sloganDiv = _container.querySelector('#slogan');
    sloganDiv.style.display = 'block';
    sloganDiv.onclick = () => {
        console.log("广告关闭")
        sloganDiv.style.display = 'none';
    }
}

//功能三:点击实现复制targetDiv里面的内容到粘贴板
function feature3(div){
    let targetDiv = div.container.querySelector('#targetDiv');
    let copyText = "Hello World"
    targetDiv.addEventListener('click', e => {
        alert('clicked!');
        console.log(copyText)
        navigator.clipboard.writeText(copyText).then(function() {
            console.log('内容已复制到剪贴板');
        }).catch(function(err) {
            console.error('复制失败: ', err);
        });
    })
}
//创建实例
let myDiv = new PluginDivClass("#Container");
//注册需要实现的插件/功能
myDiv.registerPlugins(feature1, feature2,feature3);