插件化
一个组件的功能是多样的,或者说是由一个基本的框架然后添加多个小的功能实现的。但是我们在日常搭建组件的时候,会将所有的已经确定的需求全部写在组件内部。如下图:
那我们来思考一下,这样是否是一个好的开发模式?今天要实现123这个三个功能,我一股脑全写在一个js文件或者说一个类里面,对于当天的我以及当天的任务需求来说是没问题的。但是为什么说是“我”和“当天”呢?假如一周后,是另外一个同学拿到了这份代码,可能看到的是这样的场景
小杰:功能2在哪里实现?新的需求是删掉功能2,我删掉的代码会不会影响到其他功能?新功能我要写到哪里?
没错,即使不换位思考,其实个人开发也会遇到上面小杰的情况:核心就是多个功能的代码混杂在一起,删除添加麻烦。现在我就来介绍一下插件化的编程思想:
解耦
- 将控制元素抽取成插件
- 插件与组件之间通过注入依赖的方式建立联系
在正式介绍代码实现的之前,请让我用非常通俗的话解释一下上面两种方式的区别:
前者将所有的功能都杂合在一起,就像一个封闭的仪器:对内而言,所有的功能都相互依赖(即使不是真正的依赖,开发人员也会下意识的认为相互依赖),增加新的或者删除旧的功能都需要十分小心;对外而言,因为所有的功能都“藏”在一起内部,外面的人员也很难分析各个功能的实现,也不可能轻易的增加删除某一个功能。
后者插件化,像一个多接口的设备:对内而言,设备里面只有一些非常简单的数据(比如我是谁,我在那...)和基础功能部件(输出自身的信息,启动关闭设备),内部已经是一个非常简单的结构,也不怎么需要维护,如果需要增加功能只需要提供无限多的接口,利用设备本身的信息功能拓展自身功能即可,如果要删除直接把对应插入的功能拔开就可以了;对外而言,我能看到几个插在设备上的线,对插入的设备进行标注(函数名、函数备注等)外面的人就可以非常容易的判断那个线对应哪个功能,如果不要某一个功能了,直接拔线。想要添加功能,把新功能的线往里一插,就可以使用了
插件化最核心的思想就是解耦,将控制元素抽取成插件,然后通过插件与组件之间依赖注入的方式进行联系。上文中一个个可以通过插线方式连接的功能/设备就是我们抽取出来的插件,而外设和本体之间的联系需要”插头“来建立。这个“插入”的过程就是组件的组册,注册之后的组件。
代码演示
页面Html
简单敲一个页面,显示一个广告页和一个元素盒。其中广告的display:none;,我们将广告显示权交给对应的插件:默认不显示,你需要这个功能再让它显示出来。(实际广告的透明度为1不透明,为了演示我才让它透明且显示)
为了代码的可读性,我们使用外联式引入Js代码
<!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
我们最基础也是最近常实现一个组件的方法,都是像瀑布一样的开发。下面实现的功能都是一些非常简单的功能。
相对来说,各个功能也分得比较明确,但在实际学习过程中,肯定不会那么分明,比如:人们习惯把变量定义写在前面....。如果我需要删除某一个功能,其实是很麻烦的,如果不小心删除错误,可能会影响其他的功能甚至影响整个组件的使用。
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);