实战回
实战回
本集我们来进行两个实战练习,希望能使大家对如何进行前端开发有一定了解。 本集我们实现两个功能,第一个实现一个弹出式的组件(下拉式菜单)。第二个实现一个较为复杂的页面导航条。
弹出式组件
设计目标

如图,这是一个非常常见的页面元素,也就是所谓的弹出式菜单。 正常情况下只显示上面一条主菜单,当鼠标移动到某个菜单项时,就会弹出子菜单。这是页面上非常常见的一种呈现方式。
如何实现?
我们先给出初始菜单条的代码:
<div class="container">
<nav class="nav nav1">
<ul>
<li>
<a href="#">庄体育</a>
</li>
<li>
<a href="#">消息</a>
</li>
<li>
<a href="#">动态</a>
</li>
<li>
<a href="#">收藏</a>
</li>
<li>
<a href="#">历史</a>
</li>
</ul>
</nav>
</div>
.container{
width:600px;
margin: auto;
}
.nav ul{
list-style: none;
background: #333;
padding-left:0px;
color: #fff;
}
.nav>ul>li{
display:inline-block;
}
.subnav{
min-width:110px;
}
li:hover .subnav{
visibility:visible;
opacity:100;
}
.subnav a{
border-left: 0px;
border-top: 1px solid #595959;
}
.nav a{
display:block;
padding: 10px 20px;
line-height: 1.2em;
color: #fff;
border-left: 1px solid #595959
}
a:link{
text-decoration:none;
}
如果你对一个功能的实现不知道如何下手的话,除了百度之外,还可以看别人是怎么实现的。 例如我们可以观察B站的弹出菜单时如何实现的:
开启F12之后,选中一个弹出式菜单,比如直播、番剧之类的,注意在右侧的元素里要点开对应的属性。
然后触发元素弹出和回收,你应该能观察到元素界面发生了变化:


你应该能观察到元素界面发生的变化,并通过这个变化窥探到弹出菜单的秘密:
- 收回时,其css变成display:none,关闭了显示。
- 弹出式,其css的display:none消失,因此块就显示出来了。
弹出式组件的实现方式基本都是类似的:
- 将需要弹出的组件放置在页面的合适位置。
- 默认关闭其显示。
- 当触发其显示时,通过修改css,使其显示在页面上。
- 需要收回时,通过修改css,使其从页面上消失。
- 切换消失和显示的方式有很多种,根据你的需要可以选择,最简单的是display:none和display:block的切换。
前端工程师一定要懂得分析别人的实现方式,前端实现几乎是所有代码里最容易逆向的一种。
开始动手
我们写一个ul块来放置子菜单,并将这个块放在第一个菜单项的下方:
<li>
<a href="#">庄体育</a>
</li>
<ul>
<li><a href="#">我的好友</a></li>
<li><a href="#">我的积分</a></li>
<li><a href="#">退出登录</a></li>
</ul>
这里立刻就会出现问题:
你会发现子菜单块将剩余的菜单都挤到下一行了。 道理很简单,因为子菜单是一个块,也必须是一个块,否则无法依次向下排列。而菜单快被我们设置成display:inline-block
以使他们可以显示在同一行上。那么庄体育这个内联块后接了一个块元素,自然会产生换行,这样剩余的块就会被挤到下面的行里去。 这里子菜单块不能再留在原来的文档流之中,否则会影响原有的布局,一般的处理上,我们会将其申明成position:absolute
来将其从文档流中抽出来:
<ul class="subnav">
...
</ul>
.subnav{
position:absolute;
left:0px;
}
于是你获得:
块正确了,也不影响原有的布局,但位置不对。因为position:absolute
是以body作为定位位置的。事实上这种说法并不正确,可以看这里:
绝对定位元素相对的元素是它最近的一个祖先,该祖先满足:position的值必须是:relative、absolute、fixed,若没有这样的祖先则相对于body进行定位。偏移值由其top、bottom、left、right值确定。
我们有两种处理方式:
- 就让菜单块相对body定位。
- 让菜单块随其父元素进行定位。
显然第二种方式是我们提倡的,牢记一点:程序设计中,相对定位优于绝对定位。相对定位可以让我们更方便地调整页面的结构。 所以我们在最上方的ul处加入position:relative
。
<div class="container">
<nav class="nav nav1">
<ul style="position:relative">
<li>
<a href="#">庄体育</a>
</li>
<ul class="subnav">
<li><a href="#">我的好友</a></li>
<li><a href="#">我的积分</a></li>
<li><a href="#">退出登录</a></li>
</ul>
<li>
<a href="#">消息</a>
</li>
<li>
<a href="#">动态</a>
</li>
<li>
<a href="#">收藏</a>
</li>
<li>
<a href="#">历史</a>
</li>
</ul>
</nav>
</div>
.container{
width:600px;
margin: auto;
}
.nav ul{
list-style: none;
background: #333;
padding-left:0px;
color: #fff;
}
.nav>ul>li{
display:inline-block;
}
.subnav{
min-width:110px;
position:absolute;
left:0px;
}
li:hover .subnav{
visibility:visible;
opacity:100;
}
.subnav a{
border-left: 0px;
border-top: 1px solid #595959;
}
.nav a{
display:block;
padding: 10px 20px;
line-height: 1.2em;
color: #fff;
border-left: 1px solid #595959
}
a:link{
text-decoration:none;
}
弹出和回收子菜单
现在我们看如何控制子菜单的弹出和回收。我们首先为子菜单设置display:none
来隐藏这个菜单:
.subnav{
min-width:110px;
position:absolute;
left:0px;
display: none;
}
现在来处理鼠标悬停显示的问题。我们之前已经知道这个可以用伪类:hover
来选中。然而这里的一个问题是,我们只能悬停在菜单项(也就是“庄体育”这个li)上。 然后你会发现这行不通:
<div class="container">
<nav class="nav nav1">
<ul style="position:relative">
<li class="parent">
<a href="#">庄体育</a>
</li>
<!-- ... -->
</ul>
</nav>
</div>
.parent:hover{ /* 鼠标悬停在 .parent 上时生效 */
display : block;
}
这个css选择器的意思是:当鼠标悬停在.parent类的元素上时触发。但我们简单分析一下就知道这是不对的:当鼠标悬停时,我们是想要.subnav显示,不是.parent显示。
而如果你这样写:
.navsub:hover{
display: block;
}
也是不行的,因为这个选择器必须当鼠标悬停在.navsub上时才会选中,而此时对应的.navsub已经处于display:none
状态,也就是隐藏状态了,你无法悬停在这个块上。
似乎我们陷入了死循环,不过这里有个比较不那么大众化的解决方案:相邻兄弟选择器。
所以这样写是可以的:
li:hover+.navsub{
display: block;
}
试试,你会发现非常完美:鼠标移上去之后弹出,鼠标移开之后收回。
不过很可惜,这也不是标准答案:当你试图将鼠标移动到子菜单时,你做不到。
仔细分析下,这非常符合逻辑。
因为上面css的规则的意思是:当鼠标悬停到li
时,li
相邻的.navsub
应该使用以下的规则。但当你试图移动到子菜单,也就是.navsub
时,这条规则就不成立了!。
那么怎么做呢,一个比较不那么好的解决方案是,给.navsub:hover
也加上:
.navsub:hover{
display: block;
}
也就是相当于:在你移出li
时,趁浏览器还没反应过来把.navsub
隐藏起来之前,就移入.navsub
并触发下面的那条css。
也就是说:只要你鼠标足够快,浏览器就追不上你。
不过我们有一种更优雅的实现方式:把.navsub
块移入父元素也就是li
块:
<div class="container">
<nav class="nav nav1">
<ul style="position:relative">
<li>
<a href="#">庄体育</a>
<ul class="subnav">
<li><a href="#">我的好友</a></li>
<li><a href="#">我的积分</a></li>
<li><a href="#">退出登录</a></li>
</ul>
</li>
<li>
<a href="#">消息</a>
</li>
<li>
<a href="#">动态</a>
</li>
<li>
<a href="#">收藏</a>
</li>
<li>
<a href="#">历史</a>
</li>
</ul>
</nav>
</div>
当然此时css的选择器也必须改为子类选择器:
li:hover>.subnav{
display: block;
}
简单分析一下就非常清楚这种方式是合理的:子菜单(.subnav
)也是li
的一部分,因此当你移入子元素时,上面的选择器依然会生效。
css进阶
我们上面的例子里,鼠标一移动上去,菜单就弹出了,一移出,菜单就关闭。我们说他的响应是瞬时的。如果你观察很多其他的网站的话,会发现很多网站的弹出窗口都有动画效果。那么我们要如何实现这个动画呢? 以前比较常用的方法是用jQuery,jQuery提供了fadeIn
和fadeOut
函数来实现淡入淡出等效果。更高阶的还有动画效果。他们的本质都是利用定时器来实现一段动画。 随着css3的流行,大家都逐步转移到使用css3来制作动画了。
当鼠标移动到块上是,块会缓慢地变长,看代码:
div
{
width:100px;
height:100px;
background:blue;
transition:width 2s;
}
div:hover
{
width:300px;
}
这里的div里设置了transition: width 2s;
,他的意思是,当width
变化时,这个变化需要用2s的时间来过渡完成。下面3个-开头的规则都是为了兼容旧版本。 当鼠标移动上去时,触发div:hover
规则生效,width从100px变成300px,但这个过程需要2s的时间来完成,所以你会看到一个渐变的动画。
渐隐和渐入
我们可以利用这一特性来实现动画。注意因为我们要渐入和渐隐,此时不能再用display
属性来控制显示和不显示,我们要利用visibility
属性来控制可见和不可见,再用opacity
来控制其显示的透明度,实现渐渐出现和渐渐消失的视觉效果:
.subnav{
position:absolute;
left:0px;
min-width:110px;
visibility:hidden; /*初始不可见*/
opacity: 0; /*透明度0*/
transition: all 0.25s;/*所有效果都在0.25s的时间内渐变*/
}
li:hover>.subnav{
visibility: visible;
opacity: 100;
}
思考一下
如何实现菜单弹出的效果? 提示一下: 看看height
,max-height
和overflow
这几个css规则。
网页导航条
设计目标
我们给出初始的代码,本节只讨论如何布局,不讨论如何美化,css风格的调整请自行尝试——一个很简单的技巧,你可以对照B站的F12对照着做。
我已经把初始css配置放到style.css
里了,我们对布局的调整将在layout.css
里进行。
最终我们希望能达到这样的效果:

这种模式是比较常见的一种导航条。
如果你对代码不是很熟悉的话,看代码很容易晕掉,这里有两个技巧:
看代码的技巧
- 善用你的编辑器,尤其对这种html语言的文件,要懂得对代码进行折叠,这样可以更容易搞清楚代码的逻辑框架。
- 跑起来,对于我们的页面,浏览器可以帮我们更好地将元素和css对照起来。
实际上这两点对于所有类型的代码都是适用的,尤其对于一些逻辑比较复杂的代码,打断点并跑起来是理解代码的捷径。 所以请记住:程序设计(软件开发)是实践性学科,一切以实际运行为准。
原始代码解析
在浏览器和你的vscode里打开页面。我们先来分析html的架构。
在我们这个代码里,所有内容都包括在body
下的div.header-bar
块里。div.header-bar
也是我们的主块。
主块里包含三个块,分别是:ul.left-entry
、div.center-search-container
和ul.right-entry
。 这样左侧的菜单组件就是li.left-entry-item
,右侧的菜单组件我们用li.right-entry-item
来控制。
标记简写法
到现在你应该适应标记的简写法了。例如ul.left-entry
,表示<ul class="left-entry">
。 又或者form#nav-searchform
,就是<form id="nav-searchform">
。
我们先不看中间的搜索栏,单看左右菜单条的话,就会发现实际上就是简单的ul>li
(那么你知道,为什么li
的前面没有像普通li
前面一样的圆点吗?)。
这些菜单项的内容除了文字内容之外,还有一些是svg
,这是一种可缩放矢量图形,对应你页面上看到的图标。
step 1:总体布局和左侧菜单
显然我们希望这三个块横向并列排布。那么我们就需要将这三个块的父块(div.header-bar
)设置成display:flex
。 同时我们希望这三个块尽量分布在整个页面宽度上,所以我们用space-between
来设置分布:
.header-bar{
display: flex;
justify-content: space-between;
}

菜单项也应该要水平分布,因此左右菜单项也要配置flex
:
.left-entry{
display: flex;
}
.right-entry{
display: flex;
}
我们得到类似的结果:

挤在一起了,而且几个项的竖直方向上并没有对齐,我们设置一下边距,并设置flex
容器交叉轴上居中对齐:
.left-entry{
display: flex;
align-items: center;
}
.left-entry-item{
margin-left: 20px;
}
你应该会看到一个相当像样的结果了:

现在开始第一步的技术总结
display:flex
:启动弹性布局。justify-content: space-between
:将元素在主轴(横轴)上尽量分散分布占满宽度。align-items: center
:在交叉轴(纵轴)上居中对齐元素。- 适当使用
margin
来调整元素之间的间隔。
你可能会说:你没说过space-between
属性啊,你没说过align-items
啊。
且住,大学的课程本来就不应该做成说明书的罗列。我们在布局那章已经讲过flex布局的很多知识,并给出了对应的属性。 如果你按大学的学习方式学习过:也就是说,自己动手试过,那么你至少应该有一些印象。
另一种能让你记住的方式:照着教程做几遍——也就是本节正在做的事。
你看,无论何种方式,都要求你动手做起来。
step 2:右侧菜单
和左侧一样,我们也为右侧菜单设置主轴对齐。不过这里有一个问题,就是右侧菜单的图标是要位于文字之上的。 不过根据默认的状态,他们是在同一行的:

我们观察下右侧菜单项的布局,都是a
下面套一个svg
和一个文字span
:
<li class="right-entry-item">
<a href="#" target="_blank">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path>....</path>
<path>....</path>
</svg>
<span class="right-entry-text">消息</span>
</a>
</li>
我们要将svg
放置到span
上,当然我们可以将这两个块都申明为display:block
,之后再做处理。
另一种比较好的方式还是继续使用flex布局,在a
上设置,svg
和span
就是这个容器的两个元素。
注意flex布局可以设置纵轴为主轴,再配置align-items:center
就可以居中对齐了:
.right-entry-item > a{
display: flex;
flex-direction: column; /* 主轴设置为纵轴 */
align-items: center; /* 交叉轴居中 */
margin-left: 20px;
}
我们同时配置margin
来略作排版。
最后我们对投稿按钮进行风格化:
.header-upload-entry{
display: flex;
flex-direction: row;
align-items: center;
padding: 10px 20px;
}
.header-upload{
margin-left: 20px;
}
并对整个右侧的ul.right-entry
配置一个右边距来调整他的位置:
.right-entry{
display: flex;
align-items: center;
margin-right: 50px
}
step 3:中间搜索栏
这里不再仔细分析搜索栏的具体处理过程,直接给出最后的结果:
#nav-searchform{
display: flex;
align-items: center;
}
.center-search-content{
min-width: 200px;
max-width: 400px;
flex-grow: 1;
}
.center-search-container{
margin: 0px 50px;
flex-grow: 1;
display: flex;
justify-content: center;
}
.nav-search-content{
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-search-input{
flex: 1;
}
注意中间.center-search-content
和.center-search-container
的两段规则,是为了处理搜索栏的逻辑。自己实际动手时可以先不管这段。
最后要注意,搜索条和左右的菜单栏可能没有对齐,这我们已经处理很多次了。其次,我们的结果还少了菜单下面的一条阴影。这些部分就留给大家自己做了。
这里可以得到最终的代码。