第一个故事:切换到夜间模式
在 WEB 开发中,HTML 负责网页的结构,CSS 负责网页上各个元素的展示样式,JS 则负责网页和用户的交互。想要成为一名优秀的前端工程师,首先要做的就是遵守这三者各司其职的原则,让我们的代码易于维护和扩展。
但是,有时候我们常常一不小心就破坏了这个原则。又或者,我们为了实现业务需求,根本不管这个规则。这都会导致我们的代码结构混乱,维护困难。那么下面,我就通过一个例子,来谈谈遵守各司其职这个原则的好处。
现在我们有一个任务,它的具体需求是这样的:给一个网页实现一个深色系和浅色系主题的切换,以使得在夜晚访问这个网页的读者能够使用“夜间模式”。
这个网页的 HTML 大概是这样的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>深夜食堂</title>
<style>
body,
html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
body {
padding: 10px;
box-sizing: border-box;
}
div.pic img {
width: 100%;
}
#modeBtn {
font-size: 2rem;
float: right;
}
</style>
</head>
<body>
<header>
<button id="modeBtn">太阳</button>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg" />
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返
[6] 。
</p>
</div>
</main>
</body>
</html>
现在的页面,在手机上看起来是这样的效果:
任务的要求是当用户点击网页右上角的太阳太阳图标时,将网页变为深夜模式,即用深色背景、浅色字体来显示网页内容,同时太阳太阳标记变为月亮月亮标记。
这个任务很简单,我们可能非常快的就写下按钮的响应处理:
<script>
const btn = document.getElementById("modeBtn");
console.log(btn);
btn.addEventListener("click", (e) => {
const body = document.body;
if (e.target.innerHTML === "太阳") {
body.style.backgroundColor = "black";
body.style.color = "white";
e.target.innerHTML = "月亮";
} else {
body.style.backgroundColor = "white";
body.style.color = "black";
e.target.innerHTML = "太阳";
}
});
</script>
以上这段代码给按钮注册了click
事件,当用户点击按钮的时候,如果当前按钮文字是“太阳”,说明要从日间模式转换为夜间模式,那么将 body 的背景样式转换成深色,文字样式转换成浅色,否则将 body 的背景颜色转换为浅色,文字样式转换为深色
点击后出现的效果
看起来,我们完美地实现了产品的需求,可以交差了。但是实际上,上面的代码存在以下三个问题:
- 对于其他不了解需求的同事,阅读这段代码能够直接理解这个按钮按下的含义吗?
- 如果产品需求变更,要求用深灰色背景、浅黄色文字来显示夜间模式,JS 代码可以避免修改吗?
- 如果要给切换过程增加动画效果,能方便添加吗?
作为读者的你,知道如何解决这些问题吗?
第二个故事:用 class 属性表示元素的业务状态
第一个故事中,我们直接用 JS 操作元素,让元素在夜间和白天模式互换:
body.style.backgroundColor = "black";
body.style.color = "white";
这样做的缺点是其他的程序员只知道这两个语句是将body
的background
样式改为了黑色,将color
样式改为了白色,却并不知道这个样式代表的是什么业务需求或者状态。
之所以会这样,是因为我们将本该由 CSS 完成的工作交由 JS 来做了,本来应该由 CSS 设置元素的样式,却让 JS 代替了。所以,我们需要重构一下代码,让它能体现出业务的需求。
我们把夜间模式下元素的样式的设置还给 CSS 来完成:
body.night {
background-color: black;
color: white;
}
然后将 JS 代码重构为如下形式:
const btn = document.getElementById("modeBtn");
btn.addEventListener("click", (e) => {
const body = document.body;
if (body.className !== "night") {
body.className = "night";
e.target.innerHTML = "月亮";
} else {
body.className = "";
e.target.innerHTML = "太阳";
}
});
如上代码所示,当body
元素的class
属性不等于night
时,表示(点击前)当前元素的状态是白天模式,所以现在需要将它的状态修改为夜间模式,于是我们只要将它的class
属性设置为night
,页面就会呈现夜间模式的样式。同理,当body
元素的class
属性等于night
时,表示(点击前)body
元素是夜间模式,所以需要将这个元素的状态修改为白天模式,也就是默认状态,即class
属性等于空。
上面的代码,虽然改动十分微小,只是把之前的两行代码:
body.style.backgroundColor = "black";
body.style.color = "white";
替换成一行
body.className = "night";
但是,它能解决前面提出的几个问题:
- 首先,
className
设为night
,这个操作本身透露了需求信息,它描述了这是一个夜间(night)模式的业务状态。这样就便于后来的维护者快速理解业务需求。 - 其次,如果产品需求变更,把模式对应的颜色换了,我们不需要修改 JS 代码,只需要修改
body.night
的样式规则即可! - 第三,如果要增加切换过程的动画效果,可以使用 CSS3 支持的过渡动画,例如:
body {
padding: 10px;
box-sizing: border-box;
transition: all 1s;
}
body.night {
background-color: black;
color: white;
transition: all 1s;
}
给body
和body.night
都添加样式规则transition all 1s
,就可以实现简单的切换动画了。
最后,实际上还有个细节可以改进,那就是e.target.innerHTML = '月亮'
;这样的切换也不是很好,也应该合并到 CSS 中。这个可以通过伪元素来实现:
#modeBtn::after {
content: '太阳';
}
body.night #modeBtn::after {
content: '月亮';
}
我们去掉<button id="modeBtn"></button>
中间的文本内容,然后给它添加伪元素样式,这样我们 JS 代码简化成
const btn = document.getElementById("modeBtn");
btn.addEventListener("click", (e) => {
const body = document.body;
if (body.className !== "night") {
body.className = "night";
} else {
body.className = "";
}
});
这时,JS 代码只负责切换元素的状态,而不需要代替 CSS 改变元素的样式了。
完整的代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>深夜食堂</title>
<style>
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
body {
padding: 10px;
box-sizing: border-box;
transition: all 1s;
}
body.night {
background-color: black;
color: white;
transition: all 1s;
}
div.pic img {
width: 100%;
}
#modeBtn {
font-size: 2rem;
float: right;
}
#modeBtn::after {
content: '太阳';
}
body.night #modeBtn::after {
content: '月亮';
}
</style>
</head>
<body>
<header>
<button id="modeBtn"></button>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6] 。
</p>
</div>
</main>
</body>
<script>
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
</scrpit>
</html>
从这个故事,我们可以看出,元素的 class 属性不仅仅只是为了给 CSS 提供类选择器,还能表示元素的业务状态。这样,我们就可以将这些业务状态对应的展示样式交由 CSS 处理,而 JS 只需要处理状态的切换即可,从而保证了各司其职的原则,使得我们的代码既能体现业务的需求,也便于将来的维护和扩展。
对于这个例子来说,对于状态的切换,可不可以完全可以不使用 JS,纯用 CSS 来实现呢?
第三个故事:最好的 JS 代码是没有 JS 代码
不使用 JS,只使用 CSS 实现“夜间模式”效果,看起来是一个不小的挑战。不过仔细想想,也不是不可能做到。其实这个问题,最核心的部分是要使用 CSS 代替 JS 来切换并记住与用户交互的状态。
让我们回忆了一下,在 HTML 中,能够完成状态切换的元素也是有的,比如表单元素中的选择框(checkbox)元素,那么……
尝试改变一下 HTML 文档结构:
<body>
<input id="modeCheckBox" type="checkbox" />
<div class="content">
<header>
<button id="modeBtn"></button>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg" />
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返。
</p>
</div>
</main>
</div>
</body>
上面的代码中,我们在 body 的子元素中添加一个type="checkbox"
的input
元素。当我们点击这个元素时,就有两种状态:普通状态和选中状态。其中,选中状态可以用伪类选择器#modeCheckBox:checked
来标记。
由于<input>
元素是 body 的第一个子元素,它后面的子元素可以通过 CSS 的兄弟节点选择器来命中。为了便于统一操作,我们给 header 和 main 元素外层增加一个<div class="content">
的容器,这样我们就可以通过 CSS 选择器改变这个容器的样式:
/* 匹配checkbox选中状态下的.content */
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
上面的这条规则表示当checkbox
选中状态下,.content
元素的样式为黑底白字。
然后,微调一下上一版本的样式,将body
中的padding
移到.content
容器中,将body.night
的样式移到#modeCheckBox:checked + .content
规则中。
body {
box-sizing: border-box;
}
.content {
padding: 10px;
transition: all 1s;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
这样我们就可以通过点击 checkbox 元素来进行“夜间模式”状态切换了:
如上图所示,当 checkbox 被选中时,页面进入夜间模式;当 checkbox 不被选中时,页面又进入白天模式。
当然,这个效果和需求还有一定差距 —— 不能让用户点击页面顶部的 checkbox 来替代点击右上角太阳图标,这不符合业务需求。于是,我们打算使用 label 元素代替 checkbox 触发用户的点击行为。
通过 label 元素的 for 属性指定的 id,能够将 label 元素与对应的表单元素绑定,这样当用户点击 label 元素的时候,就相当于直接点击对应的表单元素。
<body>
<input id="modeCheckBox" type="checkbox" />
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg" />
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返
[6] 。
</p>
</div>
</main>
</div>
</body>
如上代码所示,我们将原来的button
元素用label
元素代替,并将label
元素的for
属性指向checkbox的id(modeCheckBox)
。这样就能够让label
元素绑定checkbox
元素。
然后,我们再将checkbox
框隐藏起来
#modeCheckBox {
display: none;
}
这样,我们就完美地实现了只用 CSS,不使用 JS 代码实现的“夜间模式”切换。
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>深夜食堂</title>
<style>
body,
html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
body {
box-sizing: border-box;
}
.content {
padding: 10px;
transition: all 1s;
}
div.pic img {
width: 100%;
}
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
#modeBtn {
font-size: 2rem;
float: right;
}
#modeBtn::after {
content: "太阳";
}
#modeCheckBox:checked + .content #modeBtn::after {
content: "月亮";
}
</style>
</head>
<body>
<input id="modeCheckBox" type="checkbox" />
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg" />
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返。
</p>
</div>
</main>
</div>
</body>
</html>
现在,我们来比较一下这个版本和第二个故事中有 JS 的版本,它们各自的的优缺点在哪里:
-
JS 版本更加简洁,虽然用了 JS,但 HTML 结构更简单。而且 JS 版本的兼容性要好一些,因为 CSS 版本使用了兄弟结点选择器,在早期的浏览器上,可能不能被支持。
-
CSS 版本不用写 JS 代码,这样就不用维护 JS 代码,也不可能有 JS 的 bug,所以也是有优势的,尤其是在移动端页面,不用担心浏览器兼容性的前提下,使用这一版更加放心。
[codesandbox完整示例](https://gmp7t.csb.app/index.html) [示例1](https://gmp7t.csb.app/1.html) [示例2](https://gmp7t.csb.app/2.html) [示例3](https://gmp7t.csb.app/3.html)
虽然这两个版本各有优劣,但是要知道,再简单的代码,也可能会有 bug,唯一不会有 bug 的方式就是不用写代码。所以,最好的 JS 代码就是没有 JS 代码。 这个项目到这里就结束了,记住你学到的这些,就离高级前端工程师更近了一步。
示例: