第一个故事:切换到夜间模式

在 WEB 开发中,HTML 负责网页的结构,CSS 负责网页上各个元素的展示样式,JS 则负责网页和用户的交互。想要成为一名优秀的前端工程师,首先要做的就是遵守这三者各司其职的原则,让我们的代码易于维护和扩展。

6A0179B1-7DAA-46CA-9F45-0547B516AEE6.png

但是,有时候我们常常一不小心就破坏了这个原则。又或者,我们为了实现业务需求,根本不管这个规则。这都会导致我们的代码结构混乱,维护困难。那么下面,我就通过一个例子,来谈谈遵守各司其职这个原则的好处。
现在我们有一个任务,它的具体需求是这样的:给一个网页实现一个深色系和浅色系主题的切换,以使得在夜晚访问这个网页的读者能够使用“夜间模式”。
这个网页的 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>

现在的页面,在手机上看起来是这样的效果:

20201211104525.gif

任务的要求是当用户点击网页右上角的太阳太阳图标时,将网页变为深夜模式,即用深色背景、浅色字体来显示网页内容,同时太阳太阳标记变为月亮月亮标记。
这个任务很简单,我们可能非常快的就写下按钮的响应处理:

<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 的背景颜色转换为浅色,文字样式转换为深色
点击后出现的效果

94B89F81-4DF7-494B-B7D8-DF667587F1CB.png

看起来,我们完美地实现了产品的需求,可以交差了。但是实际上,上面的代码存在以下三个问题:

  1. 对于其他不了解需求的同事,阅读这段代码能够直接理解这个按钮按下的含义吗?
  2. 如果产品需求变更,要求用深灰色背景、浅黄色文字来显示夜间模式,JS 代码可以避免修改吗?
  3. 如果要给切换过程增加动画效果,能方便添加吗?
    作为读者的你,知道如何解决这些问题吗?

第二个故事:用 class 属性表示元素的业务状态

第一个故事中,我们直接用 JS 操作元素,让元素在夜间和白天模式互换:

body.style.backgroundColor = "black";
body.style.color = "white";

这样做的缺点是其他的程序员只知道这两个语句是将bodybackground样式改为了黑色,将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;
}

bodybody.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 元素来进行“夜间模式”状态切换了:

616AD207-CFBC-477F-BB92-8B29D6E1D1DB.jpg

如上图所示,当 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,所以也是有优势的,尤其是在移动端页面,不用担心浏览器兼容性的前提下,使用这一版更加放心。
    虽然这两个版本各有优劣,但是要知道,再简单的代码,也可能会有 bug,唯一不会有 bug 的方式就是不用写代码。所以,最好的 JS 代码就是没有 JS 代码。 这个项目到这里就结束了,记住你学到的这些,就离高级前端工程师更近了一步。
    示例:

    codesandbox完整示例
    示例1
    示例2
    示例3