[译]为恐龙解释现代HTML

Dinosaur comic panel 1

在三种主要的前端技术( HTML,CSS 和 JavaScript )中,HTML 仍然是最一致的。如果您唯一关心的是创建内容,那么 1990 年代的 HTML 文档看起来与 2018 年创建的文档非常相似:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
  </head>
  <body>
    <h1>Hello there!</h1>
  </body>
</html>

你有带有标签和内容的元素,带有属性的标签——除了第一行的简化文档类型外,没有太大变化!然而,多年来,Web 开发已经发生了巨大的转变,从创建静态网站(专注于内容)到创建动态 Web 应用程序(专注于交互)——这是 Web 最初设计的目的。创建仍然语义和可访问的自定义用户界面,使用属性和工具提高性能,组织代码以进行重用和可维护性 - 现在有一组全新的问题在起作用。

本文的目的是提供一个历史背景,说明 HTML 如何在 2018 年演变成今天的语言。我们将从结构良好且易于访问的 HTML 的基础知识开始,就像古代的恐龙一样。然后,我们将介绍不同的技术来提高性能、响应能力和可维护性。CSS 和 JavaScript 将不可避免地进入这个对话;出于本文的目的,将从它们如何影响 HTML 本身编写的角度来介绍它们。通过了解这段历史,您将能够充分利用该语言经常被忽视的新旧功能。让我们开始吧!

使用语义元素编写内容

让我们向前面的 HTML 示例添加更多内容。现在,我们将创建一个基本网站,其中包含一个带有链接和搜索输入的导航部分,一个用于显示一般网站信息的大型展示部分(通常称为英雄部分或 巨型屏幕 ),文章的三列部分和一个版权信息的页脚部分。

<!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> 等标签的基本元素来标记内容。这里的 HTML 是有效的,但它不是完全语义的——也就是说,标签不能尽可能地传达内容的含义。

当 HTML5 在 2008 年推出时,它提供了新的元素来改进文档语义。以下是使 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>
    <!--[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]>...--> 注释添加了一个 JavaScript 文件,该文件仅适用于旧版本的 Internet Explorer,因为它们不支持上述 HTML5 标签。请注意,如今许多最近的网站都没有包含该评论,因为支持这些浏览器的网站越来越少。
  • role 属性还提供辅助功能信息。请注意,使用 <nav> 标记通常足以确保可访问性,如果无法识别 <nav> 标记,则使用额外的 role="navigation"

编写语义 HTML 似乎并不重要,尤其是当它不影响网站的视觉外观时。但是,您的网站不仅被人类查看 - 网络浏览器,搜索引擎,屏幕阅读器都依赖于语义 HTML 才能正常运行。

使用 WAI-ARIA 属性改进可访问性

让我们看一下上面示例中导航栏中的搜索输入:

<form>
  <input type="text" placeholder="Search" />
  <button type="submit">Search</button>
</form>

此输入元素使用 placeholder 属性而不是标签元素让用户知道其用途。这适用于人类,但它不是正确的语义 HTML,屏幕阅读器和其他技术可能会错过。使其可访问的方法是使用 aria-label 属性:

<form>
  <input type="text" placeholder="Search" aria-label="Search" />
  <button type="submit">Search</button>
</form>

WAI-ARIA 代表“Web 可访问性倡议 — 可访问的富互联网应用程序”(通常简称为 ARIA),是一组属性,当语义标记不够时,使 HTML 更易于访问。上一节中看到的 role 属性是 ARIA 属性。到目前为止,这些属性似乎是很小的变化,但是当我们使用 HTML 来处理基本文档之外的事情时,它变得更加重要。

让我们看一个更复杂的例子——假设我们想在 HTML 中添加一些选项卡式内容,以提供有关如何在 Windows,Mac 和 Linux 上安装某些程序的说明。由于没有在 HTML 中构建选项卡的本机方法,因此我们必须使用无序列表、链接和 div 之类的东西来构建我们自己的选项卡标记。在这里,我们可以使用 rolearia-controlsaria-selectedaria-labelledby 等可访问性属性来标记 HTML,如下所示:

<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"
      alt="microsoft logo"
    />
    ...
  </div>
  <div id="mac" role="tabpanel" aria-labelledby="mac-tab">
    <img
      src="https://i.ytimg.com/vi/ipOzBWuYZvg/maxresdefault.jpg"
      alt="apple logo"
    />
    ...
  </div>
  <div id="linux" role="tabpanel" aria-labelledby="linux-tab">
    <img
      src="https://noware.tech/wp-content/uploads/sites/140/2018/04/linux-1024x565.jpg"
      alt="linux logo"
    />
    ...
  </div>
</div>

如果没有这些辅助功能属性,选项卡式内容将与选项卡控件没有可识别的关系。具有这些属性有助于屏幕阅读器识别内容,启用具有正确制表符的键盘快捷键等。使用适当的 ARIA 属性本身就是一项完整的研究——要更深入地了解,请查看 官方指南

所有这些似乎都是为了提高网站的可访问性而做很多工作。重要的是要承认可访问性是网络的一个组成部分,网络被设计为一个与每个人自由共享信息的平台,而不仅仅是少数人。使网站可访问可以改善每个访问者的体验 - 例如,可访问的键盘快捷键可以帮助那些永远无法使用鼠标的人,那些暂时无法使用鼠标的人,以及那些不喜欢不使用鼠标的人(也就是大多数程序员)。在处理其他功能时,可访问性很容易被忽视,但不应忽视。

如果您有兴趣改善网站的可访问性,那么有 A11Y 项目 中的清单,这是 一个 很好的起点。然而,使网站可访问不仅仅是检查项目——它总是可以改进的,就像用户体验的任何方面一样。使您的网站可访问的最佳方法是实际使用您的网站,就像受众中的不同人一样 - 使用屏幕阅读器进行测试,尝试仅使用键盘而不是鼠标,使用 色盲过滤器 查看您的网站等。

用 CSS 和 JavaScript 让它变得漂亮

如果我们到目前为止看一下网站,它看起来就像你期望的那样光秃秃的:

Example website without styling

为了美化它,我们将添加一个 CSS 文件来应用样式。现在,如果你不是特别擅长 CSS,可能需要很多天才能使这个网站看起来很漂亮。与其编写自己的 CSS,不如始终使用 CSS 框架,该框架本质上是其他人以可重用的方式编写的 CSS。

一个流行的 CSS 框架是 Bootstrap,它于 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 添加到我们的网站。请注意,我们链接到在线托管的文件,这可能会带来一些安全风险 - integritycrossorigin 属性有助于确保链接到的文件正确无误。

  • 添加的类都是特定于 Bootstrap 的 — 添加的 Bootstrap CSS 具有针对具有特定 HTML 结构的特定类名的样式。

  • 在三篇文章周围添加了一个额外的 <div class="row"> ,以利用 Bootstrap 的网格布局系统(它使用这种特定的 HTML 结构)。

以下是该网站现在的样子:

Example website with Bootstrap styling

不错!请注意,为了使用像 Bootstrap 这样的 CSS 框架,您实际上根本不需要编写任何 CSS 即可开始使用 — 您只需要在 HTML 中添加适当的类即可利用框架附带的 CSS。

这里要注意的一件事是,虽然选项卡的样式正确(任何时候只有一个选项卡可见),但它们还没有正常工作——单击选项卡不会执行任何操作。这是因为在这种情况下,这种类型的自定义交互不是由 CSS 处理的,而是由 JavaScript 处理的。在这种情况下,我们可以通过在 <head> 标签中添加 Bootstrap 框架附带的 JavaScript 文件来让 Bootstrap 选项卡工作:

<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。前两个是 Bootstrap 使用的依赖项(jQuery 和 Popper),在加载 Bootstrap 脚本之前必须先加载它们。如果您查看 此实时示例,您可以看到选项卡现在可以工作了!

Bootstrap 被广泛使用,因为它帮助解决了当时 CSS 的主要痛点,例如浏览器不一致和缺乏适当的网格系统。使用像 Bootstrap 这样的 CSS 框架有一些缺点——特别是,与从头开始编写 CSS 相比,它们可能难以定制,这可能会使您的网站与其他网站相比显得通用。

此外,随着智能手机和移动流量的增加,减少 CSS 和 JS 文件大小变得越来越重要 - 任何超过几千字节的东西都会显着影响较慢的互联网连接的性能。在上面的例子中使用 Bootstrap 的方式,我们要求用户下载整个 Bootstrap 框架与站点一起,即使我们只使用几种样式和功能。在下一节中,我们将介绍几种有助于解决这些性能问题的技术。

注意:对 CSS 和 JavaScript 的扎实掌握与使用 HTML 制作复杂的网站有着内在的联系;但是,深入研究这些语言超出了本文的范围。如果你想了解更多关于 CSS 和 JavaScript 的基础知识,MDN Web Docs 总是一个不错的起点。如果你想更好地了解 CSS 的所有新功能(flexbox,grid,SASS 等)如何与所涉及的所有工具和技术结合在一起,请查看我的文章 Modern CSS Explain For Dinosaurs

使用 HTML 属性提高性能

在这一点上,我们有一个网站,具有相当好的组织,语义 HTML。如果这就是我们正在考虑的全部内容,那么我们的网站就会完成!然而,在性能(网站为用户加载的速度)和可维护性(开发人员更改代码的难易程度)方面,网站有许多方面可以改进。

脚本的 defer 属性

对于我们的网站,一个主要的优化是解决标题中加载的 JavaScript 文件。这些文件足够大,实际上会减慢网站的速度。为了呈现页面,Web 浏览器读取给定的 HTML 并将其转换为它理解的格式 - 文档对象模型或 DOM。如您所料,Web 浏览器从 HTML 文档的顶部开始,然后向下工作。这意味着如果它看到 <script> 标记,它将下载并执行脚本,然后再转到下一行。您可以在此处查看此过程的插图:

Diagram of JavaScript normal loading order

一个常见的优化技巧是将所有 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> 标记中会产生降低页面呈现性能的意外副作用。将所有 <script> 个标记移动到 <body> 个标记的底部是提高性能的一种方法。

在 2018 年,许多网站仍然使用这种将所有 <script> 标签移动到 <body> 标签底部的技巧。然而,浏览器已经支持了一种不那么黑客的方法近 10 年—— defer 属性。通过将此属性添加到 <script> 标记中,浏览器将下载外部文件而不会阻止构建 DOM 的其余部分,并将在 DOM 构建完成后执行脚本。您可以在此处查看此过程的插图:

Diagram of JavaScript with defer attribute loading order

在许多情况下,在 <head> 中保留 <script> 个标签和 defer 属性将导致更快的页面加载速度,因为文件可以与正在构建的 DOM 并行下载。这就是 Bootstrap 的初学者模板使用 defer 属性的样子:

<!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>

这样做的好处是网站会渲染得更快,并且它会按预期使用 <head> 标记中的脚本组织 HTML。为了更精细地控制哪些文件以什么顺序下载和执行,还有 async 属性以及 <link> 个标签的 rel="preload" 属性(您可以在 此处 阅读更多相关信息)。

图像的 srcset 属性

对于我们的网站,另一个主要的性能优化是图像。现在,这些图像正在被“热链接”,这意味着它们被直接链接到其他人的网站上。这不仅从维护的角度来看是有问题的(如果其他人改变他们的形象,就会破坏我们的网站),从性能的角度来看,它也可能是有问题的。

我们可以下载文件并在本地链接到它们,而不是直接链接文件。此外,我们可以通过将图像文件大小调整为适当的分辨率来优化图像文件大小。而不是直接链接到另一个网站上的单个图像的图像标签:

<img
  class="img-fluid"
  src="http://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png"
  alt="microsoft logo"
/>

我们可以在本地创建图像的多个版本,并响应式地链接到它们:

<img
  class="img-fluid"
  src="microsoft-logo-small.png"
  srcset="microsoft-logo-medium.png 1000w, microsoft-logo-large.png 2000w"
  alt="microsoft logo"
/>

在这里,我们使用徽标的小型、中型和大型版本。 srcset 属性告诉浏览器根据浏览器宽度加载适当的版本。 srcset 属性是在 2013 年左右引入的,但浏览器花了几年时间才完全支持它。截至 2018 年,它具有 相当不错的浏览器支持 ,因此绝对值得将其作为工作流程的一部分。

对于许多网站来说,优化图像大小通常是最大的性能提升——图像下载大小通常比任何 JavaScript 和 CSS 文件大几个数量级。您可以使用 <picture> 元素对图像进行更精细的控制;但是,对于大多数用例来说,使用简单的 srcset 属性 通常绰绰有余

其他 HTML 属性

作为一种语言,HTML 具有 许多属性 ,并继续添加可用于提高性能的新属性(如 importancelazyload )。虽然这可能令人生畏,但专注于下载大小(图像和脚本)方面的最大资源,无论它们可能适用于您的特定网站,通常是最好的起点。

请注意,就像可访问性一样,没有一套全面的性能规则始终适用于每个网站——您应该对您的网站进行基准测试以确定最有效的方法(Chrome 和 Firefox 等浏览器提供此类工具)。同样,最好的方法是简单地在慢速网络条件下使用您的网站(浏览器的开发工具可以模拟)——如果您在慢速网络条件下使用自己的网站即使只是一周,您很可能会找到大量性能修复来改善其体验。

使用工具提高性能

到目前为止,我们一直在使用 HTML 语言提供的工具来优化性能。您还可以使用外部工具获得更多性能优势。让我们来看看几种常用的方法。

代码缩小

一个重要的性能优化是 JavaScript 和 CSS 代码的缩小(有时称为丑化)。这涉及使用程序来分析和删除代码中不必要或冗余的数据,从简单的事情(如删除不需要的空格)到复杂的事情,如尽可能将长变量重命名为单个字符。下面是 Douglas Crockford 在 2003 年发布 的第一个 JavaScript 缩减器的示例。示例未缩小的代码如下所示:

// is.js

// (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。这还不到原来尺寸的一半——使用Grabthar’s hammer,真是省钱!但说真的,它在网站下载和显示的速度方面有很大的不同,尤其是在互联网连接速度较慢的情况下。

我们在前面的例子中使用的 Bootstrap CSS 和 JavaScript 已经缩小了,但如果你想缩小自己的代码,你可以使用 JavaScript MinifierMinify 等在线工具,有很多可供选择。或者,您可以使用命令行工具,该工具可以节省将代码复制到网站的过程。

文件串联

另一个相关的性能优化是串联,它将多个 JavaScript 文件(或 CSS 文件)转换为单个文件。浏览器下载单个文件的速度比下载多个小文件更快,这是基于浏览器自 1999 年以来使用的 HTTP/1.1 协议。

需要注意的是,新版本的协议 HTTP/2 于 2015 年发布,可能会改变此优化。HTTP / 2 允许多个同时连接,因此理论上最好有多个小文件,而不是一个大级联文件。然而,在实践中,它 似乎并不那么简单,因为串联仍然有重要的好处。截至 2018 年,连接 JavaScript 和 CSS 文件仍然是常见的做法。

要连接您的文件,理论上您可以手动完成 - 将每个 JavaScript 文件的内容复制到单个文件中,对 CSS 文件重复此操作,等等。然后修改 HTML 以链接到单个串联的 JavaScript 文件和单个串联的 CSS 文件。每次部署应用程序时都必须执行此操作,维护起来会非常痛苦 - 最好使用一些自动化过程(更多内容见下文)。

关键 CSS

近年来流行的另一个优化是内联页面的关键 CSS。这涉及使用工具来识别用户在访问网页时首先看到的所有 HTML 元素:

Example website labelling critical portion

一旦识别出这些 HTML 元素,该工具将找到影响这些元素的所有 CSS,并将它们直接添加到 HTML 文件中。通过这种方式,浏览器能够显示一个完全样式的网站,而无需等待剩余的 CSS 下载!

有不同的工具可以帮助您识别 关键 CSS,从 Addy Osmani 的基于节点的关键库到 Jonas Ohlsson Aden 的基于 Web 的关键路径 CSS 生成器。下面是我们之前 Bootstrap 示例中的 HTML <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: 0.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: 0.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](http://twitter.com/media "Twitter profile for @media") (min-width:576px)
      {
        .container {
          max-width: 540px;
        }
      }
      [@media](http://twitter.com/media "Twitter profile for @media") (min-width:768px)
      {
        .container {
          max-width: 720px;
        }
      }
      [@media](http://twitter.com/media "Twitter profile for @media") (min-width:992px)
      {
        .container {
          max-width: 960px;
        }
      }
      [@media](http://twitter.com/media "Twitter profile for @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: 0.375rem 0.75rem;
        font-size: 1rem;
        line-height: 1.5;
        color: #495057;
        background-color: #fff;
        background-clip: padding-box;
        border: 1px solid #ced4da;
        border-radius: 0.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](http://twitter.com/media "Twitter profile for @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: 0.375rem 0.75rem;
        font-size: 1rem;
        line-height: 1.5;
        border-radius: 0.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: 0.5rem 1rem;
        font-size: 1.25rem;
        line-height: 1.5;
        border-radius: 0.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: 0.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: 0.25rem;
        border-top-right-radius: 0.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: 0.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](http://twitter.com/media "Twitter profile for @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: 0.5rem;
          padding-left: 0.5rem;
        }
      }
      .navbar-light .navbar-nav .nav-link {
        color: rgba(0, 0, 0, 0.5);
      }
      .navbar-light .navbar-nav .active > .nav-link {
        color: rgba(0, 0, 0, 0.9);
      }
      .jumbotron {
        padding: 2rem 1rem;
        margin-bottom: 2rem;
        background-color: #e9ecef;
        border-radius: 0.3rem;
      }
      [@media](http://twitter.com/media "Twitter profile for @media") (min-width:576px)
      {
        .jumbotron {
          padding: 4rem 2rem;
        }
      }
      .bg-secondary {
        background-color: #6c757d !important;
      }
      .bg-light {
        background-color: #f8f9fa !important;
      }
      .my-2 {
        margin-top: 0.5rem !important;
      }
      .my-2 {
        margin-bottom: 0.5rem !important;
      }
      .mr-auto {
        margin-right: auto !important;
      }
      [@media](http://twitter.com/media "Twitter profile for @media") (min-width:576px)
      {
        .my-sm-0 {
          margin-top: 0 !important;
        }
        .my-sm-0 {
          margin-bottom: 0 !important;
        }
        .mr-sm-2 {
          margin-right: 0.5rem !important;
        }
      }
      [@media](http://twitter.com/media "Twitter profile for @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](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](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](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](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](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](https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js)"></script>
    <![endif]-->
  </head>
  <body>
    <!-- ...  -->
  </body>
</html>

如您所见,该工具添加了一个带有大量 CSS 的内联 <style> 元素。请注意,这不是 Bootstrap 的全部 CSS,只是该工具分析的 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 可以完美地工作!

实现构建步骤

此时,您可以执行诸如每次部署网站时手动缩小和连接文件之类的操作,但这将是一个巨大的痛苦。理想情况下,您将使用单个命令自动执行这组任务,这称为生成步骤。缩小和串联只是两个可能的任务 - 任何可以自动化的重复性任务。下面是生成步骤中的一些典型任务:

  • 缩小 HTML、CSS 和 JavaScript

  • 连接 JavaScript 文件和 CSS 文件

  • 优化图像(通过调整大小、删除未使用的元数据等)

  • 添加 CSS 供应商前缀以实现浏览器兼容性

  • 转译代码(从 SASS 到 CSS,或从 CoffeeScript 到 JS,等等)

  • 运行代码测试

要实现构建步骤,您需要选择一个工具,并且有很多工具可供选择。一个流行的选择是 Grunt,它于 2012 年发布。紧随其后的是 Gulp,以及 Broccoli.js,Brunch 和 webpack。截至 2018 年,webpack 似乎是最受欢迎的选择,但最终这些工具中的任何一个都将用于很好地实现构建步骤的目的。

注意:从头开始学习使用工具进行构建步骤可能非常令人生畏。大多数工具都要求您使用命令行 — 如果您以前从未使用过,您可以阅读本教程以获得入门的良好概述。2018 年许多流行的 Web 开发人员构建工具都是基于 node.js 的——如果你不熟悉 node.js 生态系统及其在前端开发中的使用,你可以阅读我的文章 Modern JavaScript Explain For Dinosaurs,了解这方面的概述。

Dinosaur comic panels 3 and 4

模板和组件提高可维护性

到目前为止,我们有一个不错的网页,既有相当的吸引力,又有性能。现在它看起来像这样:

Example website with Bootstrap styling

在导航栏中,有一个指向“关于”页面的链接,但它目前没有转到任何地方。如果我们想制作这个关于页面怎么办?最直接的答案是复制名为 about.htmlindex.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](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](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](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](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](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(不要重复自己)。

在服务器上构建模板

此问题的一个解决方案是使用模板引擎。这涉及在 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>

所以在这里你可以看到唯一改变的部分是中间的内容。如果需要更新页眉、页脚或外部依赖项,则只需更改一次。

上面的代码显然不是有效的 HTML — 您需要某种构建步骤来将 include 语句替换为单独文件中的 HTML。我们实际上可以将其合并到我们之前看到的构建步骤中(用于代码缩小、文件连接、关键 CSS 等)。但是,从模板生成 HTML 的这一步传统上是在服务器上动态完成的。

Diagram of client server model

服务器是接收 Web 请求并将 HTML/CSS/JS 作为 Web 响应发送回的计算机(与客户端相反,具有启动 Web 请求的 Web 浏览器的计算机)。服务器通常负责基于数据库中的数据创建动态 HTML。例如,如果您在 www.google.com 上搜索“红色香蕉”,则不会有一些关于红色香蕉的唯一 HTML 文件从服务器发送给您。相反,服务器运行代码以根据您的搜索词动态创建 HTML 响应。所以在这里你可以用一块石头杀死两只鸟——因为你已经有一个步骤在服务器上生成动态 HTML,你可以使用模板来定义生成的 HTML 来保持你的代码干燥。

在服务器上使用模板构建 HTML 是一种解决方案,在相当长的一段时间内一直是事实上的标准。除了 PHP,还有 Ruby on Rails 框架的 ERB,Python 的 Django 框架的 Django 模板语言,Node’s Express 框架的EJS等。这种方法可能非常令人生畏——为了利用模板引擎编写可维护的代码,您基本上首先必须学习整个编程语言和 Web 框架!如果您已经计划使用服务器和数据库,那么这是很自然的选择。但是,如果您只是对在前端编写 HTML 感兴趣,那么老实说,这是一个巨大的进入障碍。

注意:如果您的网站不需要数据库,则可以改用静态网站生成器,该生成器使用模板来构建静态 HTML 文件(Jekyll,Hugo 和 Gatsby 是一些流行的选择)。与服务器端 Web 框架相比,静态站点生成器可能更易于使用;但是,您仍然需要学习单独的编程语言或环境,因此与编写纯 HTML 相比,进入仍然存在障碍。

在客户端上使用 Web 组件

Web 组件于 2011 年首次引入,作为解决 HTML 可维护性问题的完全不同的方法。Web 组件是在客户端而不是服务器上构建的,这消除了必须学习服务器端编程语言和 Web 框架来编写可维护的 HTML 的障碍。

Web 组件的总体目标是能够创建可重用的小部件。回顾前面的示例,您可以创建导航栏组件、页眉组件和页脚组件。更进一步,您可以为页面中的内容创建一个巨型组件和一个文章组件。然后,您可以使用 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。下面是创建导航栏自定义元素所需的 JavaScript 示例:

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>  
       `;
    }
  }
);

JavaScript 看起来有点复杂(它使用新的 ES2015 语言功能),但它在这个例子中所做的只是为导航栏定义 HTML(上面以粗体显示)并将名称 navbar-component 注册为自定义元素。请注意,HTML 是使用模板文字在 JavaScript 文件中定义的,本质上是一个巨大的字符串——如果你更喜欢将其移回常规 HTML 文件,我们将需要一个不同的机制,我们将在稍后讨论。如果您将此 JavaScript 添加到页面,您现在可以在普通 HTML 元素之外创建 <navbar-component> 个元素。

到目前为止,在此示例中,此方法与服务器端模板方法相比没有太大优势。但是,当您开始向每个组件添加 JavaScript 功能和 CSS 样式时,好处变得更加明显。Web 组件提供了隔离功能和样式的功能,以保留在每个组件中,使它们可重用 — 不仅用于此站点,而且理论上可以在多个项目中重用。这个概念可以在现有的 HTML 元素(如 <video> 元素)中看到。如果你像这样写 HTML:

<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>

你会得到一个看起来像这样的视频播放器:

Example HTML video element

元素带有自己的 JavaScript 交互式控件和 CSS 样式,它们与页面的其余部分隔离。这意味着当您使用 元素时,您不必担心它会影响您网站的样式或功能,也不必担心来自网站的任何 CSS 或 JavaScript 会破坏视频组件。

Web 组件的目标是使开发人员能够创建自己的自定义组件,类似于 <video> 元素,并具有所有隔离和可重用性优势。以下是 Web 组件规范的所有部分如何协同工作:

  • 您可以在 JavaScript 中创建自定义元素,如前所述。然后,您可以在 JavaScript 中定义自定义功能,并将属性从 HTML 传递到自定义元素中。

  • 您可以使用影子 DOM 使用 CSS 设置自定义元素的样式,该样式仅适用于元素而不是整个文档(解决 CSS 最困难的方面之一)。

  • 如果你不想直接用 JavaScript 编写所有的 HTML,你可以使用 HTML 模板,在 <template> 标记中将组件的 HTML 写入普通 HTML 文件中,在被 JavaScript 调用之前不会呈现。

  • 要组织代码,您可以使用 HTML 导入,您可以在其中将定义组件所需的所有 HTML、CSS 和 JavaScript 放入名为 navbar-component.html 的文件中,然后将其导入主 HTML 文件中,就像导入外部 CSS 文件一样:<link rel="import" href="navbar-component.html">.

当 Web 组件在 2011 年首次发布时,许多开发人员对这些可能性感到兴奋。虽然服务器端模板化方法有助于解决 HTML 的一些可维护性问题,但 Web 组件提供了完全不同的东西 - 扩展 HTML 的承诺将具有完整的可重用小部件。这是使 Web 平台成为可以开发复杂应用程序的地方所缺少的部分,而不是最初设计的简单静态内容站点。

那么发生了什么?在接下来的几年里,很明显,浏览器并没有就 Web 组件作为标准达成一致。截至 2018 年,几乎没有浏览器完全支持上述 Web 组件的四个主要功能,这是由于实现性能的潜在问题,标准冲突和不同的公司利益。这让开发人员处于一个有趣的位置 - 几乎每个人都同意组件样式方法是将 Web 从静态站点转移到复杂应用程序的必要部分,但等待浏览器支持似乎是徒劳的。怎么办?

在客户端上使用 JavaScript 框架

当 HTML Web 组件规范不会很快实现时,JavaScript 已经是一种足够强大的语言来弥补这一缺陷。开发人员一直在使用 jQuery 库(2006 年发布)制作复杂的应用程序,尽管很难为大规模应用程序组织代码。Backbone.js(2010 年发布)是最早的流行库之一,旨在为大型单页应用程序提供组织代码的框架,其次是 AngularJSEmber.js 等。

所有这些框架都与现有的 JavaScript 功能一起工作 - 它们不必依赖于等待浏览器来实现 Web 组件规范。但是没有一个框架使用真正的孤立和可重用的组件;如果没有 Web 组件规范的 4 个部分(自定义元素、影子 DOM、HTML 模板和 HTML 导入),这似乎是不可能的。

2013 年,一个名为 React 的框架发布,它对这种情况有一个有趣的看法。他们能够使用以下方法在没有 Web 组件规范的情况下制作一个真正的基于组件的框架:

  • React 没有使用 Web 组件的自定义元素规范,而是采用了在 JavaScript 中定义所有 HTML 的方法。从本质上讲,您将定义 JavaScript 函数以使用称为 JSX 的特殊语法输出所需的 HTML(它看起来像 HTML,但使用构建步骤转换为 JavaScript 函数)。
  • React 没有使用 Web 组件的 HTML 模板规范,而是没有提供在 JavaScript 之外编写 HTML 的方法。
  • React 没有使用 Web 组件的 HTML 导入规范,而是采用了将 JavaScript 导入 JavaScript 的方法。这在当时实际上并不直接可行,但像Browserifywebpack这样的工具允许开发人员在 他们的 JavaScript 中编写 requireimport 语句,这些语句将在构建时转换为单个 JavaScript 包。

从本质上讲,这里的见解是,您可以通过在 JavaScript 中执行所有操作来使组件工作。请注意,这里缺少 Web 组件规范的一部分,即影子 DOM——React 在首次发布时没有隔离样式的解决方案。尽管如此,它足以为今天使用组件构建应用程序提供一个框架。

这就是 JavaScript 使用 React 制作导航栏组件的样子:

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 className="nav-item active">
            <a className="nav-link" href="#">
              Home
            </a>
          </li>
          <li className="nav-item">
            <a className="nav-link" href="#">
              Info
            </a>
          </li>
          <li className="nav-item">
            <a className="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 实现有效地更新和渲染接口(不要与影子 DOM 混淆)。这是 Web 框架设计的一个重大转变,影响力大到每个主要框架都公开借用了 React 使用 虚拟 DOM 实现的声明式方法 — Ember、Angular、Vue.js,等等。截至 2018 年,Web 开发社区已在很大程度上接受这种范式作为构建现代 Web 应用程序的方式。

请注意,以清晰且可维护的方式编写 HTML 的愿望使我们进入了一个需要大量编程知识的地方;几乎不可能特别避免 JavaScript。从某种意义上说,这打破了 HTML 的承诺,HTML 被设计成一种不需要理解编程就可以有效使用的语言。未来可能会有开发人员可以在纯 HTML 中共享预先构建的 Web 组件,但未来可能需要相当长的时间才能到来。

本节仅简要概述了 React 和其他类似框架采用的前端方法。如果你想要更完整的解释和教程,关于如何使用各种 JavaScript 框架和方法构建一个工作的应用程序,请查看我的系列比较前端方法:看看 jQuery,Vue.js,React 和 Elm

结论

简而言之,这就是现代 HTML。我们介绍了使用适当的标签和 aria 属性编写语义和可访问的内容,使用 CSS 和 JavaScript 添加样式和动态功能,使用 HTML 属性和工具提高性能,最后使用模板和组件来提高可维护性。在此过程中,我们可以看到,要充分利用现代 HTML,几乎不可能避免使用构建过程以及某种形式的独立编程语言,对于大多数方法来说,这通常是 JavaScript。

从高层次来看它有时会令人沮丧——过去是一个简单易用的工作(用 HTML 制作网站)现在变得复杂且似乎难以接近(使用 JavaScript 前端框架制作 Web 应用程序,使用具有数千个潜在脆弱依赖项的构建过程)。然而,重要的是要注意,Web 开发作为一个行业只存在了大约 30 年 - 与其他行业(例如已经存在了许多世纪的建筑)相比,这只是历史的一小部分。就好像 Web 开发人员刚刚学会了如何用粘土建造房屋,现在被要求使用相同的工具来建造摩天大楼。我们的工具和流程不断发展是很自然的;我们只需要确保它以一种包容网络作为民主平台的最初愿景的方式发展。

现代 HTML 的使用肯定会令人沮丧,因为它继续快速变化和发展。然而,我们现在能够做的比以往任何时候都多,而且我们基本上都处于一个新行业的底层,有可能将其塑造成我们希望它成为的平台。作为一名开发人员,这是一个激动人心的时刻,我希望这些信息可以作为路线图,在您的旅程中为您提供帮助!

Dinosaur comic panel 5

特别感谢 @ryanqnorth恐龙漫画,自 2003 年以来(恐龙统治网络)以来,它提供了一些最好的荒诞幽默。

对这种学习方式感兴趣?请务必查看 Actualize 编码训练营(我是教学主任和首席讲师)。我们在芝加哥提供 面对面的课程 以及 实时在线课程,以帮助人们过渡到现代网络开发人员的新职业!

原文链接:https://peterxjang.com/blog/modern-html-explained-for-dinosaurs.html

Share this post:

Related content