用纯HTML,JS,CSS实现横向滚动标签页

·

前言 #

前不久,在我的一个项目中,需要展示一个横向滚动的标签页,它支持鼠标横向拖动和点击切换。在实现的过程中,我发现这个小功能需要同时用到前端的三辆马车,但是实现难度不高,而且最终效果还不错,是个难得的初学者项目,于是萌生了写这篇文章的想法,希望对初学者有所帮助。同时为了避免初学者学习框架,我打算用纯原生的方式实现它。

我们最终的效果应该类似于下面:

last_version

需求分析 #

需求分析就是细化我们需要完成的功能,某个功能的完成需要哪些技术的参与。对于初学者,需求分析至关重要,它可以帮助我们理清思路,找到解决问题的突破口,所以应该引起足够的重视。以本篇目标为例,标签页的需求分析就可以像下面这样:

  1. 我们的展示主体是标签页,HTML就是实现主体的主要技术;
  2. 标签页需要可以拖动和点击,这涉及到鼠标事件的监听和处理,是JS的主场;
  3. 既然标签页可以拖动了,那是否要隐藏那个丑陋的滚动条,加个活动指示器,给鼠标变一个样式?很明显,这些都是CSS的优势。

如上,通过对展示,操作,样式的划分,我们进一步明确了HTML,JS,CSS需要完成的工作,甚至连实现都明朗了,所以对需求拆分得越详细,对实现就越有掌控力。

基本框架 #

对于前端来说,HTML始终是万物之源,所以一言不合先构筑个标准的HTML页面总是没错的。为了便于演示,我将所有的内容都放在一个HTML文件中,文件结构如下

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Tab演示</title>

	<!-- 这里是样式区,后续css代码会添加到这里 -->
	<style type="text/css">
		
	</style>
</head>
<body>
	<!-- 这里是页面区,后续HTML代码会添加到这里 -->
</body>
<!-- 这里是脚本区,后续JS代码会添加到这里,放在这里是因为方便写代码 -->
<script type="text/javascript">
	
</script>
</html>

这里和以往不同,我将script放到了最后,这是因为我想在写脚本的时候,页面标签直接可用,减少对页面加载的监听,降低复杂性。

实现基本功能 #

有了基本结构,下一步当然是画页面啦。从效果图中不难看出,页面主要包括一个一个的选项卡,对于HTML来说,这不就是列表嘛。于是,突破口就出现了,我们先往HTML里面加入列表

<ul>
		<li>肖申克的救赎</li>
		<li>霸王别姬</li>
		<li>阿甘正传</li>
		<li>泰坦尼克号</li>
		<li>这个杀手不太冷</li>
		<li>美丽人生</li>
		<li>千与千寻</li>
		<li>辛德勒的名单</li>
		<li>盗梦空间</li>
		<li>忠犬八公的故事</li>
</ul>

于是,我们有了原始的标签页。但是标签页是竖向的,并且有着丑陋的小黑点,不符合需求。

pure_html
发现了这些问题,下一步当然解决这些问题了,这当然就是CSS的强项啦。首要问题就是让列表横过来。横过来就是改变了元素的相对位置,也就是对应CSS的布局功能。那说起布局,CSS的布局方式有很多,像float,position等等。标签页是横向多个紧密排列的,一个挨着一个,这当然是用flex啦。至于讨厌的小黑点,这是新东西,需要百度一下。查阅文档发现,ul有个属性list-style-type,只需把它设置为none就可以去除小黑点。 此时,页面上的所有选项卡都紧密排列了。为了让它更像一个选项卡,需要给它居中,限制一下宽度,加个背景色,加点padding。下面就是改完样式的代码

ul{
	display: flex;
	justify-content: center;
	align-content: center;
	list-style-type: none;
	background-color: #2397f3;
	width: 600px;
	overflow-x: scroll;
}

li{
	padding: 16px;
	flex-shrink: 0;
}

值得注意的地方有两点。在ul的样式中,由于给ul加了宽度限制,导致它的内容超出了内容区,所以要给ul加上overflow-x的属性。同样由于宽度的原因,flex子项在宽度不够的情况下会默认缩小,表现在标签上就是文字换行啦,flex-shrink: 0;就是让子项保留原有大小。此时,再来刷新页面,可以看到选项卡的基本雏形已经出来了。虽然简陋,但是可以拖动滚动条左右滚动了。下一步,我们的目标就是去除这个丑陋的滚动条。网上搜索一番,发现火狐,IE和Chrome的方式不尽相同,为了兼容性,我们就都给写上。

ul{
	scrollbar-width: none; /* Firefox */
	-ms-overflow-style: none; /* IE 10+ */
}

ul::-webkit-scrollbar {
  display: none; /* Chrome Safari */
}

滚动条去除后,UI好看了,但是新问题出现了——选项卡滚动不了了。别着急,下一步就是添加鼠标拖动功能。

实现交互 #

在浏览器中,HTML标签有对系统事件的监听能力,响应这些事件,可以使页面实时响应用户的操作。通过对不同的事件的组合,可以实现各种丰富、有趣的功能,标签页也一样。

标签页的首要功能是可以随着鼠标的拖动而滚动元素,那么,首要任务就是监听鼠标的移动事件啦。但是光监听移动还不行,因为通常来说,用户在鼠标左键按下后才希望真正拖动,鼠标左键抬起后结束拖动。所以,这个拖动动作其实需要组合鼠标按下(mousedown),移动(mousemove),抬起(mouseup)三个事件。那么这三个方法加在哪,怎么加呢?

在Web API中,JS操作HTML的入口点是Document对象,Document提供了操作(增删改查)HTML元素的API。这一过程是有标准流程的。

  1. 通过Document查找目标元素;
  2. 对目标元素进行元素,样式变更等操作;
  3. 变更完成;

这一过程是重复且繁杂的,为了减少编写这样的样板代码,加快开发速度,一大堆前端框架应运而生。所以,在学习前端框架时,牢记这一基本步骤,有助于快速理解框架的运行原理。毕竟无论框架怎么变,最终都是要落实到这一过程上。

算法明确后,接下来就是具体实现。

查找目标元素 #

在查找目标前,需要首先明确目标是谁。用户肯定不希望在页面的其他地方拖动鼠标,标签页跟着滚动了,这很奇怪。所以我们的目标元素应该是无序列表。那么,怎样通过Document知道无序列表呢,查阅Document的API,发现它有个querySelector的方法,这个方法会从上到下查找满足条件的选择器,并返回第一个满足条件的元素,参数则是选择器的名称。上面已经明确过我们的目标是无序列表,所以查找目标元素的最终代码如下

const ul=document.querySelector('ul');

让列表滚起来 #

每一个HTML元素,在JS中都是Element的对象。上一步我们已经得到了一个Element对象ul,注意,这里的ul对象和ul标签不尽相同。一个是JS的对象对HTML标签的表示,一个是HTML标签。现在有了一个对象,那么就可以通过调用合适的方法来操作这个对象了。通过查阅Element对象的API,发现它有个addEventListener()的方法,这个方法可以完成该对象表示的HTML标签对某些事件的监听。这个方法接收两个参数,第一个参数是事件名称,这在上一节已经说过。第二个参数则是对这个事件的处理,这也是我们实现魔法的地方。

首先,在用户按下鼠标左键后,开始记录鼠标移动情况。在鼠标左键抬起后,停止记录。所以按下和抬起的主要功能就是维护记录开关,控制标签滚动的动作得在鼠标移动的回调里处理。

但在真正写逻辑前,还有两个问题没有处理。 1、怎样让标签滚动? 2、滚动的逻辑怎样写? 问题一当然需要查阅Element的API啦。搜索滚动相关的,发现两个相关性比较大的方法——scrollBy()scrollTo(),都可以滚动内容。唯一的区别是前者的参数是滚动的偏移,后者是最终值。由于鼠标移动是一点一点的,所以选择前者会更方便一点。确定了方法,也就解答了问题一。对于问题二,简单来说就是怎样提供问题一所需的参数。scrollBy()需要两个参数,横向和纵向的滚动偏移值,由于我们只希望标签页可以横向滚动,所以纵向的偏移始终是0,那么横向的呢?通常事件回调都会传递一个事件对象,称作MouseEvent,我们去查查事件对象的API,发现里面带有好几个关于坐标的属性——clientXmovementXscreenXmovementX直接就满足我们的需求,它代表上一次鼠标移动到这一次移动间的偏移,而刚好scrollBy()需要的参数就是偏移,妥了。 综上,得出以下代码

const ul=document.querySelector('ul');
let isMouseDown=false;
ul.addEventListener('mousedown',(e)=>{
	isMouseDown=true;
})
ul.addEventListener('mousemove',(e)=>{
	if(isMouseDown){
		ul.scrollBy(-e.movementX,0);
	}
})
ul.addEventListener('mouseup',(e)=>{
	isMouseDown=false;
})

可以看到,在mousemove的处理上,偏移加了个负号。因为在HTML页面中左上角为坐标原点,右边为X轴正方向。一直往右,则X坐标是增大的,而movementX的值是当前鼠标坐标与上一次坐标点的差值,上一次肯定比这一次小,两者的差值肯定是正值。基于同样的原因,scrollBy()参数正值代表增大X值,也就是显示右边的内容,隐藏左边的内容。两者结合的效果就是,鼠标往右拖,标签页右边隐藏的内容展示了出来,这和直觉相悖。通常我们希望鼠标往右拖,页面展示左边的内容,隐藏右边的。基于这样的分析,我们需要给movementX的值取反。

显示当前选中的标签页 #

现在,标签页可以滚动了,但是还不能选中。我希望点击某个标签时,标签下方出现一个小横条表示选中状态。很明显,显示小横条是一个CSS的问题,而点击标签切换小横条是JS的问题,这一次我们需要同时处理JS和CSS的问题。

首先来显示小横条。显示小横条有两个思路,一种是在HTML中搞个div标签,另一种是使用::after伪元素。我选择后一种,这样可以保持HTML的干净。 接下来需要确定小横条的样式

  • 覆盖在选中的标签上
  • 位置是标签底部
  • 和标签一样长

我们知道正常的HTML文档流是从左到右,从上到下的,新加的元素会追加到已有元素的右边或者下边。小横条需要覆盖在标签上,那么就要改变这一默认行为,position属性就是实现这个功能的关键。absolutefixed都可以脱离正常文档流,使元素覆盖在祖先元素上,不同的是前者是相对于最近的定位祖先,后者是相当于视口的。小横条是跟随着标签显示的,显然要使用前者。确定了位置,还有大小和样式。既然使用了绝对定位,那么bottom,left',right相应就能限定它的位置和大小了,小横条的样式就直接用border-bottom吧。于是,小横条的样式就出来了

.current::after{
			content: "";
			position: absolute;
			border-bottom: 4px solid #FFC109;
			border-radius: 2px;
			bottom: 0;
			left: 0;
			right: 0;
}

结束了吗,还没有!使用了绝对定位,必须时刻记得给绝对定位的元素找个锚点,也就是参照,不然top,leftrightbottom去参考谁呢?那么怎样告诉绝对定位的参照物呢,还是position属性。只不过这一次它要出现在参照物的CSS里面。而由前面的样式分析,小横条始终跟着标签页走,也就是说小横条的参照物就是标签页。所以,还要在标签页的样式上加上position的属性。当然,为了区分更明显,我还改变了一下颜色。

.current{
			color: white;
			position: relative;
}

至此,小横条可以正常显示出来了。

小横条跟随鼠标点击显示 #

有了前面拖动功能的经验支持,这一次轻车熟路了,鼠标点击某个标签页,小横条显示在对应的标签页下方。这一次事件的对象变成了单个标签页,所以点击事件要加在单个标签页上。但是这一次标签页太多了,我们不能还是按照之前的查找-设置方法,这样太繁杂了。巧合的是,前面我们已经得到了ul对象了,通过它的children属性,可以得到所有的li,这不就妥了吗。 小横条要切换到不同的标签页上显示,也就是小横条这个样式要根据点击对象的不同而动态增加或者删除。查阅Element的API,发现有个className的属性,改变它的值就可以增减样式了。

let last=null;
for(let l of ul.children){
		l.addEventListener('click',(e)=>{
			if(last){
				last.className='';
			}
			e.target.className='current';
			last=e.target;
	})
}

代码的实现中,多了个last对象。因为通常标签页只能同时选中一个,当新的标签页被选中之后,上一个选中的标签页应该恢复原始样式,这就是last对象的作用。我们先取消选中上一个元素,然后再选中当前点击的对象,这样就完成了小横条跟随点击选中的效果了。

总结 #

总的来说,这个项目的难点不在于实现有多难,而是新。很多初学者,面对这种新问题往往束手无策,找不到切入点。本篇尝试以例子的形式,以初学者的思维方式分析需求,拆解问题,提炼方法,最终解决问题。从最朴素的直觉出发,引导思考,找到一条易于接受和理解的方法。

所以,遇到新问题不要慌,对问题拆解后,看能不能找到突破口,如果找不到,再从涉及到的几个主要对象中寻找灵感,通常都会有所收获。最后就是多逛逛MDN,关键时刻真能派上大用场。

最后,情人节快乐,祝有情人终成眷属!

参考