
在 HTML、CSS、JavaScript 这三样前端技术里,HTML 一直是变动最小的一个。若你只管「写内容」,1990 年代的文档和 2018 年的文档乍看会很像:
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <h1>Hello there!</h1> </body></html>元素有标签与内容,标签上还能挂属性——除了第一行简化的 文档类型声明(doctype),表面结构几乎没变。但这些年 Web 开发早已从「做静态站点、以内容为主」转向「做动态应用、以交互为主」——而这并不是 Web 诞生之初的设计重心。要在当下做出语义正确、可访问的界面,又要用属性和工具链抠性能,还要把代码组织得可复用、可维护——一整套新问题都压在 HTML 上。
本文想补一段历史脉络:HTML 是如何一路演进到 2018 年前后的。我们会像恐龙时代那样,从结构清晰、可访问的 HTML 基础讲起;再谈性能、响应式与可维护性等手法。CSS 与 JavaScript 免不了要出场,但本文只从它们如何反过来塑造你写 HTML 的方式来谈。弄清这段历史,你会更敢用、也更会用那些常被忽略的新旧特性。下面开始。
使用语义元素编写内容
在前面那个极简示例上再加一点结构:做一个包含导航(链接 + 搜索框)、一块放站点概览的「英雄区」(也常叫 jumbotron / 巨幕)、三栏文章区,以及放版权信息的页脚。
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <div class="navbar"> <ul> <li> <a href="#">Home</a> </li> <li> <a href="#">Info</a> </li> <li> <a href="#">About</a> </li> </ul> <form> <input type="text" placeholder="Search"> <button type="submit">Search</button> </form> </div> <div class="main"> <div class="hero"> <h1>Hello there!</h1> <p>General info about the page goes here</p> <p><a href="#">Learn more</a></p> </div> <div class="grid"> <div class="column"> <h2>First Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </div> <div class="column"> <h2>Second Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </div> <div class="column"> <h2>Third Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </div> </div> </div> <div class="footer"> <p>Copyright info goes here</p> </div> </body></html>这里仍用 <div>、<h1>、<h2>、<p> 等通用标签把内容框起来。语法合法,但语义不足:标签没有清楚说明各块在文档里的角色。
HTML5 引入了一批区块级语义元素,用来补这份含义。下面是把同一页面改得更语义化的一版:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>My test page</title> <!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script> <![endif]--> </head> <body> <nav role="navigation"> <ul> <li> <a href="#">Home</a> </li> <li> <a href="#">Info</a> </li> <li> <a href="#">About</a> </li> </ul> <form> <input type="text" placeholder="Search"> <button type="submit">Search</button> </form> </nav> <main role="main"> <section class="hero"> <h1>Hello there!</h1> <p>General info about the page goes here</p> <p><a href="#">Learn more</a></p> </section> <section class="grid"> <article class="column"> <h2>First Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </article> <article class="column"> <h2>Second Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </article> <article class="column"> <h2>Third Heading</h2> <p>Article content goes here</p> <p><a href="#">View details</a></p> </article> </section> </main> <footer role="contentinfo"> <p>Copyright info goes here</p> </footer> </body></html>来看看这些改动:
<html lang="en">指定文档语言,帮助搜索引擎和浏览器识别内容。- 额外的
<meta>标签提供站点的元数据(不可见),用于搜索引擎等;还包含响应式布局信息(如视口设置)。 <nav>、<main>、<section>、<article>、<footer>等标签让 HTML 结构更可访问(相比通用的<div>)。这些是 HTML5 引入的。<!-- [if lt IE 9]>...-->是条件注释,只为旧版 IE(不支持 HTML5 标签)加载 polyfill 脚本。注意:现在很多站点已不再包含它,因为支持那些旧浏览器的站点也越来越少了。role属性提供辅助功能信息。一般来说,用<nav>标签本身就足够可访问了;加role="navigation"是为了防止<nav>标签不被识别时的兜底。
写语义化 HTML 看似不重要——尤其当它不影响视觉效果时。但你的网站不只是给人看的:浏览器、搜索引擎、屏幕阅读器等都依赖语义化 HTML 才能正常工作。
看看导航栏里的搜索框:
<form> <input type="text" placeholder="Search"> <button type="submit">Search</button></form>这个输入框用 placeholder 而非 <label> 来告诉用户用途。这对人管用,但对屏幕阅读器等技术可能失效。要让它可访问,应加上 aria-label 属性:
<form> <input type="text" placeholder="Search" aria-label="Search"> <button type="submit">Search</button></form>WAI-ARIA(Web Accessibility Initiative — Accessible Rich Internet Applications,常简称 ARIA)是一套在语义标签不够用时让 HTML 更可访问的属性。前面提到的 role 就是一个 ARIA 属性。这些属性看似微不足道,但随着我们把 HTML 用于更复杂的场景(不止是静态文档),它们的重要性会凸显出来。
来看一个更复杂的例子——假设要在页面上加一个标签页(tab)组件,分别显示 Windows、Mac、Linux 的安装说明。HTML 没有原生的标签页组件,只能用无序列表、链接、div 等拼凑一个。此时就能用 role、aria-controls、aria-selected、aria-labelledby 等属性来标记:
<ul role="tablist"> <li> <a id="windows-tab" href="#windows" role="tab" aria-controls="windows" aria-selected="true">Windows</a> </li> <li> <a id="mac-tab" href="#mac" role="tab" aria-controls="mac" aria-selected="false">Mac</a> </li> <li> <a id="linux-tab" href="#linux" role="tab" aria-controls="linux" aria-selected="false">Linux</a> </li></ul>
<div> <div id="windows" role="tabpanel" aria-labelledby="windows-tab"> <img src="http://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png"> <p>Download the program from this link (then double click it)</p> </div> <div id="mac" role="tabpanel" aria-labelledby="mac-tab"> <img src="http://1000logos.net/wp-content/uploads/2017/04/Mac-Logo.png"> <p>Download the program from this link (then double click it)</p> </div> <div id="linux" role="tabpanel" aria-labelledby="linux-tab"> <img src="http://1000logos.net/wp-content/uploads/2017/06/Linux-logo.png"> <p>Download the program from this link (then triple click it)</p> </div></div>这些属性让浏览器和屏幕阅读器能理解你的自定义组件。ARIA 指南涵盖了常见模式(标签页、模态框、手风琴菜单等)。下面只强调一点:ARIA 不改变外观或行为——它只是给那些「长得像标签页」的 div 赋予含义。要真的让标签页能用,你还得写 JavaScript 来处理点击事件、切换可见面板等。ARIA 只是让这件事对辅助技术可见。
随着我们构建的应用越来越复杂,ARIA 属性变得不可或缺。浏览器和辅助技术能理解这些属性,并让用户访问那些原本只靠 HTML 无法实现的功能。

若无这些 ARIA 属性,标签页控件与内容之间就没有可识别的关联。这些属性能帮助屏幕阅读器识别内容、支持键盘快捷键等。ARIA 属性的使用本身可以是一门学问——想深入了解,可查阅 官方指南。
为提高网站可访问性要做这么多事,看似麻烦。但可访问性是 Web 不可或缺的一部分:Web 被设计成一个向所有人(而非少数人)自由分享信息的平台。让网站可访问能改善每位访客的体验——例如,键盘快捷键能帮助:永远无法使用鼠标的人、暂时无法使用鼠标的人、以及不想用鼠标的人(比如大多数程序员)。在开发其他功能时,可访问性很容易被忽略,但它不应被忽视。
若你想改善网站的可访问性,可从 A11Y 项目的检查清单 开始。不过,让网站可访问不是打勾完成任务——它像用户体验的其他方面一样,总有改进空间。最好的方式是站在不同用户的角度实际使用你的网站:用屏幕阅读器测试、只用键盘不用鼠标、用色盲滤镜查看等。
看看目前的网站,如你所料,很素:

要美化它,可以加一个 CSS 文件来应用样式。但如果你不太擅长 CSS,可能要花好几天才能让网站好看。不用自己写 CSS,你可以直接用CSS 框架——本质上是别人写好的、可复用的 CSS。
Bootstrap 是一个流行的 CSS 框架,2011 年发布,很快被数以百万计的网站采用。看看用 Bootstrap 改写后的代码:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>My test page</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script> <![endif]--> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Info</a> </li> <li class="nav-item"> <a class="nav-link" href="#">About</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </nav> <main role="main"> <!-- ... --> </main> <footer class="navbar navbar-dark bg-secondary" role="contentinfo"> <p>Copyright info goes here</p> </footer> </body></html>完整代码示例 在此。
来看看改动:
<head>里的<link>引入了 Bootstrap CSS。注意我们链接的是一个在线文件,这可能带来安全风险——integrity和crossorigin属性帮助确保链接的文件正确无误(子资源完整性)。- 新增的
class都是 Bootstrap 专用的——Bootstrap CSS 里有针对这些类名和 HTML 结构的样式。 - 多了一个
<div class="row">包裹三个<article>,以利用 Bootstrap 的网格布局系统(它依赖这种 HTML 结构)。
现在网站长这样:

不错!注意:要用 Bootstrap 这样的 CSS 框架,你完全不需要自己写 CSS——只要给 HTML 加上合适的类名,就能直接用框架自带的样式。
有一点要注意:虽然标签页的样式已经正确(一次只显示一个),但它们还不能用——点标签页没反应。这是因为这类自定义交互不是 CSS 处理的,而是 JavaScript。要让 Bootstrap 的标签页工作,只需在 <head> 里加上 Bootstrap 附带的 JavaScript 文件:
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>最后一个是 Bootstrap 的 JavaScript。前两个是它的依赖(jQuery 和 Popper),必须在 Bootstrap 脚本之前加载。看看 这个在线示例,标签页现在能用了!
Bootstrap 之所以流行,是因为它解决了当时 CSS 的主要痛点:浏览器不一致、缺乏好的网格系统等。但用 CSS 框架也有缺点——尤其是难以定制,相比从零写 CSS,用框架容易让你的网站看起来千篇一律。
另外,随着智能手机和移动流量的兴起,减小 CSS 和 JS 文件大小变得愈发重要——超过几 KB 的文件都会显著影响慢速网络连接的性能。在上面的例子里,我们要求用户下载整个 Bootstrap 框架,哪怕只用到了少数几个样式和功能。下一节将介绍几种性能优化手法。
注:要做出成熟的 HTML 网站,对 CSS 和 JavaScript 的掌握密不可分;但本文不深入这两门语言。想学习 CSS 和 JavaScript 基础,MDN Web Docs 永远是好起点。若想弄清 CSS 新特性(flexbox、grid、SASS 等)与工具链如何配合,可看我写的 《Modern CSS Explained For Dinosaurs》。

性能属性
至此,我们有了一个组织合理、语义化的 HTML 网站。如果只考虑这些,网站就算完成了!但它仍有许多可改进之处——性能(网站加载多快)和可维护性(代码多容易改)。
对当前网站来说,一大优化点是 <head> 里加载的 JavaScript 文件。这些文件足够大,会拖慢网站。要渲染页面,浏览器会读取 HTML 并转换为它能理解的格式——DOM(Document Object Model,文档对象模型)。浏览器从 HTML 文档顶部开始向下处理。这意味着看到 <script> 标签时,它会先下载并执行脚本,再继续下一行。流程如下图所示:
(来源:hacks.mozilla.org)
一个常见的优化「技巧」是:把所有 JavaScript <script> 标签从 <head> 移到 <body> 末尾。Bootstrap 的官方模板就这么做:
<!doctype html><html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <title>Hello, world!</title> </head> <body> <h1>Hello, world!</h1> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> </body></html>这算是个「技巧」,因为 HTML 本来不是这么设计的——CSS 和 JavaScript 本该放在 <head> 里。但全放 <head> 会导致意外的副作用:拖慢页面渲染。把 <script> 移到 <body> 底部是改善性能的一种方式。
2018 年,很多站点仍用这招。但有个更规范的做法,已支持近 10 年——defer 属性。给 <script> 加上 defer,浏览器会下载外部文件但不阻塞 DOM 构建,等 DOM 建完再执行脚本。流程如下:
(来源:hacks.mozilla.org)
多数情况下,把 <script> 留在 <head> 并加上 defer 能让页面加载更快——文件可以与 DOM 构建并行下载。下面是用 defer 改造后的 Bootstrap 模板:
<!doctype html><html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script defer src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> <title>Hello, world!</title> </head> <body> <h1>Hello, world!</h1> </body></html>好处是网站渲染更快,且保持 HTML 结构整洁(脚本仍在 <head>)。若想更细粒度控制文件的下载与执行顺序,还有 async 属性,以及 <link> 标签的 rel="preload" 属性(详见此处)。
另一大性能优化点是图片。目前图片是「热链接」(hotlinked)——直接链到别人的网站。这不仅维护麻烦(对方改图我们的站就挂了),性能也有问题。
更好的做法是下载图片并本地引用。进一步,可通过调整分辨率来优化文件大小。与其这样直接链外部图:
<img class="img-fluid" src="http://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png" alt="microsoft logo">不如在本地做多张尺寸版本,用 srcset 响应式引用:
<img class="img-fluid" src="microsoft-logo-small.png" srcset="microsoft-logo-medium.png 1000w, microsoft-logo-large.png 2000w" alt="microsoft logo">这里用了小、中、大三个版本的 logo。srcset 告诉浏览器根据视口宽度加载合适的版本。srcset 大约 2013 年引入,但花了好几年才得到浏览器全面支持。截至 2018 年,它的浏览器支持度已经相当不错,绝对值得纳入工作流。
优化图片大小往往是多数网站最大的性能收益——图片的下载量通常比 JS 和 CSS 大几个数量级。用 <picture> 元素能获得更细的控制,但对多数场景来说,简单的 srcset 已经够用。
HTML 作为语言,提供了许多属性,且不断新增(如 importance、lazyload),可用来改善性能。虽然令人望而生畏,但优先关注下载量最大的资源(通常是图片和脚本)往往是最有效的切入点。
注意,与可访问性一样,不存在放之四海皆准的性能规则——你应该用基准测试工具确定什么最适合你的站点(Chrome 和 Firefox 等浏览器都提供此类工具)。最佳方式仍是在慢速网络条件下实际使用你的网站(浏览器开发工具可模拟)——哪怕只用一周,你很可能会发现一大堆性能问题需要修复。
工具与性能
至此我们用了 HTML 自带的属性来优化性能。还能借助外部工具获得进一步的收益。下面看几种常用手法。
一大性能优化是压缩(minification,有时称 uglification)JavaScript 和 CSS 代码。这要用程序分析并移除不必要或冗余的数据:从简单的移除多余空格,到复杂的将长变量名改为单字符。下面是 2003 年 Douglas Crockford 发布的第一个 JavaScript 压缩器示例。未压缩代码如下:
// (c) 2001 Douglas Crockford// 2001 June 3// is// The -is- object is used to identify the browser. Every browser edition// identifies itself, but there is no standard way of doing it, and some of// the identification is deceptive. This is because the authors of web// browsers are liars. For example, Microsoft's IE browsers claim to be// Mozilla 4. Netscape 6 claims to be version 5.var is = { ie: navigator.appName == "Microsoft Internet Explorer", java: navigator.javaEnabled(), ns: navigator.appName == "Netscape", ua: navigator.userAgent.toLowerCase(), version: parseFloat(navigator.appVersion.substr(21)) || parseFloat(navigator.appVersion), win: navigator.platform == "Win32"}is.mac = is.ua.indexOf("mac") >= 0;if (is.ua.indexOf("opera") >= 0) { is.ie = is.ns = false; is.opera = true;}if (is.ua.indexOf("gecko") >= 0) { is.ie = is.ns = false; is.gecko = true;}压缩后变成这样:
var is={ie:navigator.appName=="Microsoft Internet Explorer",java:navigator.javaEnabled(),ns:navigator.appName=="Netscape",ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=="Win32"}is.mac=is.ua.indexOf("mac")>=0;if(is.ua.indexOf("opera")>=0){is.ie=is.ns=false;is.opera=true;}if(is.ua.indexOf("gecko")>=0){is.ie=is.ns=false;is.gecko=true;}效果显著——前面例子里,未压缩的 bootstrap.js 是 124 KB,压缩后的 bootstrap.min.js 仅 51 KB,不到一半!这对网站下载和显示速度影响巨大,尤其对慢速网络连接。
前面例子用的 Bootstrap CSS 和 JS 已经是压缩版。若想压缩自己的代码,可用在线工具如 JavaScript Minifier 或 Minify,选择很多。也可用命令行工具,省去把代码粘贴到网站的麻烦。
另一相关优化是合并(concatenation)——把多个 JS 文件(或 CSS 文件)合并成单个文件。基于自 1999 年起浏览器使用的 HTTP/1.1 协议,浏览器下载单个文件比多个小文件更快。
值得注意的是,新版协议 HTTP/2 于 2015 年发布,可能会改变这一优化策略。HTTP/2 允许多个同时连接,所以理论上多个小文件比一个大合并文件更好。但实践中没那么简单,合并仍有重要好处。截至 2018 年,合并 JS 和 CSS 文件仍是常见做法。
要合并文件,理论上可以手动做——把每个 JS 文件内容复制到一个文件里,CSS 同理。然后修改 HTML 以链接单个合并后的 JS 和 CSS 文件。但每次部署都要这么做,维护起来很痛苦——最好用自动化流程(下文详述)。
近年来流行的另一优化是内联关键 CSS(critical CSS)。这要用工具识别用户访问页面时首先看到的所有 HTML 元素:
(来源:Smashing Magazine)
识别这些元素后,工具会找出影响它们的 CSS,并直接添加到 HTML 文件里。这样浏览器无需等待剩余 CSS 下载,就能显示完整样式的网站!
有不同工具能帮你识别关键 CSS,从 Addy Osmani 的 critical(基于 node)到 Jonas Ohlsson Aden 的 Critical Path CSS Generator(基于 Web)。下面是用 critical 工具分析后,前面 Bootstrap 例子的 <head>:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>My test page</title> <style> :root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}\*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar}@-ms-viewport{width:device-width}article,footer,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}h1,h2{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}ul{margin-top:0;margin-bottom:1rem}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}img{vertical-align:middle;border-style:none}button{border-radius:0}button,input{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button{text-transform:none}[type=submit],button{-webkit-appearance:button}[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}h1,h2{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}h1{font-size:2.5rem}h2{font-size:2rem}.lead{font-size:1.25rem;font-weight:300}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}.img-fluid{max-width:100%;height:auto}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.col-4{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem}.form-control::-ms-expand{background-color:transparent;border:0}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}@media (min-width:576px){.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.fade:not(.show){opacity:0}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .active>.nav-link{color:rgba(0,0,0,.9)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.bg-secondary{background-color:#6c757d!important}.bg-light{background-color:#f8f9fa!important}.my-2{margin-top:.5rem!important}.my-2{margin-bottom:.5rem!important}.mr-auto{margin-right:auto!important}@media (min-width:576px){.my-sm-0{margin-top:0!important}.my-sm-0{margin-bottom:0!important}.mr-sm-2{margin-right:.5rem!important}}@media (min-width:992px){.my-lg-0{margin-top:0!important}.my-lg-0{margin-bottom:0!important}} </style> <link rel="preload" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" as="style" onload="this.onload=null;this.rel="stylesheet""> <noscript><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"></noscript> <script>!function(n){"use strict";n.loadCSS||(n.loadCSS=function(){});var o=loadCSS.relpreload={};if(o.support=function(){var e;try{e=n.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),o.bindMediaToggle=function(t){var e=t.media||"all";function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},o.poly=function(){if(!o.support())for(var t=n.document.getElementsByTagName("link"),e=0;e<t.length;e++){var a=t[e];"preload"!==a.rel||"style"!==a.getAttribute("as")||a.getAttribute("data-loadcss")||(a.setAttribute("data-loadcss",!0),o.bindMediaToggle(a))}},!o.support()){o.poly();var t=n.setInterval(o.poly,500);n.addEventListener?n.addEventListener("load",function(){o.poly(),n.clearInterval(t)}):n.attachEvent&&n.attachEvent("onload",function(){o.poly(),n.clearInterval(t)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:n.loadCSS=loadCSS}("undefined"!=typeof global?global:this);</script> <script defer src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> <!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script> <![endif]--> </head> <body> <!-- ... --> </body></html>可见工具加了一个 <style> 元素,内含大量 CSS。注意这不是 Bootstrap 的全部 CSS,只是工具分析认为初始视图必需的部分。Bootstrap 压缩后的 CSS 本身就 51 KB;而这个新 HTML 文件(含所有 HTML + 内联 CSS + JavaScript)仅 12 KB。这个体积缩减比看起来更重要——让初始 HTML/CSS/JS 低于 14 KB,能在一些最慢的连接上毫秒级渲染。原因是浏览器与服务器之间的每次往返只能传输约 14 KB——把所有内容塞进一次往返,就能避免额外往返的开销(详情见此)。
工具还在 CSS 的 <link> 上加了 rel="preload",让 CSS 文件能异步加载。通常你不该这么做——虽然能加速,但用户会先看到无样式的裸 HTML,等 CSS 加载完才看到正确样式。但在本例中,由于我们已经内联了关键 CSS,这就不是问题了,所以异步加载剩余 CSS 完全可行!
至此,你可以每次部署网站时手动压缩、合并文件,但那会很痛苦。理想情况下,你会用一个命令自动化这套流程——这就是构建步骤(build step)。压缩与合并只是两种可能的任务——任何可自动化的重复性工作都能变成构建任务。以下是典型的构建任务:
- 压缩 HTML、CSS、JavaScript
- 合并 JS 文件和 CSS 文件
- 内联关键 CSS
- 优化图片(调整大小、移除元数据等)
- 添加 CSS 厂商前缀以兼容浏览器
- 转译代码(SASS → CSS、CoffeeScript → JS 等)
- 运行代码测试
要实现构建步骤,需要选一个工具,选择很多。2012 年发布的 Grunt 曾是流行之选,随后是 Gulp,以及 Broccoli.js、Brunch、webpack。截至 2018 年,webpack 似乎最受欢迎,但归根结底,这些工具都能很好地实现构建步骤。
注:从零开始学习构建工具可能令人望而生畏。多数工具要求用命令行——若你从未用过,可读 这篇教程 入门。2018 年多数流行的 Web 开发构建工具基于 node.js——若你不熟悉 node.js 生态及其在前端开发中的应用,也可看我写的 《Modern JavaScript Explained For Dinosaurs》 了解概况。

模板与服务器端渲染
至此我们有了一个不错的网页——既有吸引力又性能良好。现在它长这样:

导航栏里有个链接指向「About」页面,但目前无处可去。若想制作这个 About 页面怎么办?最直接的答案是复制 index.html 为 about.html,并相应更新内容。具体来说,<main> 元素里的内容要改,其余 HTML 保持不变。下面是简单的 about.html:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>My test page</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <script defer src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> <!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script> <![endif]--> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Info</a> </li> <li class="nav-item"> <a class="nav-link" href="#">About</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </nav> <main role="main"> <h1>About</h1> <p>Info about this site</p> </main> <footer class="navbar navbar-dark bg-secondary" role="contentinfo"> <p>Copyright info goes here</p> </footer> </body></html>注意 about.html 里除高亮内容外,其余与 index.html 完全相同。虽然能用,但从维护角度看很成问题。若用这技术做 7 个不同页面,会有大量重复代码。日后若想改导航栏,就得把改动复制到所有 7 个文件里。这违反了著名的软件原则 DRY(Don’t Repeat Yourself,不要重复自己)。
一个解决方案是使用模板引擎。这要在 HTML 里写非标准的 HTML,然后交给另一个程序,把非标准 HTML 替换为标准 HTML。最好用示例说明。
假设你用 PHP——最早为与 HTML 协同工作而设计的语言之一(至今仍被许多大公司使用)。你要创建 head.php(含 <head> 元素内容)、header.php(含导航栏元素)、footer.php(含页脚元素)。
有了这些文件后,可以创建 index.php 如下:
<!DOCTYPE html><html lang="en"> <head> <?php include("head.php");?> </head> <body> <?php include("header.php");?> <main role="main"> <section class="jumbotron"> ... </section> <section class="container"> ... </section> </main> <?php include("footer.php");?> </body></html>about.php 则长这样:
<!DOCTYPE html><html lang="en"> <head> <?php include("head.php");?> </head> <body> <?php include("header.php");?> <main role="main"> <h1>About</h1> <p>Info about this site</p> </main> <?php include("footer.php");?> </body></html>可见只有中间内容变了。若要更新 header、footer 或外部依赖,只需改一次。
上面的代码显然不是有效的 HTML——你需要某种构建步骤,把 include 语句替换为独立文件中的 HTML。我们其实可以把它纳入前面看到的构建步骤(用于代码压缩、文件合并、关键 CSS 等)。但这步从模板生成 HTML 的操作,传统上是在服务器端动态完成的。
(来源:wikipedia.org)
服务器是接收 Web 请求并返回 HTML/CSS/JS 的计算机(相对客户端——发起请求、带浏览器的计算机)。服务器通常负责根据数据库中的数据创建动态 HTML。例如,在 www.google.com 搜索「red bananas」时,并没有某个专门关于 red bananas 的 HTML 文件发给你。相反,服务器运行代码,根据你的搜索词动态创建 HTML 响应。所以这里能一箭双雕——既然已经有了在服务器端生成动态 HTML 的步骤,就能用模板来定义生成的 HTML,让代码保持 DRY。
在服务器端用模板构建 HTML 是长期以来的事实标准。除 PHP 外,还有 Ruby on Rails 的 ERB、Python Django 的 Django 模板语言、Node.js Express 的 EJS 等。这种方法可能相当令人望而生畏——要想利用模板引擎写出可维护的代码,基本上得先学整门编程语言和 Web 框架!若你本打算搞服务器和数据库,这很自然。但若你只在前端写 HTML,老实说,这是个巨大的门槛。
注:若你的网站不需要数据库,可用静态站点生成器——用模板构建静态 HTML 文件(Jekyll、Hugo、Gatsby 是流行选择)。相比服务器端 Web 框架,静态站点生成器可能更简单;但你仍得学一门独立的编程语言或环境,所以相比纯 HTML 仍有门槛。
组件与客户端 JavaScript 框架
Web Components 于 2011 年首次引入,是一种完全不同的方法,用来解决 HTML 的可维护性问题。Web Components 是在客户端构建的,无需学习服务器端编程语言和框架就能写出可维护的 HTML。
Web Components 的总体目标是创建可复用的部件。回顾前面的例子,你可以创建 navbar 组件、header 组件、footer 组件。进一步,还能创建 jumbotron 组件和 articles 组件来放页面内容。然后就能在 index.html 里这样用:
<!DOCTYPE html><html lang="en"> <head> ... </head> <body> <navbar-component></navbar-component> <main role="main"> <jumbotron-component></jumbotron-component> <section class="container"> <articles-component></articles-component> </section> </main> <footer-component></footer-component> </body></html>要让它工作,需要创建自定义元素——本质上是为 HTML 语言定义新元素(只在这个站点有效)。创建自定义元素必须用 JavaScript。下面是创建 navbar 自定义元素所需的 JS:
window.customElements.define( "navbar-component", class extends HTMLElement { connectedCallback() { this.innerHTML = \` <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Info</a> </li> <li class="nav-item"> <a class="nav-link" href="#">About</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </nav> \`; } });JS 开头看起来有点复杂(用了 ES2015 新特性),但本例中它只是定义 navbar 的 HTML(上面粗体部分)并将 navbar-component 注册为自定义元素。注意 HTML 是用模板字符串(template literal,本质上是超长字符串)在 JS 文件中定义的——若你想把它搬回普通 HTML 文件,需要别的机制,稍后讨论。若把这段 JS 加到页面,你就能用 <navbar-component> 元素了。
至此,相比服务器端模板方法,这方法似乎没太大优势。但当你开始给每个组件加 JS 功能和 CSS 样式时,好处就更明显了。Web Components 提供功能隔离和样式隔离,让组件可复用——不仅对这个站点,理论上还能跨多个项目复用。现有 HTML 元素如 <video> 就能看到这概念:
<video width="320" height="240" controls loop muted> <source src="movie.mp4" type="video/mp4"> <source src="movie.ogg" type="video/ogg"> <p>Your browser doesn't support HTML5 video. Here is a <a href="movie.mp4">link to the video</a> instead.</p></video>你会得到一个视频播放器,长这样:
(这是视频的图片,不是真视频)
<video> 元素自带 JS 交互控件和 CSS 样式,与页面其余部分隔离。这意味着用 <video> 元素时,不用担心它影响站点的样式或功能,也不必担心站点的 CSS 或 JS 破坏视频组件。
Web Components 的目标是让开发者能创建自己的自定义组件,像 <video> 一样,享有隔离和复用的好处。下面是 Web Components 规范各部分如何协同工作:
- 可用 JS 创建自定义元素(如前所示),并定义自定义功能,还能从 HTML 传属性给自定义元素。
- 可用 Shadow DOM 给自定义元素应用 CSS,且只作用于该元素而非整篇文档(解决 CSS 最难的问题之一)。
- 若不想直接在 JS 里写所有 HTML,可用 HTML 模板——在
<template>标签里写组件的 HTML,默认不渲染,等 JS 调用时才渲染。 - 为组织代码,可用 HTML Imports——把定义组件所需的 HTML、CSS、JS 放在
navbar-component.html文件中,然后像外部 CSS 那样在主 HTML 中导入:<link rel="import" href="navbar-component.html">。
2011 年首次公布时,许多开发者对 Web Components 的可能性感到兴奋。虽然服务器端模板方法解决了部分 HTML 可维护性问题,但 Web Components 提供了完全不同的东西——扩展 HTML 本身的承诺,打造功能完备的可复用部件。这是让 Web 平台从原本设计用于简单静态内容站点,转向开发复杂应用所缺失的那块拼图。
那后来怎样?接下来几年,浏览器厂商显然对 Web Components 作为标准无法达成一致。截至 2018 年,几乎没有浏览器完整支持上述四大特性,原因是实现性能问题、标准冲突、厂商利益不一。这让开发者陷入尴尬境地——几乎人人都认同组件式方法是将 Web 从静态站点推向复杂应用的必要部分,但干等浏览器支持似乎是徒劳。怎么办?
当 HTML Web Components 规范短期内无法实现变得明朗时,JavaScript 已经是一门足够强大的语言来补位。开发者一直用 jQuery(2006 年发布)构建复杂应用,虽然很难组织大规模应用的代码。Backbone.js(2010 年发布)是最早的流行框架之一,用于组织大型单页应用(SPA)的代码,随后是 AngularJS、Ember.js 等。
所有这些框架都用现有 JS 特性——不必干等浏览器实现 Web Components 规范。但没一个框架用真正的隔离可复用组件;没有 Web Components 规范的四件套(自定义元素、Shadow DOM、HTML 模板、HTML Imports),似乎不可能。
2013 年,React 发布了,对这局面有个有趣的解法。它不用 Web Components 规范就做出了真正的组件式框架,方法如下:
- 不用 Web Components 的自定义元素规范,React 选择在 JS 里定义所有 HTML。本质上用 JS 函数输出想要的 HTML,用一种叫 JSX 的特殊语法(长得像 HTML,但用构建步骤转成 JS 函数)。
- 不用 Web Components 的 HTML 模板规范,React 不让在 JS 之外写 HTML。
- 不用 Web Components 的 HTML Imports 规范,React 选择在 JS 里导入 JS。这在当时其实还不能直接实现,但 Browserify 和 webpack 等工具让开发者能在 JS 里写
require或import语句,构建时转成单个 JS 包。
本质上的洞见是:全用 JS 能让组件工作。注意 Web Components 规范里缺了一块——Shadow DOM——React 刚发布时没有样式隔离的解决方案。但这已足够提供一个用组件构建应用的框架。
下面是用 React 写 navbar 组件的 JS:
import React, { Component } from "react";
class Navbar extends Component { render() { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <ul className="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Info</a> </li> <li class="nav-item"> <a class="nav-link" href="#">About</a> </li> </ul> <form className="form-inline my-2 my-lg-0"> <input className="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"/> <button className="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </nav> ); }}
export default Navbar;这与前面的自定义元素示例没太大不同(虽然 JSX 语法让多数开发者初次见时感到别扭)。React 的思路是把它进一步拆成子组件,让每个组件只做一件事。
React 刚发布时遭到很多批评,尤其是它看似缺乏关注点分离(开发者一直被教导要把 HTML、CSS、JS 完全分开以保可维护性)。但 React 推进了一个理念:对复杂 Web 应用,关注点分离不是技术之间的边界(HTML、CSS、JS),而是功能单元之间的边界(即组件)。
除了是组件式框架,React 还对如何管理应用内数据有强烈主张——用声明式方法。这意味着用 React 时,你不必写代码直接更新界面,而是定义界面应该长什么样(用 JSX),写代码更新数据,然后让 React 用它的虚拟 DOM(virtual DOM,别和 Shadow DOM 混淆)实现决定如何高效更新和渲染界面。这是 Web 框架设计的重大转变,影响深远——之后每个主流框架都公开借鉴了 React 的声明式方法和虚拟 DOM 实现:Ember、Angular、Vue.js 等等。截至 2018 年,Web 开发社区已广泛接受这种范式作为构建现代 Web 应用的方式。
注意,想要清晰可维护地写 HTML,最终把我们需要大量编程知识;尤其几乎不可能避开 JavaScript。在某种意义上,这打破了 HTML 的承诺——HTML 被设计成一门不需要懂编程就能有效使用的语言。也许未来开发者能用纯 HTML 分享预制 Web 组件,但那一天可能还很远。
本节只简要概述了 React 及其他类似框架的前端方法。若想更完整地了解如何用各种 JS 框架和方法构建可运行应用,可看我写的系列文章 Comparing Frontend Approaches: A look at jQuery, Vue.js, React, and Elm。
结语
简单总结一下现代 HTML:我们用合适的标签和 ARIA 属性编写语义化、可访问的内容;用 CSS 和 JavaScript 添加样式和动态特性;用 HTML 属性和工具链改善性能;最后用模板和组件提升可维护性。一路走来,可见要充分利用现代 HTML,几乎不可能避开构建流程和某种独立的编程语言(多数情况下是 JavaScript)。
从宏观角度看,有时会让人泄气:曾经简单易懂的事(用 HTML 做网站),如今变得复杂且看似难以接近(用 JS 前端框架构建 Web 应用,用构建流程处理成千上万个脆弱的依赖)。但要记住,Web 开发作为一个行业只存在了约 30 年——相比其他行业(如建筑,已有数百年历史),这只是历史的极小片段。就好比 Web 开发者刚学会用黏土建房子,现在就被要求用同样的工具建摩天大楼——工具和流程必然会演化;我们只需确保它以包容的方式演化,不负 Web 作为一个民主平台的初衷。
现代 HTML 确实可能令人沮丧,因为它继续快速变化和演进。但我们现在能做的比以往任何时候都多,而我们所有人都处在这个新兴行业的起步阶段,有机会把它塑造成我们想要的平台。这是成为开发者的激动人心的时代,希望这份信息能作为路线图帮助你踏上旅程!

特别感谢 @ryanqnorth 的 Dinosaur Comics,自 2003 年(恐龙统治 Web 的时代)以来一直提供最棒的荒诞幽默。
本文为学习目的的个人翻译,译文仅供参考。
原文链接:Modern HTML Explained For Dinosaurs。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。