js、html和css分离思想,组件化与插件化编写|青训营笔记

572 阅读11分钟

这是我参与「第四届青训营 」笔记创作活动的的第2天

总纲反思篇

和自己想象中要将关于JS的内容差异很大很大,没有JS的语法,没有JS的写法,出乎我的意料,但收获却是无与伦比的多。事后,我是这样思考的,对于那些“墨守成规”的基础性的东西,我们可以也是必须要自己学习牢记的,但是仅仅能写、会写还远远达不到我们想要的优雅代码程度。唯有注入自己思想和灵魂的舞蹈才算一个好得dancer!

在掌握基本语法后,提升思维能力是进阶的关键,接收领悟别人的总结,站在巨人的肩膀上,我们的视野会更开阔!第二天的课程尤为精彩,月影老师的讲解给我看看到了前端世界一个与众不同的精彩面。下面我们用笔记的形式回顾老师所提到的思想和相关demo。

css html js的职能分离

案例引入

现在市面上巨大多数应用都有一个白天和晚上的显示效果切换,我们在可能没有做整体的背景切换,但总会遇到过一些奇奇怪怪的效果切换,比如点击后把哪一个组件的样式切换一下。通常的操作时,我们在点击方法里面获取到目标DOM,由于DOM和JS是完全独立的两个模块,两者的交互是通过API进行的。因此我们在改变样式的时候JS和CSS就耦合在一起了。

JS操作DOM改变

切换前: image.png 点击后:

image.png

逻辑代码:

	<script type="text/javascript">
		const btn=document.getElementById('btn');
		btn.addEventListener('click',(e)=>{
			const essat=document.getElementById('essay');
			if(e.target.innerHTML=='变变变'){
				essat.style.background='black';
				essat.style.color='white';
				e.target.innerHTML='哈哈哈'
			}else{
				essat.style.background='white';
				essat.style.color='black';
				e.target.innerHTML='变变变'
			}
		})
	</script>

分析

直接操作DOM,当遇到需要改变的样式内容有很多条的时候,我们一次一次的去改变样式,存在以下不足:

  1. 首先改变样式渲染可能会触发多次重排或重绘,对于性能极为浪费。解决方法:可以通过一个函数将所有CSS属性以键值对形式,‘;’分割,传入整体字符串,一次性修改完后再去重排或重绘。
  2. 如果遇到修改的需求,每一次都需要在对应方法里面改CSS样式,麻烦!不合理,这就是JS和CSS耦合的副作用。

修改类名切换样式

我们在切换前后实现的效果是相同的,只是我们对应的实现确是不同的。这个案例在后面的效果就参照JS操作DOM改变篇的图片。

逻辑代码:

		<style>
			body {
				transition: all 1s;
			}

			.changed {
				background-color: black;
				color: white;
				transition: all 1s;
			}

			#btn::after {
			 content: '🌞';
			}
			.changed #btn::after {
				content: '🌜';
			}
		</style>
                
	<script type="text/javascript">
		const btn = document.getElementById('btn');
		btn.addEventListener('click', (e) => {
			const body = document.body;
			if (body.className !== 'changed') {
				body.className = 'changed';
			} else {
				body.className = '';
			}
		})
	</script>

分析

这一次我们尝试着将css样式写在外部的一个css类选择器里,这样做的好处有多个,首先就DOM的渲染开销算是最低的了,其次我们在更改样式的时候,只需要在对应的类选择器里做相关操作就可以。看似完美的一批,实则大家都忽略了CSS的强大了,再有优化空间就是,我们将点击逻辑所做的变化全部交给CSS来完成。

纯CSS来实现效果切换

HTML+CSS代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			.content {
				transition: background-color 1s, color 1s;
			}

			#modeCheckBox {
				display: none;
			}

			#btn::after {
				content: '🌞';
			}

			#modeCheckBox:checked+.content {
				background-color: black;
                                 color: white;
				transition: all 1s;
			}

			#modeCheckBox:checked+.content #btn::after {
				content: '🌜';
			}
		</style>
	</head>
	<body>
		<input id="modeCheckBox" type="checkbox">
		<div class="content">
			<label id="btn" for="modeCheckBox"></label>
			<p id="essay">
				牵挂虽无声,却最暖心,牵挂虽无言,却最温柔,牵挂,是情感深处最温婉的涌动,无论相隔千山万水,依然暖在心间,牵挂虽不张扬,却是内心情感最真挚的对白,让真情更浓
				  人生因为有人牵挂,前行的路上不再孤独;四季因为牵挂,没有寒凉,前行的路上,因为彼此牵挂,心不再孤冷;岁月有了牵挂,生命就多了缤纷的色彩。
				  林清玄说:“最大的感恩是,我们生而为有情的人,不是无情的东西。”我们这一生,唯爱永恒,因为有情,所以彼此牵挂,因为有爱,所以足够温暖,有一种牵挂,相顾无言,只是静静陪伴
				  走过生命的历程,谁不曾孤单,看过人间百味,谁不曾寒凉,感谢爱我们的人和我爱的人,感谢牵挂我们的人,感谢曾牵手走过一段路的人,是他们丰盈着我们的情怀,让幸福和甜美总是萦绕在身边。
			</p>
		</div>
	</body>
</html>

分析

这回可是妥妥的把CSS的样式切换交给了HTML和CSS本身,实现了与JS的分离。这里实现的设计是巧妙的将input标签中checkbox类型(复选框)的选中和未选中两种状态实现切换前后的样式变化,至于点击和显示区域则使用了id和for属性,这两个属性能够是的我们在点击lable标签(for)等同于点击input(id)这个两个属性的值需要保持相同。对于切换前后样式类,我们使用了伪类:checked来设置选中后的样式。 注意这个切换具有一定的局限性:

  1. checkbox只有0|1两种状态,即这种0JS的样式切换方案适用于只有两种样式的时候,当切换样式大于2中时候就需要再寻求其他方案了。
  2. 我们在CSS选择器对于HTML的结构有一定的限制,可能你会觉得稍繁琐一些。控制组件和显示内容需要切换的内容保持兄弟节点的关系。例如#modeCheckBox:checked+.content{},这样才可以将checked作为条件去控制后面样式的显示。

小结

HtML、CSS、JS应该尽可能的分离,避免JS直接操控CSS样式变化,利用样式类实现交互方案的设计。

组件封装

案例引入

习惯了使用UI库的我们,可能对于组件封装的概念并不是很深刻,自己做过的也无非是对某个UI库中的租价做一个简易的二次封装,实现一些简单的功能。然而,实际项目中组件重用,轻微改变,重写的代价是比较大的,因此大多使用封装的方法来达到组件的重用,通过插件化的方式组合出我们需要的功能。下面将通过轮播图的一步步实现,来体会组件封装的概念。

效果图

image.png

实现思路分析

基于HTML CSS JS对应骨架、外貌、行为三部分的设计原则,我们首先搭建HTML,通过无序列表ul配合li img将所用到的图片陈列出来;通过CSS绝对定位将所有图片重叠在一起;最后添加行为来控制每一张照片的切换。针对行为控制即JS的书写,我们可以创建一个轮播图类Slider,来存放我们的操作和数据,给每一个轮播图通过ID绑定一个实例对象(Slider)即可。

Slider类: 绑定的DOM节点,实现与轮播图一一对应的关系。通过JS实现每一张图片的切换,其实就是通过定时任务一次顺序的更换显示的图片,由于我们在CSS样式中对所有slider-item都设置了opacity:0;对选中样式slider-item-selected设置opacity:1;通俗的说只有类名为slider-item-selected的图片才会显示。 为了简化操作,分隔功能模块。我们又引入了getSelectItem()获取当前选中DOM节点,getSelectItemIndex()获取选中节点在所有图片节点NodeList数组中的索引,slideTo(idx)显示指定索引的图片,sliderNext()下一张图片 sliderPrevious() 前一张图片 start() 开始定时任务 stop()清除定时任务。

实现代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>原生轮播图</title>
		<style>
			#mySlider{
				position: relative;
				width: 790px;
				height: 340px;
			}
			.sliderList ul{
				list-style-type: none;
				position: relative;
				height: 100%;
				width: 100%;
				padding: 0;
				margin: 0;
			}
			.slider-item,.slider-item-selected{
				position: absolute;
				transition: opacity 1s;
				opacity: 0;
				text-align: center;
			}
			.slider-item-selected{
				opacity:1 ;
				transition: opacity 1s;
			}
		</style>
	</head>
	<body>
		<div>
			<div id="mySlider" class="sliderList">
				<ul>
				<li class="slider-item slider-item-selected"> <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/></li>
				<li class="slider-item"> <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/></li>
				<li class="slider-item"> <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/></li>
				<li class="slider-item"> <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/></li>
				</ul>
			</div>
		</div>
	</body>
	<script type="text/javascript">
		class Slider{
			constructor(id,cycle=3000){
				this.container=document.getElementById(id);
				this.items=this.container.querySelectorAll('.slider-item','.slider-item-selected');
				//console.log(this.items)
				this.cycle=cycle;
			}
			getSelectItem(){
				return this.container.querySelector('.slider-item-selected');
			}
			getSelectItemIndex(){
				
				return Array.from(this.items).indexOf(this.getSelectItem());
			}
			slideTo(idx){
				const selected=this.getSelectItem();
				if(selected)
				{
					selected.className='slider-item';
				}
				const target=this.items[idx];
				if(target)
				{
					target.className='slider-item-selected';
				}
				
			}
			sliderNext(){
				const currentIdx=this.getSelectItemIndex();
				const nextIdx=(currentIdx+1)%this.items.length;
				this.slideTo(nextIdx);
			}
			sliderPrevious(){
				const currentIdx=this.getSelectItemIndex();
				const previousIdx=(this.items.length+currentIdx-1)%this.items.length;
				this.slideTo(previousIdx);
				
			}
			start(){
				this._timer=setInterval(()=>this.sliderNext(),this.cycle);
			}
			stop(){
				clearInterval(this._timer);
			}
		}
		let a=new Slider('mySlider',2000);
		a.start();
	</script>
</html>

进一步增加指示点和前进后退按钮

思路分析 在我们见过的轮播图中,下方有小白点指示当前轮播位置和前进后退控制轮播是应用比较广泛的样式和功能。通常我们的做法是还是在原来的基础上再次添加样式和控制逻辑。新的样式包括左右跳转和下部小圆点表示当前图片位置,自然也支持切换。完成后需要我们设置对应的样式即可。

image.png 如图所示,我们完成了样式定位于设计,那么接下来需要做的就是将事件绑定上对应DOM上,我们再次重写一下Class类。个人在实现这一块的时候有遇到过自定义监听事件的问题,后面再作补充。

部分时间代码分析:前面我们实现了页面的定时自动切换,页面的变化是和下方原点对应的,由此我们应该针对页面变化slideTo(idx)做出变化,即页面发生变化,自动同步下方样式的变化,引出了自定义事件slide,然后根据页面变化传递过来的idx页面索引值,,我们获取并更改对应索引的类名即可。

			slideTo(idx) {
				const selected = this.getSelectItem();
				if (selected) {
					selected.className = 'slider-item';
				}
				const target = this.items[idx];
				if (target) {
					target.className = 'slider-item-selected';
				}
                                //自定义事件
				const detail={index:idx};
				const event=new CustomEvent('slide',{bubbles:true,detail})
				this.container.dispatchEvent(event);
			}
                        //监听事件,实现样式同步变换 
                        this.container.addEventListener('slide',evt=>{
			const idx=evt.detail.index;
			const selected=controller.querySelector('.slide-button-selected');
			if(selected) selected.className='slide-button';
			buttons[idx].className='slide-button-selected';
			});

底部样式除了监听自定义样式外,还要监听鼠标滑动的样式,实现鼠标移入时停止页面切换,移出时继续页面切换。

		const buttons=controller.querySelectorAll('.slide-button,.slide-button-selected');
		controller.addEventListener('mouseover',evt=>{
		const idx=Array.from(buttons).indexOf(evt.target);
		if(idx>=0){
			this.slideTo(idx);
			this.stop();
                        }});
		controller.addEventListener('mouseout',evt=>{
                        this.start();})

两侧的按钮就简单多了,绑定点击事件即可,触发时调用next、previous函数控制前进后退。

					const previous=this.container.querySelector('.slide-list-previous');
					if(previous)
					{
						previous.addEventListener('click',evt=>{
							this.stop();
							this.sliderPrevious();
							this.start();
							evt.preventDefault();
						})
					}
					const next=this.container.querySelector('.slide-list-next');
					if(next)
					{
						next.addEventListener('click',evt=>{
							this.stop();
							this.sliderNext();
							this.start();
							evt.preventDefault();
						})
					}

小结

这样我们就完成了一个轮播器的所有部分,包括结构设计、样式设计、行为设计,通过学习感觉行为设计中将基本功能作为方法抽象出来,集中于控制流的思想来操控整个组件的运行是更为合理的设计理念。分离开了也维护,扩展,也好新的控制流来了进行组装式重用。

插件化来解耦优化

插件化的哪一部分?为什么要插件化?

插件化针对的是行为设计,插件化最直观的一面就是constructor没有那么长了,其次插件化就类似于我们vscode里的插件一样,最大的优势在于扩展,需要的时候,我就安装,不需要这一块功能了,我直接禁用就OK了,非常的方便。没有插件化时,好比我们现在要删除左右两侧的点击事件切换播放,那么我们能做的只有改源代码了。想想就知道那个更具优势了!

实现思路分析

我们轮播图涉及到的插件有下方圆点,左右点击切换这三个块。而且极为重要的是,这三个功能块是相互独立的,没有相互间的依赖关系。所有我们在constructor中本本分分的全部写在一块,增加了代码阅读的难度。通常我们在一个比较大的功能实现方法中,会分成几个小的函数,然后再组合就可以了。举个例子:Date类型中包含了获取年、月、日、时、分、秒,我们在获取时间的时候可以组合这些方法来达到代码更加简洁和分离的功效。参照这种实现思想,我们将所有插件的实现逻辑单独拎出来作为方法,参数则是Slide组件,即对象,来显示。在类中实现一个方法,该方法作为组件中插件功能实现的方法,接受一个函数对象列表作为参数,遍历执行函数即可完成插件插入。

                        //简化后的构造函数
			constructor(id, cycle = 3000) {
				this.container = document.getElementById(id);
				this.items = this.container.querySelectorAll('.slider-item', '.slider-item-selected');
				this.cycle = cycle;
				// 获取圆点按钮
			}
                        //插件的插入方法
			registerPlugins(...plugins){
				plugins.forEach(plugin=>plugin(this));
			}
		//下方圆点拎出来
		function pluginController(slider){
			const controller=slider.container.querySelector('.slide-list__control');
			if(controller)
			{
				const buttons=controller.querySelectorAll('.slide-button,.slide-button-selected');
				controller.addEventListener('mouseover',evt=>{
					const idx=Array.from(buttons).indexOf(evt.target);
					if(idx>=0){
						slider.slideTo(idx);
						slider.stop();
					}
				});
				controller.addEventListener('mouseout',evt=>{
					slider.start();
				})
				slider.container.addEventListener('slide',evt=>{
					const idx=evt.detail.index;
					const selected=controller.querySelector('.slide-button-selected');
					if(selected) selected.className='slide-button';
					buttons[idx].className='slide-button-selected';
				});
				}	
		}
		//左右按钮分别拎出来
		function pluginPrevious(slider){
			const previous=slider.container.querySelector('.slide-list-previous');
			if(previous)
			{
				previous.addEventListener('click',evt=>{
					slider.stop();
					slider.sliderPrevious();
					slider.start();
					evt.preventDefault();
				})
			}
		}
		function pluginNext(slider){
			const next=slider.container.querySelector('.slide-list-next');
			if(next)
			{
				next.addEventListener('click',evt=>{
					slider.stop();
					slider.sliderNext();
					slider.start();
					evt.preventDefault();
				})
			}
		}

结果演示

与上图结果一样,测试后方法功能完整。通过减少函数参数达到了插件减少的功能。

总结

插件化的方式还是和html的设计部分具有一定的耦合,比如抽象出来的组合实现方法中,仍然需要获取类名等等,其插件功能的定制性比较高,通用性比较少,在产品经理反复无常的增加或者删除操作下,能最大限度的保留代码功能实现,通过简单的参数传递就可以实现插件的增加与删除。