Template: HTML 教程

HTML 语言是互联网开发的基础。

HTML 语言简介s

概述

HTML 是网页使用的语言,定义了网页的结构和内容。浏览器访问网站,其实就是从服务器下载 HTML 代码,然后渲染出网页。

HTML 的全名是“超文本标记语言”(HyperText Markup Language),上个世纪90年代由欧洲核子研究中心的物理学家蒂姆·伯纳斯-李(Tim Berners-Lee)发明。它的最大特点就是支持超链接,点击链接就可以跳转到其他网页,从而构成了整个互联网。

1999年,HTML 4.01 版发布,成为广泛接受的 HTML 标准。2014年,HTML 5 发布,这是目前正在使用的版本。

浏览器的网页开发,涉及三种技术:HTML、CSS 和 JavaScript。HTML 语言定义网页的结构和内容,CSS 样式表定义网页的样式,JavaScript 语言定义网页与用户的互动行为。HTML 语言是网页开发的基础,CSS 和 JavaScript 都是基于 HTML 才能生效,即使没有这两者,HTML 本身也能使用,可以完成基本的内容展示。

下面就是一个简单网页的 HTML 源码。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>网页标题</title>
</head>
<body>
  <p>Hello World</p>
</body>
</html>

上面这段代码,可以保存成文件hello.html。浏览器打开这个本地文件,就能看到文字“Hello World”。

浏览器右键菜单的“查看源码”(View page source),可以展示当前网页的 HTML 源码。

网页的基本概念

标签

网页的 HTML 代码由许许多多不同的标签(tag)构成。学习 HTML 语言,就是学习各种标签的用法。

下面就是标签的一个例子。

<title>网页标题</title>

上面代码中,<title></title>就是一对标签。

标签用来告诉浏览器,如何处理这段代码。标签的内容就是浏览器所要渲染的、展示在网页上的内容。

标签放在一对尖括号里面(比如<title>),大多数标签都是成对出现的,分成开始标签和结束标签,结束标签在标签名之前加斜杠(比如</title>)。但是,也有一些标签不是成对使用,而是只有开始标签,没有结束标签,比如上一节示例的<meta>标签。成对出现叫双边标签;单个出现的叫单边标签。

<meta charset="utf-8">

上面代码中,<meta>标签就没有结束标签</meta>

这种单独使用的标签,通常是因为标签本身就足够完成功能了,不需要标签之间的内容。实际应用中,它们主要用来提示浏览器,做一些特别处理。

标签可以嵌套。

<div><p>hello world</p></div>

上面代码中,<div>标签内部包含了一个<p>标签。

嵌套时,必须保证正确的闭合顺序,不能跨层嵌套,否则会出现意想不到的渲染结果。

<div><p>hello world</div></p>

上面代码就是错误的嵌套,闭合顺序不正确。

HTML 标签名是大小写不敏感,比如<title><TITLE>是同一个标签。不过,一般习惯都是使用小写。

另外,HTML 语言忽略缩进和换行。下面几种写法的渲染结果是一样的。

<title>网页标题</title>

<title>
  网页标题
</title>

<title>网页
标题</title>

进一步说,整个网页的 HTML 代码完全可以写成一行,浏览器照样解析,结果完全一样。所以,正式发布网页之前,开发者有时会把源码压缩成一行,以减少传输的字节数。

各种网页的样式效果,比如内容的缩进和换行,主要靠 CSS 来实现。

元素

浏览器渲染网页时,会把 HTML 源码解析成一个标签树,每个标签都是树的一个节点(node)。这种节点就称为网页元素(element)。所以,“标签”和“元素”基本上是同义词,只是使用的场合不一样:标签是从源码角度来看,元素是从编程角度来看,比如<p>标签对应网页的p元素。

嵌套的标签就构成了网页元素的层级关系。

<div><p>hello world</p></div>

上面代码中,div元素内部包含了一个p元素。上层元素又称为“父元素”,下层元素又称为“子元素”,即divp的父元素,pdiv的子元素。

块级元素,行内元素

所有元素可以分成两大类:块级元素(block)和行内元素(inline)。

块级元素默认占据一个独立的区域,在网页上会自动另起一行,占据 100% 的宽度。

<p>hello</p>
<p>world</p>

上面代码中,p元素是块级元素,因此浏览器会将内容分成两行显示。

行内元素默认与其他元素在同一行,不产生换行。比如,span就是行内元素,通常用来为某些文字指定特别的样式。

<span>hello</span>
<span>world</span>

上面代码中,span元素是行内元素,因此浏览器会将两行内容放在一行显示。

属性

属性(attribute)是标签的额外信息,使用空格与标签名和其他属性分隔。

<img src="demo.jpg" width="500">

上面代码中,<img>标签有两个属性:srcwidth

属性可以用等号指定属性值,比如上例的demo.jpg就是src的属性值。属性值一般放在双引号里面,这不是必需的,但推荐总是使用双引号。

注意,属性名是大小写不敏感的,onclickonClick是同一个属性。

HTML 提供大量属性,用来定制标签的行为。

网页的基本标签

符合 HTML 语法标准的网页,应该满足下面的基本结构。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title></title>
</head>
<body>
</body>
</html>

不管多么复杂的网页,都是从上面这个基本结构衍生出来的。

前面说过,HTML 代码的缩进和换行,对于浏览器不产生作用。所以,上面的代码完全可以写成一行,渲染结果不变。上面这样分行写,只是为了提高可读性。

下面就依次介绍,这个基本结构的主要标签。它们构成了网页的骨架。

<!doctype>

网页的第一个标签通常是<!doctype>,即文档声明头(DocType Declaration),简称DTD。表示文档类型,告诉浏览器如何解析网页。

一般来说,只要像下面这样,简单声明doctypehtml即可。浏览器就会按照 HTML 5 的规则处理网页。

<!doctype html>

有时,该标签采用完全大写的形式,以便区别于正常的 HTML 标签。因为<!doctype>本质上不是标签,更像一个处理指令。

<!DOCTYPE html>

如果在 HTML 文档的开始部分没有使用 doctype ,浏览器在渲染文档时,切换到我们称为“怪异模式(兼容模式)”的渲染模式。

<html>

<html>标签是网页的顶层容器,即标签树结构的顶层节点,也称为根元素(root element),其他元素都是它的子元素。一个网页只能有一个<html>标签。

该标签的lang属性,表示网页内容默认的语言。

<html lang="zh-CN">

上面代码表示,网页是中文内容。如果是英文内容,zh-CN要改成en

<head>标签是一个容器标签,用于放置网页的元信息。它的内容不会出现在网页上,而是为网页渲染提供额外信息。

<!doctype html>
<html>
  <head>
    <title>网页标题</title>
  </head>
</html>

<head><html>的第一个子元素。如果网页不包含<head>,浏览器会自动创建一个。

<head>的子元素一般有下面七个。

  • <meta>:设置网页的元数据。
  • <link>:连接外部样式表。
  • <title>:设置网页标题。
  • <style>:放置内嵌的样式表。
  • <script>:引入脚本。
  • <noscript>:浏览器不支持脚本时,所要显示的内容。
  • <base>:设置网页内部相对 URL 的计算基准。

<meta>

<meta>标签用于设置或说明网页的元数据,必须放在<head>里面。一个<meta>标签就是一项元数据,网页可以有多个<meta><meta>标签约定放在<head>内容的最前面。

不管什么样的网页,一般都可以放置以下两个<meta>标签。

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Page Title</title>
</head>

上面例子中,第一个<meta>标签表示网页采用 UTF-8 格式编码,第二个<meta>标签表示网页在手机端可以自动缩放。

<meta>标签有五个属性,下面依次介绍。

(1)charset 属性

<meta>标签的charset属性,用来指定网页的编码方式。该属性非常重要,如果设置得不正确,浏览器可能无法正确解码,就会显示乱码。

<meta charset="utf-8">

上面代码声明,网页为 UTF-8 编码。虽然开发者可以使用其他的编码方式,但正确的做法几乎总是应该采用 UTF-8。

注意,这里声明的编码方式,应该与网页实际的编码方式一致,即声明了utf-8,网页就应该使用 UTF-8 编码保存。如果这里声明了utf-8,实际却是使用另一种编码(比如 GB2312),并不会导致浏览器的自动转码,网页可能会显示为乱码。

有如下字符集:

  • ASCII码:用1个字节来表示一个字符。共可以表示28=256个字符。
  • ANSI编码:每个国家为了显示本国的语言,都对ASCII码进行了扩展。用2个字节来表示一个汉字。
    • 中国的ANSI编码是GB2312编码(简体),对6763汉字进行编码,含600多特殊字符。
    • 台湾的ANSI编码是BIG5编码(繁体)
  • GBK(简体):对GB2312进行了扩展,用来显示罕见的、古汉语的汉字。
  • Unicode编码(统一编码):用4个字节来表示一个字符。
  • UTF-8(Unicode Transform Format)编码:根据字符的不同,选择其编码的长度。一个字符A用1个字节表示,一个汉字用2个字节表示。

(2)name 属性,content 属性

<meta>标签的name属性表示元数据的名字,content属性表示元数据的值。它们合在一起使用,就可以为网页指定一项元数据。

<head>
  <meta name="description" content="HTML 语言入门">
  <meta name="keywords" content="HTML,教程">
  <meta name="author" content="张三">
</head>

上面代码包含了三个元数据:description是网页内容的描述,keywords是网页内容的关键字,author是网页作者。

元数据有很多种,大部分涉及浏览器内部工作机制,或者特定的使用场景。下面是一些例子。

<!--viewport代表的是浏览器中网站可见内容的部分。视口外的内容在被滚动进来前都是不可见的-->
<meta name="viewport" content="width=device-width, initial-scale=1">

<meta name="application-name" content="Application Name">
<meta name="generator" content="program">
<meta name="subject" content="your document's subject">
<meta name="referrer" content="no-referrer">

(3)http-equiv 属性,content 属性

<meta>标签的http-equiv属性用来覆盖 HTTP 回应的头信息字段,content属性是对应的字段内容。这两个属性与 HTTP 协议相关。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

上面代码可以覆盖 HTTP 回应的Content-Security-Policy字段。

下面是另一些例子。

<meta http-equiv="Content-Type" content="Type=text/html; charset=utf-8">
<meta http-equiv="refresh" content="30">
<meta http-equiv="refresh" content="30;URL='http://website.com'">

上面代码 http-equiv="refresh" 的 content 只包含一个正整数,则为重新载入页面的时间间隔(秒);如果 content 包含一个正整数,并且后面跟着字符串 ‘;url=’ 和一个合法的 URL,则是重定向到指定链接的时间间隔(秒)。

<title>

<title>标签用于指定网页的标题,会显示在浏览器窗口的标题栏。

<head>
  <title>网页标题</title>
</head>

搜索引擎根据这个标签,显示每个网页的标题。它对于网页在搜索引擎的排序,有很大的影响,应该精心安排,反映网页的主题。

<title>标签的内部,不能再放置其他标签,只能放置无格式的纯文本。

<body>

<body>标签是一个容器标签,用于放置网页的主体内容。浏览器显示的页面内容,都放置在它的内部。它是<html>的第二个子元素,紧跟在<head>后面。每一个 HTML 页面都含有 body 元素,不论写不写。

<html>
  <head>
    <title>网页标题</title>
  </head>
  <body>
    <p>hello world</p>
  </body>
</html>

空格和换行

HTML 语言有自己的空格处理规则。标签内容的头部和尾部的空格,一律忽略不计。

<p>  hello world   </p>

上面代码中,hello前面的空格和world后面的空格,浏览器一律忽略不计。

标签内容里面的多个连续空格(包含制表符\t),会被浏览器合并成一个。

<p>hello      world</p>

上面代码中,helloworld之间有多个连续空格,浏览器会将它们合并成一个。网页渲染的结果是,helloworld之间只有一个空格。

浏览器还会将文本里面的换行符(\n)和回车符(\r),替换成空格。

<p>hello



world
</p>

上面代码中,helloworld之间有多个换行,浏览器会将它们替换成空格,然后再将多个空格合并成一个。网页渲染的结果是,helloworld之间有一个空格。

这意味着,HTML 源码里面的换行,不会产生换行效果。

注释

HTML 代码可以包含注释,浏览器会自动忽略注释。注释以<!--开头,以-->结尾,下面就是一个注释的例子。

<!-- 这是一个注释 -->

注释可以是多行的,并且内部的 HTML 都不再生效了。

<!--
  <p>hello world</p>
-->

上面代码是一个注释的区块,内部的代码都是无效的,浏览器不会解析,更不会渲染它们。

注释有助于理解代码的含义,复杂的代码块前面最好加上注释。

URL 简介

概述

URL 是“统一资源定位符”(Uniform Resource Locator)的首字母缩写,中文译为“网址”,表示各种资源的互联网地址。下面就是一个典型的 URL。

https://www.example.com/path/index.html

所谓资源,可以简单理解成各种可以通过互联网访问的文件,比如网页、图像、音频、视频、JavaScript 脚本等等。只有知道了它们的 URL,才能在互联网上获取它们。

只要资源可以通过互联网访问,它就必然有对应的 URL。一个 URL 对应一个资源,但是同一个资源可能对应多个 URL。

URL 是互联网的基础。互联网之所以“互联”,就是因为网页可以通过“链接”(link),包含其他 URL。用户只要点击,就可以从一个 URL 跳转到另一个 URL,前往不同的网站。

网址的组成部分

URL 由多个部分组成。下面是一个比较复杂的 URL,实际的 URL 通常不会有这么多部分。

https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor

我们看看,这个 URL 的各个部分。

协议

协议(scheme)是浏览器请求服务器资源的方法,上例是https://的部分,表示使用 HTTPS 协议。

互联网支持多种协议,必须指明网址使用哪一种协议,默认是 HTTP 协议(2021年默认 HTTPS协议)。也就是说,如果省略协议,直接在浏览器地址栏输入www.example.com,那么浏览器默认会访问http://www.example.com。HTTPS 是 HTTP 的加密版本,出于安全考虑,越来越多的网站使用这个协议。

HTTP 和 HTTPS 的协议名称后面,紧跟着一个冒号和两个斜杠(://)。其他协议不一定如此,邮件地址协议mailto:的协议名后面只有一个冒号,比如mailto:foo@example.com

主机

主机(host)是资源所在的网站名或服务器的名字,又称为域名。上例的主机是www.example.com

有些主机没有域名,只有 IP 地址,比如192.168.2.15。这种情况常常出现在局域网。

端口

同一个域名下面可能同时包含多个网站,它们之间通过端口(port)区分。“端口”就是一个整数,可以简单理解成,访问者告诉服务器,想要访问哪一个网站。默认端口是80,如果省略了这个参数,服务器就会返回80端口的网站。

端口紧跟在域名后面,两者之间使用冒号分隔,比如www.example.com:80

路径

路径(path)是资源在网站的位置。比如,/path/index.html这个路径,指向网站的/path子目录下面的网页文件index.html

互联网的早期,路径是真实存在的物理位置。现在由于服务器可以模拟这些位置,所以路径只是虚拟位置。

路径可能只包含目录,不包含文件名,比如/foo/,甚至结尾的斜杠都可以省略。这时,服务器通常会默认跳转到该目录里面的index.html文件(即等同于请求/foo/index.html),但也可能有其他的处理(比如列出目录里面的所有文件),这取决于服务器的设置。一般来说,访问www.example.com这个网址,很可能返回的是网页文件www.example.com/index.html

查询参数

查询参数(parameter)是提供给服务器的额外信息。参数的位置是在路径后面,两者之间使用?分隔,上例是?key1=value1&key2=value2

查询参数可以有一组或多组。每组参数都是键值对(key-value pair)的形式,同时具有键名(key)和键值(value),它们之间使用等号(=)连接。比如,key1=value就是一个键值对,key1是键名,value1是键值。

多组参数之间使用&连接,比如key1=value1&key2=value2

锚点

锚点(anchor)是网页内部的定位点,使用#加上锚点名称,放在网址的最后,比如#anchor。浏览器加载页面以后,会自动滚动到锚点所在的位置。

锚点名称通过网页元素的id属性命名。

URL 字符

URL 的各个组成部分,只能使用以下这些字符。

  • 26个英语字母(包括大写和小写)
  • 10个阿拉伯数字
  • 连词号(-
  • 句点(.
  • 下划线(_

此外,还有18个字符属于 URL 的保留字符,只能在给定的位置出现。比如,查询参数的开头是问号(?),也就是说,问号只能出现查询参数的开头,出现在其他位置就是非法的,会导致网址解析错误。网址的其他部分如果要使用这些保留字符,必须使用它们的转义形式。

URL 字符转义的方法是,在这些字符的十六进制 ASCII 码前面加上百分号(%)。下面是这18个字符及其转义形式。

  • !:%21
  • #:%23
  • $:%24
  • &:%26
  • ':%27
  • (:%28
  • ):%29
  • *:%2A
  • +:%2B
  • ,:%2C
  • /:%2F
  • ::%3A
  • ;:%3B
  • =:%3D
  • ?:%3F
  • @:%40
  • [:%5B
  • ]:%5D

举例来说,有一个网页的 URL 是foo?bar.html,即文件里面包含一个问号,那么需要写成foo%3Fbar.html

URL 的合法字符,其实也可以采用这种转义方法,但是不建议使用。比如,字母a的十六进制 ASCII 码是61,转义形式后就是%61。因此,www.apple.com又可以写成www.%61pple.com,浏览器一样识别。

值得注意的是,空格的转义形式是%20。对于那些包含空格的文件名,这个转义是必须的。

既不属于合法字符、也不属于保留字符的其他字符(比如汉字),理论上不需要手动转义,可以直接写在 URL 里面,比如www.example.com/中国.html,浏览器会自动将它们转义,发给服务器。转义方法是使用这些字符的十六进制 UTF-8 编码,每两位算作一组,然后每组头部添加百分号(%)。

举例来说,汉字的 UTF-8 十六进制编码是e4b8ad,每两个字符一组,URL 转义后就为%e4%b8%ad。也就是说,URL 里面凡是有汉字的地方,都要写成%e4%b8%ad。因此,访问www.example.com/中国.html这个网址,需要写成下面的样子。

www.example.com/%e4%b8%ad%e5%9b%bd.html

上面代码中,的转义形式是%e4%b8%ad%e5%9b%bd

绝对 URL 和相对 URL

URL 分成两种:绝对 URL 和相对 URL。

绝对 URL 指的是,只靠 URL 本身就能确定资源的位置。这意味着,URL 必须带有资源的完整信息,包含协议、主机、路径等部分。前面的例子都是绝对 URL。

相对 URL 指的是,URL 不包含资源位置的全部信息,必须结合当前网页的位置,才能定位资源。比如,当前网页的 URL 是https://www.example.com/path/index.html,该网页上面有一个资源,URL 指向a.html,这个就是相对 URL。因为只知道a.html,并不能定位资源。浏览器假定,a.html与当前网址在同一个子目录下面,从而得到绝对 URL https://www.example.com/path/a.html

相对 URL 如果以斜杠(/)开头,就表示网站的根目录。否则,必须以当前目录为起点,推算资源的位置。比如,相对 URL /foo/bar.html表示网站根目录的子目录foofoo/bar.html表示在当前目录的foo子目录。

URL 还可以使用两个特殊简写,表示特定位置。

  • .:表示当前目录,比如./a.html(当前目录下的a.html文件)
  • ..:表示上级目录,比如../a.html(上级目录下的a.html文件)

这两种简写可以多个连用,比如../../表示上两级目录。

绝对 URL 也可以使用这两个简写,比如www.example.com/./index.html等同于www.example.com/index.html,这时.相当于根目录的当前目录,即根目录本身。

<base>

<base>标签指定网页内部的所有相对 URL 的计算基准。整张网页只能有一个<base>标签,而且只能放在<head>里面。它是单独使用的标签,没有闭合标签,下面是一个例子。

<head>
<base href="https://www.example.com/files/" target="_blank">
</head>

<base>标签的href属性给出计算的基准网址,target属性给出如何打开链接的说明。已知计算基准是https://www.example.com/files/,那么相对 URL foo.html,就可以转成绝对 URL https://www.example.com/files/foo.html

注意,<base>标签必须至少具有href属性或target属性之一。

<base href="http://foo.com/app/">
<base target="_blank">

一旦设置了<base>,就对整个网页都有效。如果要改变某个链接的行为,只能用绝对链接替代相对链接。尤其需要注意锚点,这时锚点也是针对<base>计算的,而不是针对当前网页的 URL。

网页元素的属性

简介

网页元素的属性(attribute)可以定制元素的行为,不同的属性会导致元素有不同的行为。元素属性的写法是 HTML 标签内部的“键值对”。

<html lang="en">

上面代码中,<html>标签内部的键值对lang="en",就称为html元素的属性。属性名为lang,属性值为en

属性名与标签名一样,不区分大小写,langLANG是同一个属性。

属性名与属性值之间,通过等号=连接。属性值可以放在单引号或双引号之中,建议统一使用双引号。某些属性值可以不使用引号,但是建议不要这样写。

有些属性是布尔属性,即属性值是一个布尔值,只有“打开”和“关闭”两种情况。这时属性值可以省略,只要添加了属性名,就表示打开该属性。

<input type="text" required>

上面代码中,required就是<input>标签的布尔属性。如果加上这个属性,就表示打开,没有就是关闭。

全局属性

全局属性(global attributes)是所有元素都可以使用的属性。也就是说,你可以把下面的属性,加在任意一个网页元素上面,不过有些属性对某些元素可能不产生意义。

下面是一些常见的全局属性。

id

id属性是元素在网页内的唯一标识符。比如,网页可能包含多个<p>标签,id属性可以指定每个<p>标签的唯一标识符。

<p id="p1"></p>
<p id="p2"></p>
<p id="p3"></p>

上面代码中,三个<p>标签具有不同的id属性,因此可以区分。

id属性的值必须是全局唯一的,同一个页面不能有两个相同的id属性。另外,id属性的值不得包含空格。

id属性的值还可以在最前面加上#,放到 URL 中作为锚点,定位到该元素在网页内部的位置。比如,用户访问网址https://foo.com/index.html#bar的时候,浏览器会自动将页面滚动到bar的位置,让用户第一眼就看到这部分内容。

class

class属性用来对网页元素进行分类。如果不同元素的class属性值相同,就表示它们是一类的。

<p class="para"></p>
<p></p>
<p class="para"></p>

上面代码中,第一个<p>和第三个<p>是一类,因为它们的class属性相同。

元素可以同时具有多个 class,它们之间使用空格分隔。

<p class="p1 p2 p3"></p>

上面的p元素同时具有p1p2p3三个 class。

title

title属性用来为元素添加附加说明。大多数浏览器中,鼠标悬浮在元素上面时,会将title属性值作为浮动提示,显示出来。

<div title="版权说明">
  <p>本站内容使用创意共享许可证,可以自由使用。</p>
</div>

上面代码中,title属性解释了这一块内容的目的。鼠标悬停在上面时,浏览器会显示一个浮动提示。一旦鼠标移开,提示就会消失。

tabindex

网页通常使用鼠标操作,但是某些情况下,用户可能希望使用键盘,或者只有键盘可以用。因此,浏览器允许使用 Tab 键,遍历网页元素。也就是说,只要不停按下 Tab 键,网页的焦点就会从一个元素转移到另一个元素,选定焦点元素以后,就可以进行下一步操作,比如按下回车键访问某个链接,或者直接在某个输入框输入文字。

这里就有一个问题,按下 Tab 键的时候,浏览器怎么知道跳到哪一个元素。HTML 提供了tabindex属性,解决这个问题。它的名字的含义,就是 Tab 的顺序(index)。

tabindex属性的值是一个整数,表示用户按下 Tab 键的时候,网页焦点转移的顺序。不同的属性值有不同的含义。

  • 负整数:该元素可以获得焦点(比如使用 JavaScript 的focus()方法),但不参与 Tab 键对网页元素的遍历。这个值通常是-1
  • 0:该元素参与 Tab 键的遍历,顺序由浏览器指定,通常是按照其在网页里面出现的位置。
  • 正整数:网页元素按照从小到大的顺序(1、2、3、……),参与 Tab 键的遍历。如果多个元素的tabindex属性相同,则按照在网页源码里面出现的顺序遍历。
<p tabindex="0">这段文字可以获得焦点。</p>

上面代码中,<p>标签的tabindex0,意味着该元素可以获得焦点,并且也可以被 Tab 键遍历,顺序由其在源码里面的位置决定。

一般来说,tabindex属性最好都设成0,按照自然顺序进行遍历,这样比较符合用户的预期,除非网页有特殊布局。如果网页所有元素都没有设置tabindex,那么只有那些默认可以遍历的元素(比如链接、输入框等)才能参与 Tab 键的遍历,顺序由其在源码的位置决定。因此实际上,只有那些无法获得焦点的元素(比如<span><div>)需要参与遍历,才有必要设置tabindex属性。

accesskey

accesskey属性指定网页元素获得焦点的快捷键,该属性的值必须是单个的可打印字符。只要按下快捷键,该元素就会得到焦点。

<button accesskey="s">提交</button>

上面代码中,<button>的快捷键是s,按下快捷键,该元素就得到了焦点。

accesskey属性的字符键,必须配合功能键,一起按下才会生效。也就是说,快捷键是“功能键 + 字符键”的组合。不同的浏览器与不同的操作系统,功能键都不一样。比如,Chrome 浏览器在 Windows 系统和 Linux 系统的快捷键是Alt + 字符键,在 Mac 系统的快捷键是Ctrl + Alt + 字符键

注意,accesskey如果跟操作系统或浏览器级别的快捷键有冲突,这时不会生效。

style

style属性用来指定当前元素的 CSS 样式。

<p style="color: red;">hello</p>

上面代码指定文字颜色为红色。

hidden

hidden是一个布尔属性,表示当前的网页元素不再跟页面相关,因此浏览器不会渲染这个元素,所以就不会在网页中看到它。

<p hidden>本句不会显示在页面上。</p>

上面代码中,这个p元素不会出现在网页上。

注意,CSS 的可见性设置,高于hidden属性。如果 CSS 设为该元素可见,hidden属性将无效。

lang,dir

lang属性指定网页元素使用的语言。

<p lang="en">hello</p>
<p lang="zh">你好</p>

上面代码中,第一个<p>lang属性,表示使用英语,第二个<p>lang属性,表示使用中文。

lang属性的值,必须符合 BCP47 的标准。下面是一些常见的语言代码。

  • zh:中文
  • zh-Hans:简体中文
  • zh-Hant:繁体中文
  • en:英语
  • en-US:美国英语
  • en-GB:英国英语
  • es:西班牙语
  • fr:法语

dir属性表示文字的阅读方向,有三个可能的值。

  • ltr:从左到右阅读,比如英语。
  • rtl:从右到左阅读,阿拉伯语、波斯语、希伯来语都属于这一类。
  • auto:浏览器根据内容的解析结果,自行决定。

contenteditable

HTML 网页的内容默认是用户不能编辑,contenteditable属性允许用户修改内容。它有两个可能的值。

  • true或空字符串:内容可以编辑
  • false:不可以编辑
<p contenteditable="true">
鼠标点击,本句内容可修改。
</p>

上面代码中,鼠标单击句子,就可以进入编辑状态,用户可以改变句子的内容。当然,除非提交到服务器,否则刷新页面还是显示原来的内容。

该属性是枚举属性,不是布尔属性,规范的写法是最好带上属性值。

spellcheck

浏览器一般会自带拼写检查功能,编辑内容时,拼错的单词下面会显示红色的波浪线。spellcheck属性就表示,是否打开拼写检查。

它有两个可能的值。

  • true:打开拼写检查
  • false:关闭拼写检查
<p contenteditable="true" spellcheck="true">
英语单词 separate 容易写错成 seperate。
</p>

上面代码中,seperate下面会有提示,表示拼错了。

注意,由于该属性只在编辑时生效,所以这个例子必须加上contenteditable属性,表示本段内容可编辑。鼠标单击就可以进入编辑状态,这时才会看到拼写提示。不可编辑的状态下,拼写错误是不提示显示的。对于那些不可编辑的元素,该属性无效。

这个属性看上去像布尔属性,但是其实是枚举属性,所以最好不要省略它的值。如果没有指定这个属性,浏览器将自行决定是否打开拼写检查。

data-属性

data-属性用于放置自定义数据。如果没有其他属性或元素合适放置数据,就可以放在data-属性。

<a href="#" class="tooltip" data-tip="this is the tip!">链接</a>

上面代码中,data-tip用于放置链接的提示文字。

data-属性只能通过 CSS 或 JavaScript 利用。下面是 CSS 的例子。

/* HTML 代码如下
<div data-role="mobile">
Mobile only content
</div>
*/
div[data-role="mobile"] {
  display:none;
}

/* HTML 代码如下
<div class="test" data-content="This is the div content">test</div>
*/
.test {
  display: inline-block;
}
.test:after {
  content: attr(data-content);
}

事件处理属性

除了上面这些属性,全局属性还包括事件处理属性(event handler),用来响应用户的动作。这里列出这些属性的名单。

onabort, onautocomplete, onautocompleteerror, onblur, oncancel, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextmenu, oncuechange, ondblclick, ondrag, ondragend, ondragenter, ondragexit, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onmousewheel, onpause, onplay, onplaying, onprogress, onratechange, onreset, onresize, onscroll, onseeked, onseeking, onselect, onshow, onsort, onstalled, onsubmit, onsuspend, ontimeupdate, ontoggle, onvolumechange, onwaiting

HTML 字符编码

简介

网页包含了大量的文字,浏览器必须知道这些文字的编码方法,才能把文字还原出来。

一般情况下,服务器向浏览器发送 HTML 网页文件时,会通过 HTTP 头信息,声明网页的编码方式。

Content-Type: text/html; charset=UTF-8

上面代码中,HTTP 头信息的Content-Type字段先声明,服务器发送的数据类型是text/html(即 HTML 网页),然后声明网页的文字编码是UTF-8

网页内部也会再用<meta>标签,再次声明网页的编码。

<meta charset="UTF-8">

字符的数字表示法

网页可以使用不同语言的编码方式,但是最常用的编码是 UTF-8。UTF-8 编码是 Unicode 字符集的一种表达方式。这个字符集的设计目标是包含世界上的所有字符,目前已经收入了十多万个字符。

每个字符有一个 Unicode 号码,称为码点(code point)。如果知道码点,就能查到这是什么字符。举例来说,英文字母a的码点是十进制的97(十六进制的61),汉字“中”的码点是十进制的20013(十六进制的4e2d)。

由于下面的原因,不是每一个 Unicode 字符都能直接在 HTML 语言里面显示。

(1)不是每个 Unicode 字符都可以打印出来,有些没有可打印形式,比如换行符的码点是十进制的10(十六进制的A),就没有对应的字面形式。

(2)小于号(<)和大于号(>)用来定义 HTML 标签,其他需要用到这两个符号的场合,必须防止它们被解释成标签。

(3)由于 Unicode 字符太多,无法找到一种输入法,可以直接输入所有这些字符。换言之,没有一种键盘,有办法输入所有符号。

(4)网页不允许混合使用多种编码,如果使用 UTF-8 编码的同时,又想插入其他编码的字符,就会很困难。

HTML 为了解决上面这些问题,允许使用 Unicode 码点表示字符,浏览器会自动将码点转成对应的字符。

字符的码点表示法是&#N;(十进制,N代表码点)或者&#xN;(十六进制,N代表码点),比如,字符a可以写成&#97;(十进制)或者&#x61;(十六进制),字符可以写成&#20013;(十进制)或者&#x4e2d;(十六进制),浏览器会自动转换它们。

<p>hello</p>
<!-- 等同于 -->
<p>&#104;&#101;&#108;&#108;&#111;</p>
<!-- 等同于 -->
<p>&#x68;&#x65;&#x6c;&#x6c;&#x6f;</p>

上面代码中,字符可以直接表示,也可以使用十进制码点或十六进制码点表示。

注意,HTML 标签本身不能使用码点表示,否则浏览器会认为这是所要显示的文本内容,而不是标签。比如,<p>一旦写成<&#112;>或者&#60;&#112;&#62;,浏览器就不再认为这是标签了,而会当作文本内容将其显示为<p>

字符的实体表示法

数字表示法的不方便之处,在于必须知道每个字符的码点,很难记忆。为了能够快速输入,HTML 为一些特殊字符,规定了容易记忆的名字,允许通过名字来表示它们,这称为实体表示法(entity)。

实体的写法是&name;,其中的name是字符的名字。下面是其中一些特殊字符,及其对应的实体。

  • <&lt;
  • >&gt;
  • "&quot;
  • '&apos;
  • &&amp;
  • ©&copy;
  • ®&reg;
  • #&num;
  • §&sect;
  • ¥&yen;
  • $&dollar;
  • £&pound;
  • ¢&cent;
  • %&percnt;
  • *$ast;
  • @&commat;
  • ^&Hat;
  • ±&plusmn;
  • ×&times;
  • ÷&divide;
  • ²&sup2;
  • ³&sup3;
  • °&deg;
  • 空格:&nbsp;

注意,上面最后一个特殊字符是空格,它也有对应的实体表示法。

字符的数字表示法和实体表示法,都可以表示正常情况无法输入的字符,逃脱了浏览器的限制,所以英语里面称为“escape”,中文翻译为“字符的转义”。

网页的语义结构

HTML 标签的名称都带有语义(semantic),使用时应该尽量符合标签的语义,不要用错误语义的标签。语义良好的网页,天然具有良好的结构,对于开发者易读易写,容易维护,也能帮助计算机更好地处理网页内容。

含义

HTML 标签的一个重要作用,就是声明网页元素的性质,使得用户只看标签,就能了解这个元素的意义,阅读 HTML 源码就能了解网页的大致结构。这被称为 HTML 的语义原则。

下面就是一个典型的语义结构的网页。

<body>
  <header>页眉</header>
  <main>
    <article>
      <h1>文章标题</h1>
      <p>文章内容</p>
    </article>
  </main>
  <footer>页尾</footer>
</body>

只看上面的代码,就可以知道,页面分成页眉(<header>)、主体(<main>)、页尾(<footer>)三个部分。

编写 HTML 网页,第一步就是写出语义结构的网页骨架。

常用标签

<header>标签可以用在多个场景,既可以表示整个网页的头部,也可以表示一篇文章或者一个区块的头部。

如果用在网页的头部,就称为“页眉”。网站导航和搜索栏通常会放在<header>里面。

<header>
  <h1>公司名称</h1>
  <ul>
    <li><a href="/home">首页</a></li>
    <li><a href="/about">关于</a></li>
    <li><a href="/contact">联系</a></li>
  </ul>
  <form target="/search">
    <input name="q" type="search" />
    <input type="submit" />
  </form>
</header>

如果<header>用在文章的头部,则可以把文章标题、作者等信息放进去。

<article>
  <header>
    <h2>文章标题</h2>
    <p>张三,发表于2010年1月1日</p>
  </header>
</article>

由于<header>可以用在多种场景,所以一个页面可能包含多个<header>,但是一个具体的场景里面只能包含一个,比如网页的页眉只能有一个。另外,<header>里面不能包含另一个<header><footer>

header 的语义化特性也可以让辅助工具快速定位到它的内容。

<footer>标签表示网页、文章或章节的尾部。如果用于整张网页的尾部,就称为“页尾”,通常包含版权信息或者其他相关信息。

<body>
  <footer>
    <p>© 2018 xxx 公司</p>
  </footer>
</body>

上面代码中,版权信息放在<footer>里面。

<footer>也可以放在文章里面。

<article>
  <header>
    <h1>文章标题</h1>
  </header>
  <footer>
    <p>© 禁止转贴</p>
  </footer>
</article>

<footer>不能嵌套,即内部不能放置另一个<footer>,也不能放置<header>

footer 元素也具有语义化的特性,可以让辅助工具快速定位到它。

<main>

<main>标签表示页面的主体内容,一个页面只能有一个<main>。是为了围绕与页面中心主题相关的信息, 而不应包含如导航连接、网页横幅等需要在多个页面中重复出现的内容。

<body>
<header>页眉</header>
<main>
  <article>文章</article>
</main>
<aside>侧边栏</aside>
<footer>页尾</footer>
</body>

上面代码就是最典型的页面结构。

注意,<main>是顶层标签,不能放置在<header><footer><article><aside><nav>等标签之中。

另外,功能性区块(比如搜索栏)不适合放入<main>,除非当前页面就是搜索页面。

默认情况下,浏览器呈现 mainheaderfooternavarticlesection 等这些元素的方式类似于普通的 div。 但是,在适当的地方使用它们会让标记文本具有更多的含义。 仅标签名称就可以表示它所包含的信息类型,这给内容增加了语义含义。 辅助技术可以获取这种信息,为用户提供更好的页面摘要或导航选项。

main 标签有一个内嵌的特性,以便辅助技术快速定位到页面的主体。 如果你在页面顶部看到过“跳转到主要内容”链接,那么使用 main 标签会自动让辅助设备具有这个跳转的功能。

<article>

<article>标签表示页面里面一段完整的内容,即使页面的其他部分不存在,也具有独立使用的意义,通常用来表示一篇文章或者一个论坛帖子。它可以有自己的标题(<h1><h6>)。

<article>
  <h2>文章标题</h2>
  <p>文章内容</p>
</article>

一个网页可以包含一个或多个<article>,比如包含多篇文章。

<aside>

<aside>标签用来放置与网页或文章主要内容间接相关的部分。网页级别的<aside>,可以用来放置侧边栏,但不一定就在页面的侧边;文章级别的<aside>,可以用来放置评论或注释。

下面是网页级别的<aside>的例子。

<body>
  <main>主体内容</main>
  <aside>侧边栏</aside>
</body>

下面是文章评注的例子。

<p>第一段</p>

<aside>
  <p>本段是文章的重点。</p>
</aside>

<section>

<section>标签表示一个含有主题的独立部分,用于对与主题相关的内容进行分组,通常用在文档里面表示一个章节,比如<article>可以包含多个<section><section>总是多个一起使用,一个页面不能只有一个<section>

<article>
  <h1>文章标题</h1>
  <section>
    <h2>第一章</h2>
    <p>...</p>
  </section>
  <section>
    <h2>第二章</h2>
    <p>...</p>
  </section>
</article>

上面代码中,<article>包含了两个<section>,代表两章。

<section>很适合幻灯片展示的页面,每个<section>代表一个幻灯片。

一般来说,<section>都应该有标题,即包含<h1>~<h6>标签。多个<section>可以放置在同一个<article>里面,一个<section>里面也可能包含多个<article>,这取决于<section><article>在当前页面的含义。

<nav>标签用于放置页面或文档的导航信息。

<nav>
  <ol>
    <li><a href="item-a">商品 A</a></li>
    <li><a href="item-b">商品 B</a></li>
    <li>商品 C</li>
  </ol>
</nav>

一般来说,<nav>往往放置在<header>里面,不适合放入<footer>。另外,一个页面可以有多个<nav>,比如一个用于站点导航,另一个用于文章导航。

<h1> ~ <h6>

HTML 提供了6个标签,用来表示文章的标题。按照标题的等级,一共分成六级。

  • <h1>:一级标题
  • <h2>:二级标题
  • <h3>:三级标题
  • <h4>:四级标题
  • <h5>:五级标题
  • <h6>:六级标题

<h1>是最高级别的标题,<h6>是最低级别的标题。下一级标题都是上一级标题的子标题,比如,一个<h1>后面可以有多个<h2>,每个<h2>后面又可以有多个<h3>

<body>
  <h1>JavaScript 语言介绍</h1>
    <h2>概述</h2>
    <h2>基本概念</h2>
      <h3>网页</h3>
      <h3>链接</h3>
    <h2>主要用法</h2>
</body>

上面代码,通过章节标题,清晰地表明了文章的主体结构。具体的内容,就可以写在章节标题的下面。

默认情况下,浏览器会粗体显示标题。h1的字号比h2大,h2h3大,以此类推。

标题标签(包括 h1h6)有很高的使用率,它们用于描述内容的主题。 在屏幕阅读器中,用户为了快速了解页面纲要,可以设置让阅读器只朗读页面标题。 这意味着我们不应仅仅为了设置不同字号而使用标题,而应让标签本身具有语义化和实质性的含义,同时不同标题之间也应关联(具有层级关系)。

语义化的意思是,标签名能准确地表达它所含内容的信息类型。

在使用中,相同级别(或者更高级别)的标题标签用于开启新的章节,低一级别的标题标签用于开启上一级标题标签的子小节。标题不应该越级,比如h1下面直接写h3。虽然这样不会报错,但会导致文章失去清晰的章节结构。

比如说,如果我们在一个 h2 标签后加上若干由 h4 标签引导的页面。 此时发生了层级的错位,这会让使用屏幕阅读器的用户感到困惑。 尽管在浏览器所显示的页面中,错误地使用这六个标题标签依然可以让它们在视觉效果上看起来很合理。 但此时,我们应该按照层级正确地使用标签,然后用 CSS 来调整样式。

最后一点,每个页面应只有一个 h1 标签,用来概括说明页面的主题。 另外,这六个标题标签可以让搜索引擎获取页面的大纲。

<hgroup>

如果主标题包含多级标题(比如带有副标题),那么可以使用<hgroup>标签,将多级标题放在其中。

<hgroup>
  <h1>Heading 1</h1>
  <h2>Subheading 1</h2>
  <h2>Subheading 2</h2>
</hgroup>

注意,<hgroup>只能包含<h1>~<h6>,不能包含其他标签。

文本标签

历史上,网页的主要功能是文本展示。所以,HTML 提供了大量的文本处理标签。

<div>

<div>是一个通用标签,表示一个区块(division)。它没有语义,如果网页需要一个块级元素容器,又没有其他合适的标签,就可以使用这个标签。

它的最常见用途就是提供 CSS 的钩子,用来指定各种样式。所以在早期,下面层层包裹的<div>就很常见。

<div class="main">
  <div class="article">
    <div class="title">
      <h1>文章标题</h1>
    </div>
  </div>
</div>

上面代码读起来很费力,因为不带有语义。后来,HTML 5 就提出了语义标签,改进了上面的代码。

<main>
  <article>
    <header>
      <h1>文章标题</h1>
    </header>
  </article>
</main>

<div>是无语义的块级元素。下面的例子使用<div>,将图像和文字组合在一起,构成一个警告区块。

<div>
  <img src="warning.jpg" alt="警告">
  <p>小心</p>
</div>

只要样式上需要多个块级元素组合在一起,就可以使用<div>。但是,这应该是最后的措施,带有语义的块级标签(比如<article><section><aside><nav>等)始终应该优先使用,当且仅当没有其他语义元素合适时,才可以使用<div>

<p>

<p>标签是一个块级元素,代表文章的一个段落(paragraph)。不仅是文本,任何想以段落显示的内容,比如图片和表单项,都可以放进<p>元素。

<p>hello world</p>

上面代码就是一个简单的段落。

Web 开发者通常用 lorem ipsum text 来做占位符。 lorem ipsum text 是从古罗马西塞罗的一段著名经文中随机抽取的。Lorem ipsum text 自 16 世纪以来就在排版中被用作占位符,这一习惯也在 Web 开发中得以延续。

<p>ipsum</p>

<span>

<span>是一个通用目的的行内标签(即不会产生换行),不带有任何语义。它通常用作 CSS 样式的钩子,如果需要对某些行内内容指定样式,就可以把它们放置在<span>

<p>这是一句<span>重要</span>的句子。</p>

上面代码中,句子里面需要强调的部分,就可以放在<span>

<br><wbr>

<br>让网页产生一个换行效果。该标签是单独使用的,没有闭合标签。

hello<br>world

浏览器渲染上面代码时,会分成两行,helloworld各占一行。

<br>对于诗歌和地址的换行非常有用。

<p>
  床前明月光,<br>
  疑是地上霜。<br>
  举头望明月,<br>
  低头思故乡。
</p>

上面的代码如果不用<br>,会显示成一行。

注意,块级元素的间隔,不要使用<br>来产生,而要使用 CSS 指定。

<p>第一段</p>
<br>
<br>
<p>第二段</p>

上面的代码希望段落之间有两个换行,这时不应该使用<br>,而应该使用 CSS。

<wbr>标签跟<br>很相似,表示一个可选的断行。如果一行的宽度足够,则不断行;如果宽度不够,需要断行,就在<wbr>的位置的断行。它是为了防止浏览器在一个很长的单词中间,不正确地断行或者不断行,所以事先标明可以断行的位置,主要用于欧洲一些单词很长的语言或者 URL 的断行。

<p>
Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
</p>

上面代码是一个很长的德语单词,为了防止不正确断行,事先用<wbr>告诉浏览器,可以选择在哪里断行。

<hr>

<hr>用来在一篇文章中分隔两个不同的主题,浏览器会将其渲染为一根水平线。该标签是单独使用的,没有闭合标签。

<p>第一个主题</p>
<hr>
<p>第二个主题</p>

上面代码的渲染结果是,两段之间会出现一根水平线。

该标签是历史遗留下来的,建议尽量避免使用。主题之间的分隔可以使用<section>,如果想要水平线的效果,可以使用 CSS。

<pre>

<pre>是一个块级元素,表示保留原来的格式(preformatted),即浏览器会保留该标签内部原始的换行和空格。浏览器默认以等宽字体显示标签内容。

<pre>hello

   world</pre>

上面代码中,换行和连续空格都会由于<pre>标签,而被保留下来,浏览器按照原样输出。

注意,HTML 标签在<pre>里面还是起作用的。<pre>只保留空格和换行,不会保留 HTML 标签。

<pre><strong>hello world</strong></pre>

上面代码中,<pre>标签的内容会加粗显示。

<strong><b>

<strong>是一个行内元素,表示它包含的内容具有很强的重要性,需要引起注意。浏览器会以粗体显示内容。

<p>开会时间是<strong>下午两点</strong>。</p>

<b><strong>很相似,也表示它包含的内容需要引起注意,浏览器会加粗显示。它是 Boldface 的缩写。

<p>开会时间是<b>下午两点</b>。</p>

它与<strong>的区别在于,由于历史原因,它没有语义,是一个纯样式的标签,违反了语义与样式分离的原则,因此不建议使用,应该优先使用<strong>标签。

<em><i>

<em>是一个行内标签,表示强调(emphasize)、需要用户着重阅读的内容,浏览器会以斜体显示它包含的内容。

<p>我们<em>已经</em>讨论过这件事情了。</p>

虽然浏览器通常会以斜体显示<em>,但无法保证一定如此,所以最好还是用 CSS 指定一下这个标签的样式。

<em> 元素是可以嵌套的,嵌套层次越深,则其包含的内容被认定为越需要着重阅读。

<i>标签与<em>相似,用于表现因某些原因需要区分普通文本的一系列文本,浏览器会以斜体显示。它是 Italic 的缩写。

<p>我心想,这件事是<i>真的</i>吗?</p>

<i>标签的语义不强,更接近是一个纯样式的标签,建议优先使用<em>标签代替它。

<sub><sup><var>

<sub>标签将内容变为下标,<sup>标签将内容变为上标。它们都是行内元素,主要用于数学公式、分子式等。

<p>水分子是 H<sub>2</sub>O。</p>

<var>标签表示代码或数学公式的变量。

<p>勾股定理是
  <var>a</var><sup>2</sup> + <var>b</var><sup>2</sup> = <var>c</var><sup>2</sup>
。</p>

<u><s>

<u>标签是一个行内元素,下划线通常用来表示重要内容或需要记忆的内容。浏览器默认以下划线渲染内容。

<p>
一个容易写错的成语是把<em>安分守己</em>写成<u>安份守己</u>。
</p>

上面代码中,<u>提示用户这是一个拼写错误,“安份守己”的下方会有一个下划线。

注意,<u>会产生下划线,由于链接也默认带有下划线,所以必须非常小心使用<u>标签,避免用户误以为可以点击。万一确有必要使用,最好使用 CSS 改变<u>的默认样式。

<s>标签是一个行内元素,为内容加上删除线。 删除线是位于文字水平中央的一条线,它代表着一段文字不再有效。

<p>今天特价商品:<s>三文鱼</s>(售完)</p>

上面代码中,“三文鱼”会有一根删除线。

<blockquote><cite><q>

<blockquote>是一个块级标签,表示引用他人的话。浏览器会在样式上,与正常文本区别显示。

<blockquote cite="https://quote.example.com">
  <p>天才就是 1% 的天赋和99%的汗水。</p>
</blockquote>

<blockquote>标签有一个cite属性,它的值是一个网址,表示引言来源,不会显示在网页上。

<cite>标签表示引言出处或者作者,浏览器默认使用斜体显示这部分内容。

<blockquote cite="https://quote.example.com">
  <p>天才就是 1% 的天赋和99%的汗水。</p>
</blockquote>
<cite>-- 爱迪生</cite>

<cite>不一定跟<blockquote>一起使用。如果文章中提到资料来源,也可以单独使用。

<p>更多资料请看<cite>维基百科</cite>。</p>

<q>是一个行内标签,也表示引用。它与<blockquote>的区别,就是它不会产生换行。

<p>
  莎士比亚的《哈姆雷特》有一句著名的台词:
  <q cite="https://quote.example.com">活着还是死亡,这是一个问题。</q>
</p>

上面例子中,引言部分跟前面的说明部分是在同一行里面。

另外,跟<blockquote>一样,<q>也有cite属性,表示引言的来源网址。

注意,浏览器默认会斜体显示<q>的内容,并且会自动添加半角的双引号。所以,引用中文内容时要小心。

<code>

<code>标签是一个行内元素,表示标签内容是计算机代码,浏览器默认会以等宽字体显示。

<code>alert()</code>的作用是让网页弹出一个提示框。

如果要表示多行代码,<code>标签必须放在<pre>内部。<code>本身仅表示一行代码。

<pre>
<code>
  let a = 1;
  console.log(a);
</code>
</pre>

<kbd><samp>

<kbd>标签是一个行内元素,原意是用户从键盘输入的内容,现在扩展到各种输入,包括语音输入。浏览器默认以等宽字体显示标签内容。

<p>Windows 可以按下 <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> 重启。</p>

<kbd>可以嵌套,方便指定样式。

<p>Windows 可以按下
<kbd> <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> </kbd>
重启。</p>

<samp>标签是一个行内元素,表示计算机程序输出内容的一个例子。浏览器默认以等宽字体显示。

<p>如果使用没有定义的变量,浏览器会报错:
<samp>Uncaught ReferenceError: foo is not defined</samp>。
</p>

<mark>

<mark>是一个行内标签,表示突出显示的内容。Chrome 浏览器默认会以亮黄色背景,显示该标签的内容。

<p>我们讨论以后决定,<mark>运行会在下周三举办</mark>。</p>

<mark>很适合在引用的内容(<q><blockquote>)中,标记出需要关注的句子。

<blockquote>
床前明月光,疑是地上霜。<mark>举头望明月,低头思故乡。</mark>
</blockquote>

除了标记感兴趣的文本,<mark>还可以用于在搜索结果中,标记出匹配的关键词。

注意,不要只为了高亮的效果,而使用这个标签,因为不能保证浏览器的处理方式。如果要保证高亮,还是要使用 CSS 样式。

<small>

<small>是一个行内标签,浏览器会将它包含的内容,以小一号的字号显示,不需要使用 CSS 样式。它通常用于文章附带的版权信息或法律信息。

<p>文章正文</p>
<p><small>以上内容使用创意共享许可证。</small></p>

<time><data>

<time>是一个行内标签,为跟时间相关的内容提供机器可读的格式。通过标准化时间格式,即使时间在文本中是以非正式的或口语化的形式编写,辅助设备依然可以获取准确的时间和日期。

<p>运动会预定<time datetime="2015-06-10">下周三</time>举行。</p>

上面代码中,<time>表示下周三的具体日期。这方便搜索引擎抓取,或者下一步的其他处理。

<time>datetime属性,用来指定机器可读的日期,可以有多种格式。

  • 有效年份:2011
  • 有效月份:2011-11
  • 有效日期:2011-11-18
  • 无年份的日期:11-18
  • 年度的第几周:2011-W47
  • 有效时间:14:5414:54:3914:54:39.929
  • 日期和时间:2011-11-18T14:54:39.929
<p>音乐会在<time datetime="20:00">晚上八点</time>开始。</p>

<data>标签与<time>类似,也是提供机器可读的内容,但是用于非时间的场合。

<p>本次马拉松比赛第一名是<data value="39">张三</data></p>。

上面代码中,选手的机读数据就放在<data>标签的value属性。

<address>

<address>标签是一个块级元素,表示某人或某个组织的联系方式。

<p>作者的联系方式:</p>

<address>
  <p><a href="mailto:foo@example.com">foo@example.com</a></p>
  <p><a href="tel:+555-34762301">+555-34762301</a></p>
</address>

该标签有几个注意点。

(1)如果是文章里提到的地址(比如提到搬家前的地址),而不是联系信息,不要使用<address>标签。

(2)<address>的内容不得有非联系信息,比如发布日期。

(3)<address>不能嵌套,并且内部不能有如下标签

<h1>~<h6>
<article>
<aside>
<section>
<nav>
<header>
<footer>`

(4)通常,<address>会放在<footer>里面,下面是一个例子。

<footer>
  <address>
    文章的相关问题请联系<a href="mailto:zhangsan@example.com">张三
    McClure</a>。
  </address>
</footer>

<abbr>

<abbr>标签是一个行内元素,表示标签内容是一个缩写。它的title属性给出缩写的完整形式,或者缩写的描述。鼠标悬停在该元素上方时,title属性值作为提示,会完整显示出来。

<abbr title="HyperText Markup Language">HTML</abbr>

注意,某些浏览器可能对该标签提供圆点下划线。

<ins><del>

<ins>标签是一个行内元素,表示原始文档添加(insert)的内容。<del>与之类似,表示删除(delete)的内容。它们通常用于展示文档的删改。

<del><p>会议定于5月8日举行。</p></del>
<ins><p>会议定于5月9日举行。</p></ins>

浏览器默认为<del>标签的内容加上删除线,为<ins>标签的内容加上下划线。

这两个标签都有以下属性。

  • cite:该属性的值是一个 URL,表示该网址可以解释本次删改。
  • datetime:表示删改发生的时间。
<ins cite="./why.html" datetime="2018-05">
  <p>项目比原定时间提前两周结束。</p>
</ins>

<dfn>

<dfn>是一个行内元素,表示标签内容是一个术语(definition),本段或本句包含它的定义。

<p>
通过 TCP/IP 协议连接的全球性计算机网络,叫做 <dfn>Internet</dfn>。
</p>

为了脚本操作的方便,可以把术语的定义写入<dfn>标签的title属性。

<p>
通过 TCP/IP 协议连接的全球性计算机网络,叫做
<dfn title="全球性计算机网络">Internet</dfn>。
</p>

上面代码中,title属性的一个作用是,鼠标悬浮的时候,术语的解释会以提示的形式显示出来。

某些时候,术语本身是一个缩写,这时<dfn><abbr>可以结合使用。

<p>
<dfn><abbr title="acquired immune deficiency syndrome">AIDS</abbr></dfn>
的全称是获得性免疫缺陷综合征。
</p>

<ruby>

<ruby>标签表示文字的语音注释,主要用于东亚文字,比如汉语拼音和日语的片假名。它默认将语音注释,以小字体显示在文字的上方。

<ruby>
汉<rp>(</rp><rt>han</rt><rp>)</rp>
字<rp>(</rp><rt>zi</rt><rp>)</rp>
</ruby>

上面代码的渲染结果是,汉字上方有小字体的拼音han zi

<ruby>标签是一个行内元素,也是一个容器标签。如果要使用语音注释,就必须把文字和注释都放在这个标签里面。

<ruby>的内部还有许多配套的标签。

(1)<rp>

<rp>标签的用处,是为不支持语音注释的浏览器,提供一个兼容方案。对于那些支持语音注释的浏览器,该标签的内容不显示。

<rp>标签一般用于放置圆括号,如果遇到不支持的浏览器,就会将语音注释显示在括号里面。

<ruby>
汉<rp>(</rp><rt>han</rt><rp>)</rp>
字<rp>(</rp><rt>zi</rt><rp>)</rp>
</ruby>

上面代码在不支持语音注释的浏览器中,渲染结果为汉(han)字(zi)。遇到支持语音注释的浏览器,就不会显示圆括号。

(2)<rt>

<rt>标签用于放置语音注释。

(3)<rb>

<rb>标签用于划分文字单位,与语音注释一一对应。

<ruby>
  <rb>汉</rb><rb>字</rb>
  <rp>(</rp>
  <rt>han</rt>
  <rt>zi</rt>
  <rp>)</rp>
</ruby>

上面例子中,汉字这两个字是写在一起的,<rb>标签用于每个字划分出来,跟<rt>标签一一对应。

(4)<rbc><rtc>

<rbc>标签表示一组文字,通常包含多个<rb>元素。<rtc>标签表示一组语音注释,跟<rbc>对应。

<ruby style="ruby-position: under;">
  <rbc>
    <rb>汉</rb><rp>(</rp><rt>han</rt><rp>)</rp>
    <rb>字</rb><rp>(</rp><rt>zi</rt><rp>)</rp>
  </rbc>
  <rtc style="ruby-position: over;">
    <rp>(</rp><rt>Chinese</rt><rp>)</rp>
  </rtc>
</ruby>

上面例子中,汉字这两个字有两组语音注释,分别是汉语拼音与英语。一组语音注释放在<rbc>标签中,另一组语音注释放在<rtc>,用来对应<rbc>。同时,分别使用style属性,指定汉语拼音显示在文字下方,英语显示在文字上方。

<bdo><bdi>

大部分文字的阅读方向是从左到右,但是有些文字的方向是从右到左,比如阿拉伯语、希伯来语等。<bdo>标签是一个行内元素,表示文字方向与网页主体内容的方向不一致。

<p>床前明月光,<bdo dir="rtl">霜上地是疑</bdo>。</p>

上面代码中,<bdo>标签里面的文字,会以相反的方向渲染,结果就是“床前明月光,疑是地上霜”。

<bdo>dir属性,指定具体的文字方向。它有两个值,ltr表示从左到右,rtl表示从右到左。

<bdi>标签用于不确定文字方向的情况。比如,网页有一个部分是用户输入的内容,但是不知道输入内容的文字方向。这种情况就可以使用<bdi>标签,告诉浏览器,不确定文字的方向,由浏览器自己决定。

<p><bdi>床前明月光,疑是地上霜。</bdi></p>

列表标签

列表是一系列排列好的项目,主要分成两类:有序列表和无序列表。

有序列表是每个列表项前面有编号,呈现出顺序,就像下面这样。

1. 列表项 A
2. 列表项 B
3. 列表项 C

无序列表则是列表项前面没有编号,只有一个列表符号,默认是一个圆点。

· 列表项 A
· 列表项 B
· 列表项 C

<ol>

<ol>标签是一个有序列表容器(ordered list),会在内部的列表项前面产生数字编号。列表项的顺序有意义时,比如排名,就会采用这个标签。

<ol>
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码会在列表项 A、B、C 前面,分别产生1、2、3的编号。

<ol>标签内部可以嵌套<ol>标签或<ul>标签,形成多级列表。

<ol>
  <li>列表项 A</li>
  <li>列表项 B
    <ol>
      <li>列表项 B1</li>
      <li>列表项 B2</li>
      <li>列表项 B3</li>
    </ol>
  </li>
  <li>列表项 C</li>
</ol>

上面代码中,一个有序列表内部嵌套了另一个有序列表,渲染结果如下。

1. 列表项 A
2. 列表项 B
  1. 列表项 B1
  2. 列表项 B2
  3. 列表项 B3
3. 列表项 C

该标签有以下属性。

(1)reversed

reversed属性产生倒序的数字列表。

<ol reversed>
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码中,列表项 A、B、C 前面,产生的编号是3、2、1。

(2)start

start属性的值是一个整数,表示数字列表的起始编号。

<ol start="5">
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码中,列表项 A、B、C 前面,产生的编号是5、6、7。

(3)type

type属性指定数字编号的样式。目前,浏览器支持以下样式。

  • a:小写字母
  • A:大写字母
  • i:小写罗马数字
  • I:大写罗马数字
  • 1:整数(默认值)
<ol type="a">
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码中,列表项 A、B、C 前面的编号,分别是英文小写字母a、b、c。

注意,即使编号是字母,start属性也依然使用整数。

<ol type="a" start="3">
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码中,type属性指定编号采用小写英文字母,start属性等于3,表示从c开始编号。

<ul>

<ul>标签是一个无序列表容器(unordered list),会在内部的列表项前面产生实心小圆点,作为列表符号。列表项的顺序无意义时,采用这个标签。

<ul>
  <li>列表项 A</li>
  <li>列表项 B</li>
  <li>列表项 C</li>
</ul>

上面代码的渲染结果是,列表项 A、B、C 前面,分别产生一个实心小圆点,作为列表符号。

<ul>标签内部可以嵌套<ul><ol>,形成多级列表。

type属性指定样式。:

  • disc(实心原点,默认)
  • square(实心方点)
  • circle(空心圆)

<li>

<li>(list item)表示列表项,只能用在<ol><ul>容器之中。

有序列表<ol>之中,<li>有一个value属性,定义当前列表项的编号,后面列表项会从这个值开始编号。

<ol>
  <li>列表项 A</li>
  <li value="4">列表项 B</li>
  <li>列表项 C</li>
</ol>

上面代码中,value属性指定第二个列表项的编号是4,因此三个列表项的编号,分别为1、4、5。

<li>标签也有type属性。

<dl><dt><dd>

<dl>标签是一个块级元素,表示一组术语的列表(description list)。

术语名(description term)由<dt>标签定义,这个标签是必须的。

术语解释(description detail)由<dd>标签定义。

<dl>常用来定义词汇表。

<dl>
  <dt>CPU</dt>
  <dd>中央处理器</dd>

  <dt>Memory</dt>
  <dd>内存</dd>

  <dt>Hard Disk</dt>
  <dd>硬盘</dd>
</dl>

<dt><dd>都是块级元素,<dd>默认会在<dt>下方缩进显示。上面代码的默认渲染结果如下。

CPU
  中央处理器

Memory
  内存

Hard Disk
  硬盘

多个术语(<dt>)对应一个解释(<dd>),或者多个解释(<dd>)对应一个术语(<dt>),都是合法的。

<dl>
  <dt>A</dt>
  <dt>B</dt>
  <dd>C</dd>

  <dt>D</dt>
  <dd>E</dd>
  <dd>F</dd>
</dl>

上面代码中,AB有共同的解释C,而D有两个解释EF

图像标签

图片是互联网的重要组成部分,让网页变得丰富多彩。

<img>

<img>标签用于插入图片。它是单独使用的,没有闭合标签。网页html文档支持的图片格式有 jpg、gif 、png 和 bmp 这四种。

<img src="foo.jpg">

上面代码在网页插入一张图片foo.jpgsrc属性指定图片的网址,上例是相对 URL,表示图片与网页在同一个目录。

<img>默认是一个行内元素,与前后的文字处在同一行。

<p>Hello<img src="foo.jpg">World</p>

上面代码的渲染结果是,文字和图片在同一行显示。

图像默认以原始大小显示。如果图片很大,又与文字处在同一行,那么图片将把当前行的行高撑高,并且图片的底边与文字的底边在同一条水平线上。

<img>可以放在<a>标签内部,使得图片变成一个可以点击的链接。

<a href="example.html">
  <img src="foo.jpg">
</a>

上面代码中,图片可以像链接那样点击,点击后会产生跳转。

(1)alt 属性

alt属性用来设定图片的文字说明。图片不显示时(比如下载失败,或用户关闭图片加载),图片的位置上会显示该文本。

<img src="foo.jpg" alt="示例图片">

上面代码中,alt是图片的说明。图片下载失败时,浏览器会在图片位置,显示文字“示例图片”。

所有的 img 元素必须有 alt 属性。 视觉障碍用户无法通过视觉获取信息,而是通过屏幕阅读器将网页内容转换为音频以获取信息。 他们无法通过直观的呈现理解信息。 屏幕阅读器可以识别 alt 属性,朗读其中的内容,来告知用户图片包含的关键信息。

在图片已经有了文字说明,或者仅仅为了美化页面的情况下,把 alt 的属性值设置为空是最佳实践。理想情况下,alt 属性不应该包含特殊字符,除非有特殊需要。对于有标题的图片,依然需要添加 alt 文本,因为这样有助于搜索引擎记录图片内容。

(2)width 属性,height 属性

图片默认以原始大小插入网页,width属性和height属性可以指定图片显示时的宽度和高度,单位是像素或百分比。

<img src="foo.jpg" width="400" height="300">

上面代码中,width属性指定图片显示的宽度为400像素,height属性指定显示高度为300像素。

注意,一旦设置了这两个属性,浏览器会在网页中预先留出这个大小的空间,不管图片有没有加载成功。不过,由于图片的显示大小可以用 CSS 设置,所以不建议使用这两个属性。

一种特殊情况是,width属性和height属性只设置了一个,另一个没有设置。这时,浏览器会根据图片的原始大小,自动设置对应比例的图片宽度或高度。举例来说,图片大小是 800像素 x 800像素,width属性设置成200,那么浏览器会自动将height设成200。

(3)srcset,sizes

详见下文。

(4)referrerpolicy

<img>导致的图片加载的 HTTP 请求,默认会带有Referer的头信息。referrerpolicy属性对这个行为进行设置。

(5)crossorigin

有时,图片和网页属于不同的网站,网页加载图片就会导致跨域请求,对方服务器可能要求跨域认证。crossorigin属性用来告诉浏览器,是否采用跨域的形式下载图片,默认是不采用。

简单说,只要打开了这个属性,HTTP 请求的头信息里面,就会加入origin字段,给出请求发出的域名,不打开这个属性就不加。

一旦打开该属性,它可以设为两个值。

  • anonymous:跨域请求不带有用户凭证(通常是 Cookie)。
  • use-credentials:跨域请求带有用户凭证。

下面是一个例子。

<img src="foo.jpg" crossorigin="anonymous">

crossorigin属性如果省略值的部分,则等同于anonymous

<img src="foo.jpg" crossorigin>

(6)loading

浏览器的默认行为是,只要解析到<img>标签,就开始加载图片。对于很长的网页,这样做很浪费带宽,因为用户不一定会往下滚动,一直看到网页结束。用户很可能是点开网页,看了一会就关掉了,那些不在视口的图片加载的流量,就都浪费了。

loading属性改变了这个行为,可以指定图片的懒加载,即图片默认不加载,只有即将滚动进入视口,变成用户可见时才会加载,这样就节省了带宽。

loading属性可以取以下三个值。

  • auto:浏览器默认行为,等同于不使用loading属性。
  • lazy:启用懒加载。
  • eager:立即加载资源,无论它在页面上的哪个位置。
<img src="image.png" loading="lazy" alt="…" width="200" height="200">

由于行内图片的懒加载,可能会导致页面布局重排,所以使用这个属性的时候,最好指定图片的高和宽。

<figure><figcaption>

<figure>标签可以理解为一个图像区块,将图像和相关信息封装在一起。<figcaption>是它的可选子元素,表示图像的文本描述,通常用于放置标题,可以出现多个。

<figure>
  <img src="https://example.com/foo.jpg">
  <figcaption>示例图片</figcaption>
</figure>

除了图像,<figure>还可以封装引言、代码、诗歌等等。它等于是一个将主体内容与附加信息,封装在一起的语义容器。

<figure>
  <figcaption>JavaScript 代码示例</figcaption>
  <p><code>const foo = 'hello';</code></p>
</figure>

响应式图像

网页在不同尺寸的设备上,都能产生良好的显示效果,叫做“响应式设计”(responsive web design)。响应式设计的网页图像,就是“响应式图像”(responsive image)。

响应式图像的解决方案有很多,JavaScript 和 CSS 都可以实现。这里只介绍语义性最好的 HTML 方法,浏览器原生支持。

问题的由来

我们知道,<img>标签用于插入网页图像,所有情况默认插入的都是同一张图像。

<img src="foo.jpg">

上面代码在桌面端和手机上,插入的都是图像文件foo.jpg

这种处理方法固然简单,但是有三大弊端。

(1)体积

一般来说,桌面端显示的是大尺寸的图像,文件体积较大。手机的屏幕较小,只需要小尺寸的图像,可以节省带宽,加速网页渲染。

(2)像素密度

随着联网设备的增加,设备间的区别不仅发生在尺寸和规格上,还发生在用于显示的设备上。 像素密度就是区分不同显示设备的一个指标,它一般会以 PPI(Pixel Per Inch,即每英寸像素)或 DPI(每英寸点数)为计量单位。

桌面显示器一般是单倍像素密度,而手机的显示屏往往是多倍像素密度,即显示时多个像素合成为一个像素,这种屏幕称为 Retina 屏幕。图像文件很可能在桌面端很清晰,放到手机上会有点模糊,因为图像没有那么高的像素密度,浏览器自动把图像的每个像素复制到周围像素,满足像素密度的要求,导致图像的锐利度有所下降。

(3)视觉风格

桌面显示器的面积较大,图像可以容纳更多细节。手机的屏幕较小,许多细节是看不清的,需要突出重点。

上面两张图片,下方的手机图片经过裁剪以后,更突出图像重点,明显效果更好。

srcset属性

为了解决上面这些问题,HTML 语言提供了一套完整的解决方案。首先,<img>标签引入了srcset属性。

srcset属性用来指定多张图像,适应不同像素密度的屏幕。它的值是一个逗号分隔的字符串,每个部分都是一张图像的 URL,后面接一个空格,然后是像素密度的描述符。请看下面的例子。

<img srcset="foo-320w.jpg,
             foo-480w.jpg 1.5x,
             foo-640w.jpg 2x"
     src="foo-640w.jpg">

上面代码中,srcset属性给出了三个图像 URL,适应三种不同的像素密度。

图像 URL 后面的像素密度描述符,格式是像素密度倍数 + 字母x1x表示单倍像素密度,可以省略。浏览器根据当前设备的像素密度,选择需要加载的图像。

如果srcset属性都不满足条件,那么就加载src属性指定的默认图像。

sizes属性

像素密度的适配,只适合显示区域一样大小的图像。如果希望不同尺寸的屏幕,显示不同大小的图像,srcset属性就不够用了,必须搭配sizes属性。

第一步,srcset属性列出所有可用的图像。

<img srcset="foo-160.jpg 160w,
             foo-320.jpg 320w,
             foo-640.jpg 640w,
             foo-1280.jpg 1280w"
     src="foo-1280.jpg">

上面代码中,srcset属性列出四张可用的图像,每张图像的 URL 后面是一个空格,再加上宽度描述符。

宽度描述符就是图像原始的宽度,加上字符w。上例的四种图片的原始宽度分别为160像素、320像素、640像素和1280像素。

第二步,sizes属性列出不同设备的图像显示宽度。

sizes属性的值是一个逗号分隔的字符串,除了最后一部分,前面每个部分都是一个放在括号里面的媒体查询表达式,后面是一个空格,再加上图像的显示宽度。

<img srcset="foo-160.jpg 160w,
             foo-320.jpg 320w,
             foo-640.jpg 640w,
             foo-1280.jpg 1280w"
     sizes="(max-width: 440px) 100vw,
            (max-width: 900px) 33vw,
            254px"
     src="foo-1280.jpg">

上面代码中,sizes属性给出了三种屏幕条件,以及对应的图像显示宽度。宽度不超过440像素的设备,图像显示宽度为100%;宽度441像素到900像素的设备,图像显示宽度为33%;宽度900像素以上的设备,图像显示宽度为254px

第三步,浏览器根据当前设备的宽度,从sizes属性获得图像的显示宽度,然后从srcset属性找出最接近该宽度的图像,进行加载。

假定当前设备的屏幕宽度是480px,浏览器从sizes属性查询得到,图片的显示宽度是33vw(即33%),等于160pxsrcset属性里面,正好有宽度等于160px的图片,于是加载foo-160.jpg

如果省略sizes属性,那么浏览器将根据实际的图像显示宽度,从srcset属性选择最接近的图片。一旦使用sizes属性,就必须与srcset属性搭配使用,单独使用sizes属性是无效的。

<picture>

响应式用法

<img>标签的srcset属性和sizes属性分别解决了像素密度和屏幕大小的适配,但如果要同时适配不同像素密度、不同大小的屏幕,就要用到<picture>标签。

<picture>是一个容器标签,内部使用<source><img>,指定不同情况下加载的图像。

<picture>
  <source media="(max-width: 500px)" srcset="cat-vertical.jpg">
  <source media="(min-width: 501px)" srcset="cat-horizontal.jpg">
  <img src="cat.jpg" alt="cat">
</picture>

上面代码中,<picture>标签内部有两个<source>标签和一个<img>标签。

<picture>内部的<source>标签,主要使用media属性和srcset属性。media属性给出媒体查询表达式,srcset属性就是<img>标签的srcset属性,给出加载的图像文件。sizes属性其实这里也可以用,但由于有了media属性,就没有必要了。

浏览器按照<source>标签出现的顺序,依次判断当前设备是否满足media属性的媒体查询表达式,如果满足就加载srcset属性指定的图片文件,并且不再执行后面的<source>标签和<img>标签。

<img>标签是默认情况下加载的图像,用来满足上面所有<source>都不匹配的情况,或者不支持<picture>的老式浏览器。

上面例子中,设备宽度如果不超过500px,就加载竖屏的图像,否则加载横屏的图像。

下面给出一个例子,同时考虑屏幕尺寸和像素密度的适配。

<picture>
  <source srcset="homepage-person@desktop.png,
                  homepage-person@desktop-2x.png 2x"
          media="(min-width: 990px)">
  <source srcset="homepage-person@tablet.png,
                  homepage-person@tablet-2x.png 2x"
          media="(min-width: 750px)">
  <img srcset="homepage-person@mobile.png,
               homepage-person@mobile-2x.png 2x"
       alt="Shopify Merchant, Corrine Anestopoulos">
</picture>

上面代码中,<source>标签的media属性给出屏幕尺寸的适配条件,每个条件都用srcset属性,再给出两种像素密度的图像 URL。

图像格式的选择

除了响应式图像,<picture>标签还可以用来选择不同格式的图像。比如,如果当前浏览器支持 Webp 格式,就加载这种格式的图像,否则加载 PNG 图像。

<picture>
  <source type="image/svg+xml" srcset="logo.xml">
  <source type="image/webp" srcset="logo.webp"> 
  <img src="logo.png" alt="ACME Corp">
</picture>

上面代码中,<source>标签的type属性给出图像的 MIME 类型,srcset是对应的图像 URL。

浏览器按照<source>标签出现的顺序,依次检查是否支持type属性指定的图像格式,如果支持就加载图像,并且不再检查后面的<source>标签了。上面例子中,图像加载优先顺序依次为 svg 格式、webp 格式和 png 格式。

链接标签

链接(hyperlink)是互联网的核心。它允许用户在页面上,从一个网址跳转到另一个网址,从而把所有资源联系在一起。

URL 是链接指向的地址。链接不仅可以指向另一个网页,也可以指向文本、图像、文件等资源。可以这样说,所有互联网上的资源,都可以通过链接访问。

<a>

链接通过<a>(Anchor,简写为 a)标签表示,用户点击后,浏览器会跳转到指定的网址。下面就是一个典型的链接。

<a href="https://wikipedia.org/">维基百科</a>

上面代码就定义了一个超级链接。浏览器显示“维基百科”,文字下面默认会有下划线,表示这是一个链接。用户点击后,浏览器跳转到href属性指定的网址。

<a>标签内部不仅可以放置文字,也可以放置其他元素,比如段落、图像、多媒体等等。

<a href="https://www.example.com/">
  <img src="https://www.example.com/foo.jpg">
</a>

上面代码中,<a>标签内部就是一个图像。用户点击图像,就会跳转到指定网址。

你可以在其他文本元素内嵌套链接。通常,文本是被包裹在 p 元素内。a 元素内的文本叫作锚文本,会显示为一个可以点击的链接。

<p>
  Here's a <a target="_blank" href="http://freecodecamp.org"> link to freecodecamp.org</a> for you to follow.
</p>

有时你想为网站添加一个 a 元素,但还不确定要将它链接到哪里。可用 # 作为链接占位符。

屏幕阅读器用户可以选择其设备读取的内容的类型,这包括跳转到(或跳过)标志标签,以便跳转到主要内容,或者从标题中获取页面摘要,还可以选择只听取页面中的有效链接。

屏幕阅读器通过阅读链接文本或者锚点标签(a)之间的内容来完成这个操作。拥有 “click here” 或者 “read more” 列表并没有什么用处。相反地,应该在a标签中使用简洁的描述性语言来为用户提供更多的信息。

<a>标签有如下属性。

(1)href

href(hypertext refrence)属性给出链接指向的网址。读作“喝瑞夫”。它的值应该是一个 URL 或者锚点。

上文已经给出了完整 URL 的例子,下面是锚点的例子。

<a href="#demo">示例</a>

上面代码中,href属性的值是#加上锚点名称。点击后,浏览器会自动滚动,停在当前页面里面demo锚点所在的位置。

(2)hreflang

hreflang属性给出链接指向的网址所使用的语言,纯粹是提示性的,没有实际功能。

<a
  href="https://www.example.com"
  hreflang="en"
>示例网址</a>

上面代码表明,href属性指向的网址的语言是英语。

该属性的值跟通用属性lang一样。

(3)title

title属性给出链接的说明信息。鼠标悬停在链接上方时,浏览器会将这个属性的值,以提示块的形式显示出来。

<a
  href="https://www.example.com/"
  title="hello"
>示例</a>。

上面代码中,用户鼠标停留在链接上面,会出现文字提示hello

(4)target

target属性指定如何展示打开的链接。它可以是在指定的窗口打开,也可以在<iframe>里面打开。

<p><a href="http://foo.com" target="test">foo</a></p>
<p><a href="http://bar.com" target="test">bar</a></p>

上面代码中,两个链接都在名叫test的窗口打开。首先点击链接foo,浏览器发现没有叫做test的窗口,就新建一个窗口,起名为test,在该窗口打开foo.com。然后,用户又点击链接bar,由于已经存在test窗口,浏览器就在该窗口打开bar.com,取代里面已经打开的foo.com

target属性的值也可以是以下四个关键字之一。

  • _self:当前窗口打开,这是默认值。
  • _blank:新窗口打开。
  • _parent:上层窗口打开,这通常用于从父窗口打开的子窗口,或者<iframe>里面的链接。如果当前窗口没有上层窗口,这个值等同于_self
  • _top:顶层窗口打开。如果当前窗口就是顶层窗口,这个值等同于_self
<a
  href="https://www.example.com"
  target="_blank"
>示例链接</a>

上面代码点击后,浏览器会新建一个窗口,在该窗口打开链接,并且新窗口没有名字。

注意,使用target属性的时候,最好跟rel="noreferrer"一起使用,这样可以避免安全风险。

(5)rel

rel属性说明链接与当前页面的关系。

<a href="help.html" rel="help">帮助</a>

上面代码的rel属性,说明链接是当前页面的帮助文档。

下面是一些常见的rel属性的值。

  • alternate:当前文档的另一种形式,比如翻译。
  • author:作者链接。
  • bookmark:用作书签的永久地址。
  • external:当前文档的外部参考文档。
  • help:帮助链接。
  • license:许可证链接。
  • next:系列文档的下一篇。
  • nofollow:告诉搜索引擎忽略该链接,主要用于用户提交的内容,防止有人企图通过添加链接,提高该链接的搜索排名。
  • noreferrer:告诉浏览器打开链接时,不要将当前网址作为 HTTP 头信息的Referer字段发送出去,这样可以隐藏点击的来源。
  • noopener:告诉浏览器打开链接时,不让链接窗口通过 JavaScript 的window.opener属性引用原始窗口,这样就提高了安全性。
  • prev:系列文档的上一篇。
  • search:文档的搜索链接。
  • tag:文档的标签链接。

(6)referrerpolicy

referrerpolicy属性用于精确设定点击链接时,浏览器发送 HTTP 头信息的Referer字段的行为。

该属性可以取下面八个值:no-referrerno-referrer-when-downgradeoriginorigin-when-cross-originunsafe-urlsame-originstrict-originstrict-origin-when-cross-origin

其中,no-referrer表示不发送Referer字段,same-origin表示同源时才发送Referer字段,origin表示只发送源信息(协议+域名+端口)。其他几项的解释,请查阅 HTTP 文档。

(7)ping

ping属性指定一个网址,用户点击的时候,会向该网址发出一个 POST 请求,通常用于跟踪用户的行为。

(8)type

type属性给出链接 URL 的 MIME 类型,比如到底是网页,还是图像或文件。它也是纯粹提示性的属性,没有实际功能。

<a
  href="smile.jpg"
  type="image/jpeg"
>示例图片</a>

上面代码中,type属性提示这是一张图片。

(9)download

download属性表明当前链接用于下载,而不是跳转到另一个 URL。

<a href="demo.txt" download>下载</a>

上面代码点击后,会出现下载对话框。

注意,download属性只在链接与网址同源时,才会生效。也就是说,链接应该与网址属于同一个网站。

如果download属性设置了值,那么这个值就是下载的文件名。

<a
  href="foo.exe"
  download="bar.exe"
>点击下载</a>

上面代码中,下载文件的原始文件名是foo.exe。点击后,下载对话框提示的文件名是bar.exe

注意,如果链接点击后,服务器的 HTTP 回应的头信息设置了Content-Disposition字段,并且该字段的值与download属性不一致,那么该字段优先,下载时将显示其设置的文件名。

download属性还有一个用途,就是有些地址不是真实网址,而是数据网址,比如data:开头的网址。这时,download属性可以为虚拟网址指定下载的文件名。

<a href="data:,Hello%2C%20World!">点击</a>

上面链接点击后,会打开一个虚拟网页,上面显示Hello World!

<a
  href="data:,Hello%2C%20World!"
  download="hello.txt"
>点击</a>

上面链接点击后,下载的hello.txt文件内容就是“Hello, World!”。

邮件链接

链接也可以指向一个邮件地址,使用mailto协议。用户点击后,浏览器会打开本机默认的邮件程序,让用户向指定的地址发送邮件。

<a href="mailto:contact@example.com">联系我们</a>

上面代码中,链接就指向邮件地址。点击后,浏览器会打开一个邮件地址,让你可以向contact@example.com发送邮件。

除了邮箱,邮件协议还允许指定其他几个邮件要素。

  • subject:主题
  • cc:抄送
  • bcc:密送
  • body:邮件内容

使用方法是将这些邮件要素,以查询字符串的方式,附加在邮箱地址后面。

<a
  href="mailto:foo@bar.com?cc=test@test.com&subject=The%20subject&body=The%20body"
>发送邮件</a>

上面代码中,邮件链接里面不仅包含了邮箱地址,还包含了ccsubjectbody等邮件要素。这些要素的值需要经过 URL 转义,比如空格转成%20

不指定邮箱也是允许的,就像下面这样。这时用户自己在邮件程序里面,填写想要发送的邮箱,通常用于邮件分享网页。

<a href="mailto:">告诉朋友</a>

Mailto 链接——如何制作 HTML 电子邮件链接

电话链接

如果是手机浏览的页面,还可以使用tel协议,创建电话链接。用户点击该链接,会唤起电话,可以进行拨号。

<a href="tel:13312345678">13312345678</a>

上面代码在手机中,点击链接会唤起拨号界面,可以直接拨打指定号码。

基本用法

<link>标签主要用于将当前网页与相关的外部资源联系起来,通常放在<head>元素里面。最常见的用途就是加载 CSS 样式表。

<link rel="stylesheet" type="text/css" href="theme.css">

上面代码为网页加载样式表theme.css

除了默认样式表,网页还可以加载替代样式表,即默认不生效、需要用户手动切换的样式表。

<link href="default.css" rel="stylesheet" title="Default Style">
<link href="fancy.css" rel="alternate stylesheet" title="Fancy">
<link href="basic.css" rel="alternate stylesheet" title="Basic">

上面代码中,default.css是默认样式表,默认就会生效。fancy.cssbasic.css是替换样式表(rel="alternate stylesheet"),默认不生效。title属性在这里是必需的,用来在浏览器菜单里面列出这些样式表的名字,供用户选择,以替代默认样式表。

<link>还可以加载网站的 favicon 图标文件。

<link rel="icon" href="/favicon.ico" type="image/x-icon">

手机访问时,网站通常需要提供不同分辨率的图标文件。

<link rel="apple-touch-icon-precomposed" sizes="114x114" href="favicon114.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="favicon72.png">

上面代码指定 iPhone 设备需要的114像素和72像素的图标。

<link>也用于提供文档的相关链接,比如下面是给出文档的 RSS Feed 地址。

<link rel="alternate" type="application/atom+xml" href="/blog/news/atom">

rel 属性

rel属性表示外部资源与当前文档之间的关系,是<link>标签的必需属性。它可以但不限于取以下值。

  • alternate:文档的另一种表现形式的链接,比如打印版。
  • author:文档作者的链接。
  • dns-prefetch:要求浏览器提前执行指定网址的 DNS 查询。
  • help:帮助文档的链接。
  • icon:加载文档的图标文件。
  • license:许可证链接。
  • next:系列文档下一篇的链接。
  • pingback:接收当前文档 pingback 请求的网址。
  • preconnect:要求浏览器提前与给定服务器,建立 HTTP 连接。
  • prefetch:要求浏览器提前下载并缓存指定资源,供下一个页面使用。它的优先级较低,浏览器可以不下载。
  • preload:要求浏览器提前下载并缓存指定资源,当前页面稍后就会用到。它的优先级较高,浏览器必须立即下载。
  • prerender:要求浏览器提前渲染指定链接。这样的话,用户稍后打开该链接,就会立刻显示,感觉非常快。
  • prev:表示当前文档是系列文档的一篇,这里给出上一篇文档的链接。
  • search:提供当前网页的搜索链接。
  • stylesheet:加载一张样式表。

下面是一些示例。

<!-- 作者信息 -->
<link rel="author" href="humans.txt">

<!-- 版权信息 -->
<link rel="license" href="copyright.html">

<!-- 另一个语言的版本 -->
<link rel="alternate" href="https://es.example.com/" hreflang="es">

<!-- 联系方式 -->
<link rel="me" href="https://google.com/profiles/someone" type="text/html">
<link rel="me" href="mailto:name@example.com">
<link rel="me" href="sms:+15035550125">

<!-- 历史资料 -->
<link rel="archives" href="http://example.com/archives/">

<!-- 目录 -->
<link rel="index" href="http://example.com/article/">

<!-- 导航 -->
<link rel="first" href="http://example.com/article/">
<link rel="last" href="http://example.com/article/?page=42">
<link rel="prev" href="http://example.com/article/?page=1">
<link rel="next" href="http://example.com/article/?page=3">

资源的预加载

某些情况下,你需要浏览器预加载某些资源,也就是先把资源缓存下来,等到使用的时候,就不用再从网上下载了,立即就能使用。预处理指令可以做到这一点。

预加载主要有下面五种类型。

(1)<link rel="preload">

<link rel="preload">告诉浏览器尽快下载并缓存资源(如脚本或样式表),该指令优先级较高,浏览器肯定会执行。当加载页面几秒钟后需要该资源时,它会很有用。下载后,浏览器不会对资源执行任何操作,脚本未执行,样式表未应用。它只是缓存,当其他东西需要它时,它立即可用。

<link rel="preload" href="image.png" as="image">

rel="preload"除了优先级较高,还有两个优点:一是允许指定预加载资源的类型,二是允许onload事件的回调函数。下面是rel="preload"配合as属性,告诉浏览器预处理资源的类型,以便正确处理。

<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">

上面代码要求浏览器提前下载并缓存style.cssmain.js

as属性指定加载资源的类型,它的值一般有下面几种。

  • “script”
  • “style”
  • “image”
  • “media”
  • “document”

如果不指定as属性,或者它的值是浏览器不认识的,那么浏览器会以较低的优先级下载这个资源。

有时还需要type属性,进一步明确 MIME 类型。

<link rel="preload" href="sintel-short.mp4" as="video" type="video/mp4">

上面代码要求浏览器提前下载视频文件,并且说明这是 MP4 编码。

下面是预下载字体文件的例子。

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

注意,所有预下载的资源,只是下载到浏览器的缓存,并没有执行。如果希望资源预下载后立刻执行,可以参考下面的写法。

<link rel="preload" as="style" href="async_style.css" onload="this.rel='stylesheet'">

上面代码中,onload指定的回调函数会在脚本下载完成后执行,立即插入页面。

(2)<link rel="prefetch">

<link rel="prefetch">的使用场合是,如果后续的页面需要某个资源,并且希望预加载该资源,以便加速页面渲染。该指令不是强制性的,优先级较低,浏览器不一定会执行。这意味着,浏览器可以不下载该资源,比如连接速度很慢时。

<link rel="prefetch" href="https://www.example.com/">

(3)<link rel="preconnect">

<link rel="preconnect">要求浏览器提前与某个域名建立 TCP 连接。当你知道,很快就会请求该域名时,这会很有帮助。

<link rel="preconnect" href="https://www.example.com/">

(4)<link rel="dns-prefetch">

<link rel="dns-prefetch">要求浏览器提前执行某个域名的 DNS 解析。

<link rel="dns-prefetch" href="//example.com/">

(5)<link rel="prerender">

<link rel="prerender">要求浏览器加载某个网页,并且提前渲染它。用户点击指向该网页的链接时,就会立即呈现该页面。如果确定用户下一步会访问该页面,这会很有帮助。

<link rel="prerender" href="http://example.com/">

media 属性

media属性给出外部资源生效的媒介条件。

<link href="print.css" rel="stylesheet" media="print">
<link href="mobile.css" rel="stylesheet" media="screen and (max-width: 600px)">

上面代码中,打印时加载print.css,移动设备访问时(设备宽度小于600像素)加载mobile.css

下面是使用media属性实现条件加载的例子。

<link rel="preload" as="image" href="map.png" media="(max-width: 600px)">
<link rel="preload" as="script" href="map.js" media="(min-width: 601px)">

上面代码中,如果屏幕宽度在600像素以下,则只加载第一个资源,否则就加载第二个资源。

其他属性

<link>标签的其他属性如下。

  • crossorigin:加载外部资源的跨域设置。
  • href:外部资源的网址。
  • referrerpolicy:加载时Referer头信息字段的处理方法。
  • asrel="preload"rel="prefetch"时,设置外部资源的类型。
  • type:外部资源的 MIME 类型,目前仅用于rel="preload"rel="prefetch"的情况。
  • title:加载样式表时,用来标识样式表的名称。
  • sizes:用来声明图标文件的尺寸,比如加载苹果手机的图标文件。

<script>

<script>用于加载脚本代码,目前主要是加载 JavaScript 代码。

<script>
console.log('hello world');
</script>

上面代码嵌入网页,会立即执行。

<script>也可以加载外部脚本,src属性给出外部脚本的地址。

<script src="javascript.js"></script>

上面代码会加载javascript.js脚本文件,并执行。指定了 src 属性的script元素标签内不应该再有嵌入的脚本。

type属性给出脚本的类型,默认是 JavaScript 代码,所以可省略。完整的写法其实是下面这样。

<script type="text/javascript" src="javascript.js"></script>

type属性也可以设成module,表示这是一个 ES6 模块,不是传统脚本。

<script type="module" src="main.js"></script>

对于那些不支持 ES6 模块的浏览器,可以设置nomodule属性。支持 ES6 模块的浏览器,会不加载指定的脚本。这个属性通常与type="module"配合使用,作为老式浏览器的回退方案。

<script type="module" src="main.js"></script>
<script nomodule src="fallback.js"></script>

<script>还有下面一些其他属性,大部分跟 JavaScript 语言有关,可以参考相关的 JavaScript 教程。

  • async:该属性指定 JavaScript 代码为异步执行,不是造成阻塞效果,JavaScript 代码默认是同步执行。
  • defer:该属性指定 JavaScript 代码不是立即执行,而是页面解析完成后执行。
  • crossorigin:如果采用这个属性,就会采用跨域的方式加载外部脚本,即 HTTP 请求的头信息会加上origin字段。
  • integrity:给出外部脚本的哈希值,防止脚本被篡改。只有哈希值相符的外部脚本,才会执行。
  • nonce:一个密码随机数,由服务器在 HTTP 头信息里面给出,每次加载脚本都不一样。它相当于给出了内嵌脚本的白名单,只有在白名单内的脚本才能执行。
  • referrerpolicy:HTTP 请求的Referer字段的处理方法。

<noscript>

<noscript>标签用于浏览器不支持或关闭 JavaScript 时,所要显示的内容。用户关闭 JavaScript 可能是为了节省带宽,以延长手机电池寿命,或者为了防止追踪,保护隐私。

<noscript>
  您的浏览器不能执行 JavaScript 语言,页面无法正常显示。
</noscript>

上面这段代码,只有浏览器不能执行 JavaScript 代码时才会显示,否则就不会显示。

多媒体标签

除了图像,网页还可以放置视频和音频。

<video>

<video>标签是一个块级元素,用于放置视频。如果浏览器支持加载的视频格式,就会显示一个播放器,否则显示<video>内部的子元素。也可以将 <video> 标签用于音频内容。

<video src="example.mp4" controls>
  <p>你的浏览器不支持 HTML5 视频,请下载<a href="example.mp4">视频文件</a>。</p>
</video>

上面代码中,如果浏览器不支持该种格式的视频,就会显示<video>内部的文字提示。

<video>有以下属性。

  • src:视频文件的网址。
  • controls:播放器是否显示控制栏。该属性是布尔属性,不用赋值,只要写上属性名,就表示打开。如果不想使用浏览器默认的播放器,而想使用自定义播放器,就不要使用该属性。
  • width:视频播放器的宽度,单位像素。
  • height:视频播放器的高度,单位像素。
  • autoplay:视频是否自动播放,该属性为布尔属性。
  • loop:视频是否循环播放,该属性为布尔属性。
  • muted:是否默认静音,该属性为布尔属性。
  • poster:视频播放器的封面图片的 URL。
  • preload:视频播放之前,是否缓冲视频文件。这个属性仅适合没有设置autoplay的情况。它有三个值,分别是none(不缓冲)、metadata(仅仅缓冲视频文件的元数据)、auto(可以缓冲整个文件)。
  • playsinline:iPhone 的 Safari 浏览器播放视频时,会自动全屏,该属性可以禁止这种行为。该属性为布尔属性。
  • crossorigin:是否采用跨域的方式加载视频。它可以取两个值,分别是anonymous(跨域请求时,不发送用户凭证,主要是 Cookie),use-credentials(跨域时发送用户凭证)。
  • currentTime:指定当前播放位置(双精度浮点数,单位为秒)。如果尚未开始播放,则会从这个属性指定的位置开始播放。
  • duration:该属性只读,指示时间轴上的持续播放时间(总长度),值为双精度浮点数(单位为秒)。如果是流媒体,没有已知的结束时间,属性值为+Infinity

下面是一个例子。

<video width="400" height="400"
       autoplay loop muted
       poster="poster.png">
</video>

上面代码中,视频播放器的大小是 400 x 400,会自动播放和循环播放,并且静音,还带有封面图。这是网站首页背景视频的常见写法。

HTML 标准没有规定浏览器需要支持哪些视频格式,完全由浏览器厂商自己决定。为了避免浏览器不支持视频格式,可以使用<source>标签,放置同一个视频的多种格式。

<video controls>
  <source src="example.mp4" type="video/mp4">
  <source src="example.webm" type="video/webm">
  <p>你的浏览器不支持 HTML5 视频,请下载<a href="example.mp4">视频文件</a>。</p>
</video>

上面代码中,<source>标签的type属性的值是视频文件的 MIME 类型,上例指定了两种格式的视频文件:MP4 和 WebM。如果浏览器支持 MP4,就加载 MP4 格式的视频,不再往下执行了。如果不支持 MP4,就检查是否支持 WebM,如果还是不支持,则显示提示。

<audio>

<audio>标签是一个块级元素,用于放置音频,用法与<video>标签基本一致。

<audio controls>
  <source src="foo.mp3" type="audio/mp3">
  <source src="foo.ogg" type="audio/ogg">
  <p>你的浏览器不支持 HTML5 音频,请直接下载<a href="foo.mp3">音频文件</a>。</p>
</audio>

上面代码中,<audio>标签内部使用<source>标签,指定了两种音频格式:优先使用 MP3 格式,如果浏览器不支持则使用 Ogg 格式。如果浏览器不能播放音频,则提供下载链接。

<audio>标签的属性与<video>标签类似。

  • autoplay:是否自动播放,布尔属性。声明该属性,音频会尽快自动播放,不会等待整个音频文件下载完成。
  • controls:是否显示播放工具栏,布尔属性。如果不设置,浏览器不显示播放界面,通常用于背景音乐。
  • crossorigin:是否使用跨域方式请求。
  • loop:是否循环播放,布尔属性。
  • muted:是否静音,布尔属性。
  • preload:音频文件的缓冲设置。
    • 让开发者自行思考来示意浏览器使用何种加载方式以达到最好的用户体验。不同浏览器会有自己的默认值,规范建议的默认值为 metadata。
      • none: 示意用户可能不会播放该音频,或者服务器希望节省带宽;换句话说,该音频不会被缓存;
      • metadata: 示意即使用户可能不会播放该音频,但获取元数据 (例如音频长度) 还是有必要的。
      • auto: 示意用户可能会播放音频;换句话说,如果有必要,整个音频都将被加载,即使用户不期望使用。
      • 空字符串 : 等效于auto属性。
  • src:音频文件网址。

<track>

<track>标签用于指定视频的字幕,格式是 WebVTT (.vtt文件),放置在<video>标签内部。它是一个单独使用的标签,没有结束标签。

<video controls src="sample.mp4">
   <track label="英文" kind="subtitles" src="subtitles_en.vtt" srclang="en">
   <track label="中文" kind="subtitles" src="subtitles_cn.vtt" srclang="cn" default>
</video>

上面代码指定视频文件的英文字幕和中文字幕。

<track>标签有以下属性。

  • label:播放器显示的字幕名称,供用户选择。
  • kind:字幕的类型,默认是subtitles,表示将原始声音成翻译外国文字,比如英文视频提供中文字幕。另一个常见的值是captions,表示原始声音的文字描述,通常是视频原始使用的语言,比如英文视频提供英文字幕。
  • src:vtt 字幕文件的网址。
  • srclang:字幕的语言,必须是有效的语言代码。
  • default:是否默认打开,布尔属性。

<source>

<source>标签用于<picture><video><audio>的内部,用于指定一项外部资源。单标签是单独使用的,没有结束标签。

它有如下属性,具体示例请参见相应的容器标签。

  • type:指定外部资源的 MIME 类型。
  • src:指定源文件,用于<video><audio>
  • srcset:指定不同条件下加载的图像文件,用于<picture>
  • media:指定媒体查询表达式,用于<picture>
  • sizes:指定不同设备的显示大小,用于<picture>,必须跟srcset搭配使用。

<embed>

<embed>标签用于嵌入外部内容,这个外部内容通常由浏览器插件负责控制。由于浏览器的默认插件都不一致,很可能不是所有浏览器的用户都能访问这部分内容,建议谨慎使用。

下面是嵌入视频播放器的例子。

<embed type="video/webm"
       src="/media/examples/flower.mp4"
       width="250"
       height="200">

上面代码嵌入的视频,将由浏览器插件负责控制。如果浏览器没有安装 MP4 插件,视频就无法播放。

<embed>标签具有如下的通用属性。

  • height:显示高度,单位为像素,不允许百分比。
  • width:显示宽度,单位为像素,不允许百分比。
  • src:嵌入的资源的 URL。
  • type:嵌入资源的 MIME 类型。

浏览器通过type属性得到嵌入资源的 MIME 类型,一旦该种类型已经被某个插件注册了,就会启动该插件,负责处理嵌入的资源。

下面是 QuickTime 插件播放 MOV 视频文件的例子。

<embed type="video/quicktime" src="movie.mov" width="640" height="480">

下面是启动 Flash 插件的例子。

<embed src="whoosh.swf" quality="medium"
       bgcolor="#ffffff" width="550" height="400"
       name="whoosh" align="middle" allowScriptAccess="sameDomain"
       allowFullScreen="false" type="application/x-shockwave-flash"
       pluginspage="http://www.macromedia.com/go/getflashplayer">

上面代码中,如果浏览器没有安装 Flash 插件,就会提示去pluginspage属性指定的网址下载。

<object><param>

<object>标签作用跟<embed>相似,也是插入外部资源,由浏览器插件处理。它可以视为<embed>的替代品,有标准化行为,只限于插入少数几种通用资源,没有历史遗留问题,因此更推荐使用。

下面是插入 PDF 文件的例子。

<object type="application/pdf"
    data="/media/examples/In-CC0.pdf"
    width="250"
    height="200">
</object>

上面代码中,如果浏览器安装了 PDF 插件,就会在网页显示 PDF 浏览窗口。

<object>具有如下的通用属性。

  • data:嵌入的资源的 URL。
  • form:当前网页中相关联表单的id属性(如果有的话)。
  • height:资源的显示高度,单位为像素,不能使用百分比。
  • width:资源的显示宽度,单位为像素,不能使用百分比。
  • type:资源的 MIME 类型。
  • typemustmatch:布尔属性,表示data属性与type属性是否必须匹配。

下面是插入 Flash 影片的例子。

<object data="movie.swf"
  type="application/x-shockwave-flash"></object>

<object>标签是一个容器元素,内部可以使用<param>标签,给出插件所需要的运行参数。

<object data="movie.swf" type="application/x-shockwave-flash">
  <param name="foo" value="bar">
</object>

iframe

<iframe>标签用于在网页里面嵌入其他网页。

基本用法

<iframe>标签生成一个指定区域,在该区域中嵌入其他网页。它是一个容器元素,如果浏览器不支持<iframe>,就会显示内部的子元素。

<iframe src="https://www.example.com"
        width="100%" height="500" frameborder="0"
        allowfullscreen sandbox>
  <p><a href="https://www.example.com">点击打开嵌入页面</a></p>
</iframe>

上面的代码在当前网页嵌入https://www.example.com,显示区域的宽度是100%,高度是500像素。如果当前浏览器不支持<iframe>,则会显示一个链接,让用户点击。

浏览器普遍支持<iframe>,所以内部的子元素可以不写。

<iframe>的属性如下。

  • allowfullscreen:允许嵌入的网页全屏显示,需要全屏 API 的支持。
  • frameborder:是否绘制边框,0为不绘制,1为绘制(默认值)。建议尽量少用这个属性,而是在 CSS 里面设置样式。
  • src:嵌入的网页的 URL。
  • width:显示区域的宽度。
  • height:显示区域的高度。
  • sandbox:设置嵌入的网页的权限。
  • importance:浏览器下载嵌入的网页的优先级,可以设置三个值。high表示高优先级,low表示低优先级,auto表示由浏览器自行决定。
  • name:内嵌窗口的名称,可以用于<a><form><base>target属性。
  • referrerpolicy:请求嵌入网页时,HTTP 请求的Referer字段的设置。参见<a>标签的介绍。

sandbox 属性

嵌入的网页默认具有正常权限,比如执行脚本、提交表单、弹出窗口等。如果嵌入的网页是其他网站的页面,你不了解对方会执行什么操作,因此就存在安全风险。为了限制<iframe>的风险,HTML 提供了sandbox属性,允许设置嵌入的网页的权限,等同于提供了一个隔离层,即“沙箱”。

sandbox可以当作布尔属性使用,表示打开所有限制。

<iframe src="https://www.example.com" sandbox>
</iframe>

sandbox属性可以设置具体的值,表示逐项打开限制。未设置某一项,就表示不具有该权限。

  • allow-forms:允许提交表单。
  • allow-modals:允许提示框,即允许执行window.alert()等会产生弹出提示框的 JavaScript 方法。
  • allow-popups:允许嵌入的网页使用window.open()方法弹出窗口。
  • allow-popups-to-escape-sandbox:允许弹出窗口不受沙箱的限制。
  • allow-orientation-lock:允许嵌入的网页用脚本锁定屏幕的方向,即横屏或竖屏。
  • allow-pointer-lock:允许嵌入的网页使用 Pointer Lock API,锁定鼠标的移动。
  • allow-presentation:允许嵌入的网页使用 Presentation API。
  • allow-same-origin:不打开该项限制,将使得所有加载的网页都视为跨域。
  • allow-scripts:允许嵌入的网页运行脚本(但不创建弹出窗口)。
  • allow-storage-access-by-user-activationsandbox属性同时设置了这个值和allow-same-origin的情况下,允许<iframe>嵌入的第三方网页通过用户发起document.requestStorageAccess()请求,经由 Storage Access API 访问父窗口的 Cookie。
  • allow-top-navigation:允许嵌入的网页对顶级窗口进行导航。
  • allow-top-navigation-by-user-activation:允许嵌入的网页对顶级窗口进行导航,但必须由用户激活。
  • allow-downloads-without-user-activation:允许在没有用户激活的情况下,嵌入的网页启动下载。

注意,不要同时设置allow-scriptsallow-same-origin属性,这将使得嵌入的网页可以改变或删除sandbox属性。

loading 属性

<iframe>指定的网页会立即加载,有时这不是希望的行为。<iframe>滚动进入视口以后再加载,这样会比较节省带宽。

loading属性可以触发<iframe>网页的懒加载。该属性可以取以下三个值。

  • auto:浏览器的默认行为,与不使用loading属性效果相同。
  • lazy<iframe>的懒加载,即将滚动进入视口时开始加载。
  • eager:立即加载资源,无论在页面上的位置如何。
<iframe src="https://example.com" loading="lazy"></iframe>

上面代码会启用<iframe>的懒加载。

有一点需要注意,如果<iframe>是隐藏的,则loading属性无效,将会立即加载。只要满足以下任一个条件,Chrome 浏览器就会认为<iframe>是隐藏的。

  • <iframe>的宽度和高度为4像素或更小。
  • 样式设为display: nonevisibility: hidden
  • 使用定位坐标为负X或负Y,将<iframe>放置在屏幕外。

局限

创建比一般的 DOM 元素慢了 1-2 个数量级

iframe 的创建比其它包括 scripts 和 css 的 DOM 元素的创建慢了 1-2 个数量级,使用 iframe 的页面一般不会包含太多 iframe,所以创建 DOM 节点所花费的时间不会占很大的比重。但带来一些其它的问题:onload 事件以及连接池(connection pool)

阻塞页面加载

及时触发 window 的 onload 事件是非常重要的。onload 事件触发使浏览器的 “忙” 指示器停止,告诉用户当前网页已经加载完毕。当 onload 事件加载延迟后,它给用户的感觉就是这个网页非常慢。

window 的 onload 事件需要在所有 iframe 加载完毕后(包含里面的元素)才会触发。在 Safari 和 Chrome 里,通过 JavaScript 动态设置 iframe 的 SRC 可以避免这种阻塞情况

唯一的连接池

浏览器只能开少量的连接到 web 服务器。比较老的浏览器,包含 Internet Explorer 6 & 7 和 Firefox 2,只能对一个域名(hostname)同时打开两个连接。这个数量的限制在新版本的浏览器中有所提高。Safari 3+ 和 Opera 9+ 可同时对一个域名打开 4 个连接,Chrome 1+, IE 8 以及 Firefox 3 可以同时打开 6 个

绝大部分浏览器,主页面和其中的 iframe 是共享这些连接的。这意味着 iframe 在加载资源时可能用光了所有的可用连接,从而阻塞了主页面资源的加载。如果 iframe 中的内容比主页面的内容更重要,这当然是很好的。但通常情况下,iframe 里的内容是没有主页面的内容重要的。这时 iframe 中用光了可用的连接就是不值得的了。一种解决办法是,在主页面上重要的元素加载完毕后,再动态设置 iframe 的 SRC。

不利于 SEO

搜索引擎的检索程序无法解读 iframe。另外,iframe 本身不是动态语言,样式和脚本都需要额外导入。综上,iframe 应谨慎使用。

使用场景

1:典型系统结构,左侧是功能树,右侧就是一些常见的table或者表单之类的。为了每一个功能,单独分离出来,采用iframe。
2:ajax上传文件。
3:加载别的网站内容,例如google广告,网站流量分析。

4: 在上传图片时,不用flash实现无刷新。

5: 跨域访问的时候可以用到iframe,使用iframe请求不同域名下的资源。

表格标签

表格(table)以行(row)和列(column)的形式展示数据。

<table><caption>

<table>是一个块级容器标签,所有表格内容都要放在这个标签里面。

<table>
  ... ...
</table>

<caption>总是<table>里面的第一个子元素,表示表格的标题。该元素是可选的。

<table>
  <caption>示例表格</caption>
</table>

<thead><tbody><tfoot>

<thead><tbody><tfoot>都是块级容器元素,且都是<table>的一级子元素,分别表示表头、表体和表尾。

<table>
  <thead>... ...</thead>
  <tbody>... ...</tbody>
  <tfoot>... ...</tfoot>
</table>

这三个元素都是可选的。如果使用了<thead>,那么<tbody><tfoot>一定在<thead>的后面。如果使用了<tbody>,那么<tfoot>一定在<tbody>后面。即书写顺序可以任意,浏览器显示按照 thead、tbody、tfoot 的顺序依次来显示内容。

大型表格内部可以使用多个<tbody>,表示连续的多个部分。

当表格非常大内容非常多的时候,用 thead、tbody、tfoot 标签的话,那么数据可以边获取边显示。如果不写,则必须等表格的内容全部从服务器获取完成才能显示出来。

<colgroup><col>

<colgroup><table>的一级子元素,用来包含一组列的定义。<col><colgroup>的子元素,用来定义表格的一列。

<table>
  <colgroup>
    <col>
    <col>
    <col>
  </colgroup>
</table>

上面代码表明表格有3列。

<col>不仅是一个单独使用的标签,没有结束标志,而且还是一个空元素,没有子元素。它的主要作用,除了申明表格结构,还可以为表格附加样式。

<table>
  <colgroup>
    <col class="c1">
    <col class="c2">
    <col class="c3">
  </colgroup>
  <tr>
    <td>1</td>
    <td>2</td>
    <td>3</td>
  </tr>
</table>

上面代码中,<colgroup>声明表格有三列,每一列有自己的 class,可以使用 CSS 针对每个 class 设定样式,会对整个表格生效。

<col>有一个span属性,值为正整数,默认为1。如果大于1,就表示该列的宽度包含连续的多列。

<table>
  <colgroup>
    <col>
    <col span="2">
    <col>
  </colgroup>
</table>

上面代码中,表格的表头定义了3列,实际数据有4列。表头的第2列会连续跨2列。

<tr>

<tr>标签表示表格的一行(table row)。如果表格有<thead><tbody><tfoot>,那么<tr>就放在这些容器元素之中,否则直接放在<table>的下一级。

<table>
  <tr>...</tr>
  <tr>...</tr>
  <tr>...</tr>
</table>

上面代码表示表格共有3行。

<th><td>

<th><td>都用来定义表格的单元格。其中,<th>是标题单元格,<td>是数据单元格。

<table>
  <tr>
    <th>学号</th><th>姓名</th>
  </tr>
  <tr>
    <td>001</td><td>张三</td>
  </tr>
  <tr>
    <td>002</td><td>李四</td>
  </tr>
</table>

上面代码中,表格一共有三行。第一行是标题行,所以使用<th>;第二行和第三行是数据行,所以使用<td>

(1)colspan属性,rowspan属性

单元格会有跨越多行或多列的情况,这要通过colspan属性和rowspan属性设置,前者表示单元格跨越的栏数,后者表示单元格跨越的行数。它们的值都是一个非负整数,默认为1。

<table>
  <tr>
    <td colspan="2">A</td><td>B</td>
  </tr>
  <tr>
    <td>A</td><td>B</td><td>C</td>
  </tr>
</table>

上面代码中,第一行的第一个单元格会跨两列。

(2)headers属性

如果表格很大,单元格很多,源码里面会看不清,哪个单元格对应哪个表头,这时就可以使用headers属性。

<table>
  <tr>
    <th id="no">学号</th><th id="names">姓名</th>
  </tr>
  <tr>
    <td headers="no">001</td><td headers="names">张三</td>
  </tr>
  <tr>
    <td headers="no">002</td><td headers="names">李四</td>
  </tr>
</table>

上面代码中,标题栏的<th>设置了id属性,后面的<td>单元格的headers属性就对应这些id属性的值,因此就能看出来这些单元格对应哪个标题栏。

headers属性的值总是对应<th>标签的id属性的值。由于一个单元格可以对应多个标题栏(跨行的情况),所以headers属性可以是一个空格分隔的字符串,对应多个id属性的值。

(3)scope属性

scope属性只有<th>标签支持,一般不在<td>标签使用,表示该<th>单元格到底是栏的标题,还是列的标题。

<table>
  <tr>
    <th scope="col">姓名</th>
    <th scope="col">学号</th>
    <th scope="col">性别</th>
  </tr>
  <tr>
    <th scope="row">张三</th>
    <td>001</td>
    <td>男</td>
  </tr>
  <tr>
    <th scope="row">李四</th>
    <td>002</td>
    <td>男</td>
  </tr>
</table>

上面代码中,第一行的标题栏都是列标题,所以<th>scope属性为col,第二行和第三行的第一列是行标题,所以<th>标签的scope属性为row

scope属性可以取下面这些值。

  • row:该行的所有单元格,都与该标题单元格相关。
  • col:该列的所有单元格,都与该标题单元格相关。
  • rowgroup:多行组成的一个行组的所有单元格,都与该标题单元格相关,可以与rowspan属性配合使用。
  • colgroup:多列组成的一个列组的所有单元格,都与该标题单元格相关,可以与colspan属性配合使用。
  • auto:默认值,表示由浏览器自行决定。

下面是一个colgroup属性和rowgroup属性的例子。

<table>
  <thead>
    <tr>
      <th scope="col">海报名称</th>
      <th scope="col">颜色</th>
      <th colspan="3" scope="colgroup">尺寸</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="3" scope="rowgroup">Zodiac</th>
      <th scope="row">Full color</th>
      <td>A2</td>
      <td>A3</td>
      <td>A4</td>
    </tr>
    <tr>
      <th scope="row">Black and white</th>
      <td>A1</td>
      <td>A2</td>
      <td>A3</td>
    </tr>
    <tr>
      <th scope="row">Sepia</th>
      <td>A3</td>
      <td>A4</td>
      <td>A5</td>
    </tr>
  </tbody>
</table>

上面的例子中,列标题“尺寸”的scope属性为colgroup,表示这个标题单元格对应多列(本例为3列);行标题的scope属性为rowgroup,表示这个标题单元格对应多行(本例为3行)。

表单标签

表单(form)是用户输入信息与网页互动的一种形式。大多数情况下,用户提交的信息会发给服务器,比如网站的搜索栏就是表单。

表单由一种或多种的小部件组成,比如输入框、按钮、单选框或复选框。这些小部件称为控件(controls)。

<form>

简介

<form>标签用来定义一个表单,所有表单内容放到这个容器元素之中。

<form>
  <!-- 各种表单控件-->
</form>

上面代码就是表单的基本形式。

下面是一个比较常见的例子。

<form action="https://example.com/api" method="post">
  <label for="POST-name">用户名:</label>
  <input id="POST-name" type="text" name="user">
  <input type="submit" value="提交">
</form>

上面代码就是一个表单,一共包含三个控件:一个<label>标签,一个文本输入框,一个提交按钮。其中,文本输入框的name属性是user,表示将向服务器发送一个键名为user的键值对,键值就是这个控件的value属性,等于用户输入的值。

用户在文本输入框里面,输入用户名,比如foobar,然后点击提交按钮,浏览器就会向服务器https://example.com/api发送一个 POST 请求,发送user=foobar这样一段数据。

<form>有以下属性。

  • accept-charset:服务器接受的字符编码列表,使用空格分隔,默认与网页编码相同。
  • action:服务器接收数据的 URL。指定表单数据的处理程序。
  • autocomplete:如果用户没有填写某个控件,浏览器是否可以自动填写该值。它的可能取值分别为off(不自动填写)和on(自动填写)。
  • method:提交数据的 HTTP 方法,可能的值有post(表单数据作为 HTTP 数据体发送),get(表单数据作为 URL 的查询字符串发送),dialog(表单位于<dialog>内部使用)。
  • enctype:当method属性等于post时,该属性指定提交给服务器的 MIME 类型。可能的值为application/x-www-form-urlencoded(默认值),multipart/form-data(文件上传的情况),text/plain(用于调试)。
  • name:表单的名称,应该在网页中是唯一的。注意,如果一个控件没有设置name属性,那么这个控件的值就不会作为键值对,向服务器发送。
  • novalidate:布尔属性,表单提交时是否取消验证。
  • target:在哪个窗口展示服务器返回的数据,可能的值有_self(当前窗口),_blank(新建窗口),_parent(父窗口),_top(顶层窗口),<iframe>标签的name属性(即表单返回结果展示在<iframe>窗口)。

enctype 属性

<form>表单的enctype属性,指定了采用 POST 方法提交数据时,浏览器给出的数据的 MIME 类型。该属性可以取以下值。

(1)application/x-www-form-urlencoded

application/x-www-form-urlencoded是默认类型,控件名和控件值都要转义(空格转为+号,非数字和非字母转为%HH的形式,换行转为CR LF),控件名和控件值之间用=分隔。控件按照出现顺序排列,控件之间用&分隔。

(2)multipart/form-data

multipart/form-data主要用于文件上传。这个类型上传大文件时,会将文件分成多块传送,每一块的 HTTP 头信息都有Content-Disposition属性,值为form-data,以及一个name属性,值为控件名。

Content-Disposition: form-data; name="mycontrol"

下面是上传文件的表单。

<form action="https://example.com/api"
      enctype="multipart/form-data"
      method="post">
  用户名:<input type="text" name="submit-name"><br>
  文件:<input type="file" name="files"><br>
  <input type="submit" value="上传"> <input type="reset" value="清除">
</form>

上面代码中,输入用户名Larry,选中一个file1.txt文件,然后点击“上传”。浏览器发送的实际数据如下。

Content-Type: multipart/form-data; boundary=--AaB03x

--AaB03x
Content-Disposition: form-data; name="submit-name"

Larry
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain

... contents of file1.txt ...
--AaB03x--

上面代码中,浏览器将这个表单发成多个数据块。最上面使用Content-Type字段告诉服务器,数据格式是multipart/form-data(即多个数据块),每个数据块的分隔标志是--AaB03x。每个数据块的第一行是Content-Disposition,其中的name字段表示这个数据块的控件名,数据体则是该控件的数据值,比如第一个数据块的name属性是submit-name控件,数据体是该控件的值Larry。第二个数据块是控件files,由于该控件是上传文件,所以还要用filename属性给出文件名file1.txt,数据体是file1.txt的内容。

<fieldset><legend>

<fieldset>标签是一个块级容器标签,表示控件的集合,用于将一组相关控件组合成一组。

<form>
  <fieldset>
    <p>年龄:<input type="text" name="age"></p>
    <p>性别:<input type="text" name="gender"></p>
  </fieldset>
</form>

上面代码中,两个输入框是一组,它们的外面会显示一个方框。

<fieldset>有以下属性。

  • disabled:布尔属性,一旦设置会使得<fieldset>内部包含的控件都不可用,都变成灰色状态。
  • form:指定控件组所属的<form>,它的值等于<form>id属性。
  • name:该控件组的名称。

<legend>标签用来设置<fieldset>控件组的标题,通常是<fieldset>内部的第一个元素,会嵌入显示在控件组的上边框里面。

<fieldset>
  <legend>学生情况登记</legend>
  <p>年龄:<input type="text" name="age"></p>
  <p>性别:<input type="text" name="gender"></p>
</fieldset>

上面代码中,这个控件组的标题会,嵌入显示在<fieldset>的上边框。

<label>

<label>标签是一个行内元素,提供控件的文字说明,帮助用户理解控件的目的。

<label for="user">用户名:</label>
<input type="text" name="user" id="user">

上面代码中,输入框前面会有文字说明“用户名:”。

<label>的一大优势是增加了控件的可用性。有些控件比较小(比如单选框),不容易点击,那么点击对应的<label>标签,也能选中该控件。点击<label>,就相当于控件本身的click事件。

<label>for属性关联相对应的控件,它的值是对应控件的id属性。所以,控件最好设置id属性。

控件也可以放在<label>之中,这时不需要for属性和id属性。

<label>用户名:
  <input type="text" name="user">
</label>

<label>的属性如下。

  • for:关联控件的id属性。
  • form:关联表单的id属性。设置了该属性后,<label>可以放置在页面的任何位置,否则只能放在<form>内部。

一个控件可以有多个关联的<label>标签。

<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<label for="username"><abbr title="required">*</abbr></label>

上面代码中,<input>有两个关联的<label>

<input>

简介

<input>标签是一个行内元素,用来接收用户的输入。它是一个单独使用的标签,没有结束标志。

它有多种类型,取决于type属性的值,默认值是text,表示一个输入框。

<input>
<!-- 等同于 -->
<input type="text">

上面代码会生成一个单行的输入框,用户可以在里面输入文本。

<input>的属性非常多,有些属性是某个类型专用的,放在下文的“类型”部分介绍。这里介绍一些所有类型的共同属性。

  • autofocus:布尔属性,是否在页面加载时自动获得焦点。
  • disabled:布尔属性,是否禁用该控件。一旦设置,该控件将变灰,用户可以看到,但是无法操作。
  • form:关联表单的id属性。设置了该属性后,控件可以放置在页面的任何位置,否则只能放在<form>内部。
  • list:关联的<datalist>id属性,设置该控件相关的数据列表。
  • name:控件的名称,主要用于向服务器提交数据时,控件键值对的键名。注意,只有设置了name属性的控件,才会向服务器提交,不设置就不会提交。
  • readonly:布尔属性,是否为只读。
  • required:布尔属性,是否为必填。
  • type:控件类型。
  • value:控件的值。

类型

type属性决定了<input>的形式。该属性可以取以下值。

(1)text

type="text"是普通的文本输入框,用来输入单行文本。如果用户输入换行符,换行符会自动从输入中删除。

<input type="text" id="name" name="name" required
       minlength="4" maxlength="8" size="10">

text输入框有以下配套属性。

  • maxlength:可以输入的最大字符数,值为一个非负整数。
  • minlength:可以输入的最小字符数,值为一个非负整数,且必须小于maxlength
  • pattern:用户输入必须匹配的正则表达式,比如要求用户输入4个~8个英文字符,可以写成pattern="[a-z]{4,8}"。如果用户输入不符合要求,浏览器会弹出提示,不会提交表单。
  • placeholder:输入字段为空时,用于提示的示例值。只要用户没有任何字符,该提示就会出现,否则会消失。
  • readonly:布尔属性,表示该输入框是只读的,用户只能看,不能输入。
  • size:表示输入框的显示长度有多少个字符宽,它的值是一个正整数,默认等于20。超过这个数字的字符,必须移动光标才能看到。
  • spellcheck:是否对用户输入启用拼写检查,可能的值为truefalse

(2)search

type="search"是一个用于搜索的文本输入框,输入文本中的换行会被自动去除,基本等同于type="text"。某些浏览器会在输入的时候,在输入框的尾部显示一个删除按钮,点击就会删除所有输入,让用户从头开始输入。

下面是一个例子。

<form>
  <input type="search" id="mySearch" name="q"
    placeholder="输入搜索词……" required>
  <input type="submit" value="搜索">
</form>

(3)button

type="button"是没有默认行为的按钮,通常脚本指定click事件的监听函数来使用。

<input type="button" value="点击">

建议尽量不使用这个类型,而使用<button>标签代替,一则语义更清晰,二则<button>标签内部可以插入图片或其他 HTML 代码。

(4)submit

type="submit"是表单的提交按钮。用户点击这个按钮,就会把表单提交给服务器。

<input type="submit" value="提交">

如果不指定value属性,浏览器会在提交按钮上显示默认的文字,通常是Submit

该类型有以下配套属性,用来覆盖<form>标签的相应设置。

  • formaction:提交表单数据的服务器 URL。
  • formenctype:表单数据的编码类型。
  • formmethod:提交表单使用的 HTTP 方法(getpost)。
  • formnovalidate:一个布尔值,表示数据提交给服务器之前,是否要忽略表单验证。
  • formtarget:收到服务器返回的数据后,在哪一个窗口显示。

(5)image

type="image"表示将一个图像文件作为提交按钮,行为和用法与type="submit"完全一致。

<input type="image" alt="登陆" src="login-button.png">

上面代码中,图像文件是一个可以点击的按钮,点击后会提交数据到服务器。

该类型有以下配套属性。

  • alt:图像无法加载时显示的替代字符串。
  • src:加载的图像 URL。
  • height:图像的显示高度,单位为像素。
  • width:图像的显示宽度,单位为像素。
  • formaction:提交表单数据的服务器 URL。
  • formenctype:表单数据的编码类型。
  • formmethod:提交表单使用的 HTTP 方法(getpost)。
  • formnovalidate:一个布尔值,表示数据提交给服务器之前,是否要忽略表单验证。
  • formtarget:收到服务器返回的数据后,在哪一个窗口显示。

用户点击图像按钮提交时,会额外提交两个参数xy到服务器,表示鼠标的点击位置,比如x=52&y=55x是横坐标,y是纵坐标,都以图像左上角作为原点(0, 0)。如果图像按钮设置了name属性,比如name="position",那么将以该值作为坐标的前缀,比如position.x=52&position.y=55。这个功能通常用来地图类型的操作,让服务器知道用户点击了地图的哪个部分。

(6)reset

type="reset"是一个重置按钮,用户点击以后,所有表格控件重置为初始值。

<input type="reset" value="重置">

如果不设置value属性,浏览器会在按钮上面加上默认文字,通常是Reset

这个控件用处不大,用户点错了还会使得所有已经输入的值都被重置,建议不要使用。

(7)checkbox

type="checkbox"是复选框,允许选择或取消选择该选项。

<input type="checkbox" id="agreement" name="agreement" checked>
<label for="agreement">是否同意</label>

上面代码会在文字前面,显示一个可以点击的选择框,点击可以选中,再次点击可以取消。上面代码中,checked属性表示默认选中。

value属性的默认值是on。也就是说,如果没有设置value属性,以上例来说,选中复选框时,会提交agreement=on。如果没有选中,提交时不会有该项。

多个相关的复选框,可以放在<fieldset>里面。所有关联的复选框应该拥有相同的 name 属性。

<fieldset>
  <legend>你的兴趣</legend>
  <div>
    <input type="checkbox" id="coding" name="interest" value="coding">
    <label for="coding">编码</label>
  </div>
  <div>
    <input type="checkbox" id="music" name="interest" value="music">
    <label for="music">音乐</label>
  </div>
</fieldset>

上面代码中,如果用户同时选中两个复选框,提交的时候就会有两个name属性,比如interest=coding&interest=music

(8)radio

type="radio"是单选框,表示一组选择之中,只能选中一项。单选框通常为一个小圆圈,选中时会被填充或突出显示。

<fieldset>
  <legend>性别</legend>
  <div>
    <input type="radio" id="male" name="gender" value="male">
    <label for="male">男</label>
  </div>
  <div>
    <input type="radio" id="female" name="gender" value="female">
    <label for="female">女</label>
  </div>
</fieldset>

上面代码中,性别只能在两个选项之中,选择一项。

注意,多个单选框的name属性的值,应该都是一致的。提交到服务器的就是选中的那个值。

该类型的配套属性如下。

  • checked:布尔属性,表示是否默认选中当前项。
  • value:用户选中该项时,提交到服务器的值,默认为on

(9)email

type="email"是一个只能输入电子邮箱的文本输入框。表单提交之前,浏览器会自动验证是否符合电子邮箱的格式,如果不符合就会显示提示,无法提交到服务器。

<input type="email" pattern=".+@foobar.com" size="30" required>

上面代码会生成一个必填的文本框,只能输入后缀为foobar.com的邮箱地址。

该类型有一个multiple的布尔属性,一旦设置,就表示该输入框可以输入多个逗号分隔的电子邮箱。

<input id="emailAddress" type="email" multiple required>

注意,如果同时设置了multiple属性和required属性,零个电子邮箱是允许的,也就是该输入框允许为空。

该类型的配套属性如下。

  • maxlength:可以输入的最大字符数。
  • minlength:可以输入的最少字符数。
  • multiple:布尔属性,是否允许输入多个以逗号分隔的电子邮箱。
  • pattern:输入必须匹配的正则表达式。
  • placeholder:输入为空时的显示文本。
  • readonly:布尔属性,该输入框是否只读。
  • size:一个非负整数,表示输入框的显示长度为多少个字符。
  • spellcheck:是否对输入内容启用拼写检查,可能的值为truefalse

该类型还可以搭配<datalist>标签,提供输入的备选项。

<input type="email" size="40" list="defaultEmails">

<datalist id="defaultEmails">
  <option value="jbond007@mi6.defence.gov.uk">
  <option value="jbourne@unknown.net">
  <option value="nfury@shield.org">
  <option value="tony@starkindustries.com">
  <option value="hulk@grrrrrrrr.arg">
</datalist>

上面代码中,输入焦点进入输入框以后,会显示一个下拉列表,里面有五个参考项,供用户参考。

(10)password

type="password"是一个密码输入框。用户的输入会被遮挡,字符通常显示星号(*)或点(·)。如果站点不安全,会警告用户。

<input type="password" id="pass" name="password"
           minlength="8" required>

浏览器对该类型输入框的显示,会有所差异。一种常见的处理方法是,用户每输入一个字符,先在输入框里面显示一秒钟,然后再遮挡该字符。

如果用户输入内容包含换行符(U+000A)和回车符(U+000D),浏览器会自动将这两个字符过滤掉。

该类型的配套属性如下。

  • maxlength:可以输入的最大字符数。
  • minlength:可以输入的最少字符数。
  • pattern:输入必须匹配的正则表达式。
  • placeholder:输入为空时的显示文本。
  • readonly:布尔属性,该输入框是否只读。
  • size:一个非负整数,表示输入框的显示长度为多少个字符。
  • autocomplete:是否允许自动填充,可能的值有on(浏览器可以根据用户先前的填表情况对此字段自动填值)、off(用户必须手动填值,或者该页面提供了自己的自动补全方法。浏览器不对此字段自动填充)、current-password(填入当前网站保存的密码)、new-password(自动生成一个随机密码)。
  • inputmode:允许用户输入的数据类型,可能的值有none(不使用系统输入法)、text(标准文本输入)、decimal(数字,包含小数)、numeric(数字0-9)等。

(11)file

type="file"是一个文件选择框,允许用户选择一个或多个文件,常用于文件上传功能。

<input type="file"
       id="avatar" name="avatar"
       accept="image/png, image/jpeg">

该类型有以下属性。

  • accept:允许选择的文件类型,使用逗号分隔,可以使用 MIME 类型(比如image/jpeg),也可以使用后缀名(比如.doc),还可以使用audio/*(任何音频文件)、video/*(任何视频文件)、image/*(任何图像文件)等表示法。
  • capture:用于捕获图像或视频数据的源,可能的值有user(面向用户的摄像头或麦克风),environment(外接的摄像头或麦克风)。
  • multiple:布尔属性,是否允许用户选择多个文件。

(12)hidden

type="hidden"是一个不显示在页面的控件,用户无法输入它的值,主要用来向服务器传递一些隐藏信息。比如,CSRF 攻击会伪造表单数据,那么使用这个控件,可以为每个表单生成一个独一无二的隐藏编号,防止伪造表单提交。

<input id="prodId" name="prodId" type="hidden" value="xm234jq">

上面这个控件,页面上是看不见的。用户提交表单的时候,浏览器会将prodId=xm234jq发给服务器。

(13)number

type="number"是一个数字输入框,只能输入数字。浏览器通常会在输入框的最右侧,显示一个可以点击的上下箭头,点击向上箭头,数字会递增,点击向下箭头,数字会递减。

<input type="number" id="tentacles" name="tentacles"
       min="10" max="100">

上面代码指定数字输入框,最小可以输入10,最大可以输入100。

该类型可以接受任何数值,包括小数和整数。可以通过step属性,限定只接受整数。

该类型有以下配套属性。

  • max:允许输入的最大数值。
  • min:允许输入的最小数值。
  • placeholder:用户输入为空时,显示的示例值。
  • readonly:布尔属性,表示该控件是否为只读。
  • step:点击向上和向下箭头时,数值每次递减的步长值。如果用户输入的值,不符合步长值的设定,浏览器会自动四舍五入到最近似的值。默认的步长值是1,如果初始的value属性设为1.5,那么点击向上箭头得到2.5,点击向下箭头得到0.5

(14)range

type="range"是一个滑块,用户拖动滑块,选择给定范围之中的一个数值。因为拖动产生的值是不精确的,如果需要精确数值,不建议使用这个控件。常见的例子是调节音量。

<input type="range" id="start" name="volume"
         min="0" max="11">

上面代码会产生一个最小值为0、最大值为11的滑块区域。用户拖动滑块,选择想要的音量。

该类型的配套属性如下,用法与type="number"一致。

  • max:允许的最大值,默认为100。
  • min:允许的最小值,默认为0。
  • step:步长值,默认为1。

value属性的初始值就是滑块的默认位置。如果没有设置value属性,滑块默认就会停在最大值和最小值中间。如果max属性、min属性、value属性都没有设置,那么value属性为50。

该类型与<datalist>标签配合使用,可以在滑动区域产生刻度。

<input type="range" list="tickmarks">

<datalist id="tickmarks">
  <option value="0" label="0%">
  <option value="10">
  <option value="20">
  <option value="30">
  <option value="40">
  <option value="50" label="50%">
  <option value="60">
  <option value="70">
  <option value="80">
  <option value="90">
  <option value="100" label="100%">
</datalist>

上面代码会在0~100之间产生11个刻度。其中,0%50%100%三个位置会有文字提示,不过浏览器很可能不支持。

注意,浏览器生成的都是水平滑块。如果想要生成垂直滑块,可以使用 CSS 改变滑块区域的方向。

(15)url

type="url"是一个只能输入网址的文本框。提交表单之前,浏览器会自动检查网址格式是否正确,如果不正确,就会无法提交。

<input type="url" name="url" id="url"
       placeholder="https://example.com"
       pattern="https://.*" size="30"
       required>

上面代码的pattern属性指定输入的网址只能使用 HTTPS 协议。

注意,该类型规定,不带有协议的网址是无效的,比如foo.com是无效的,http://foo.com是有效的。

该类型的配套属性如下。

  • maxlength:允许的最大字符数。
  • minlength:允许的最少字符串。
  • pattern:输入内容必须匹配的正则表达式。
  • placeholder:输入为空时显示的示例文本。
  • readonly:布尔属性,表示该控件的内容是否只读。
  • size:一个非负整数,表示该输入框显示宽度为多少个字符。
  • spellcheck:是否启动拼写检查,可能的值为true(启用)和false(不启用)。

该类型与<datalist>标签搭配使用,可以形成下拉列表供用户选择。随着用户不断键入,会缩小显示范围,只显示匹配的备选项。

<input id="myURL" name="myURL" type="url"
       list="defaultURLs">

<datalist id="defaultURLs">
  <option value="https://developer.mozilla.org/" label="MDN Web Docs">
  <option value="http://www.google.com/" label="Google">
  <option value="http://www.microsoft.com/" label="Microsoft">
  <option value="https://www.mozilla.org/" label="Mozilla">
  <option value="http://w3.org/" label="W3C">
</datalist>

上面代码中,<option>label属性表示文本标签,显示在备选下拉框的右侧,网址显示在左侧。

(16)tel

type="tel"是一个只能输入电话号码的输入框。由于全世界的电话号码格式都不相同,因此浏览器没有默认的验证模式,大多数时候需要自定义验证。

<input type="tel" id="phone" name="phone"
       pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
       required>

<small>Format: 123-456-7890</small>

上面代码定义了一个只能输入10位电话号码的输入框。

该类型的配套属性如下。

  • maxlength:允许的最大字符数。
  • minlength:允许的最少字符串。
  • pattern:输入内容必须匹配的正则表达式。
  • placeholder:输入为空时显示的示例文本。
  • readonly:布尔属性,表示该控件的内容是否只读。
  • size:一个非负整数,表示该输入框显示宽度为多少个字符。

(17)color

type="color"是一个选择颜色的控件,它的值一律都是#rrggbb格式。

<input type="color" id="background" name="background"
           value="#e66465">

上面代码在 Chrome 浏览器中,会显示一个#e66465的色块。点击色块,就会出现一个拾色器,供用户选择颜色。

如果没有指定value属性的初始值,默认值为#000000(黑色)。

(18)date

type="date"是一个只能输入日期的输入框,用户可以输入年月日,但是不能输入时分秒。输入格式是YYYY-MM-DD

<input type="date" id="start" name="start"
       value="2018-07-22"
       min="2018-01-01" max="2018-12-31">

上面代码会显示一个输入框,默认日期是2018年7月22日。用户点击以后,会日期选择器,供用户选择新的日期。

该类型有以下配套属性。

  • max:可以允许的最晚日期,格式为yyyy-MM-dd
  • min:可以允许的最早日期,格式为yyyy-MM-dd
  • step:步长值,一个数字,以天为单位。

(19)time

type="time"是一个只能输入时间的输入框,可以输入时分秒,不能输入年月日。日期格式是24小时制的hh:mm,如果包括秒数,格式则是hh:mm:ss。日期选择器的形式则随浏览器不同而不同。

<input type="time" id="appt" name="appt"
       min="9:00" max="18:00" required>

<small>营业时间上午9点到下午6点</small>

该类型有以下配套属性。

  • max:允许的最晚时间。
  • min:允许的最早时间。
  • readonly:布尔属性,表示用户是否不可以编辑时间。
  • step:步长值,单位为秒。
<input id="appt" type="time" name="appt" step="2">

上面代码中,调节控件的话,时间每次改变的幅度是2秒钟。

(20)month

type="month"是一个只能输入年份和月份的输入框,格式为YYYY-MM

<input type="month" id="start" name="start"
       min="2018-03" value="2018-05">

该类型有以下配套属性。

  • max:允许的最晚时间,格式为yyyy-MM
  • min:允许的最早时间,格式为yyyy-MM
  • readonly:布尔属性,表示用户是否不可以编辑时间。
  • step:步长值,单位为月。

(21)week

type="week"是一个输入一年中第几周的输入框。格式为yyyy-Www,比如2018-W18表示2018年第18周。

<input type="week" name="week" id="camp-week"
       min="2018-W18" max="2018-W26" required>

该类型有以下配套属性。

  • max:允许的最晚时间,格式为yyyy-Www
  • min:允许的最早时间,格式为yyyy-Www
  • readonly:布尔属性,表示用户是否不可以编辑时间。
  • step:步长值,单位为周。

(22)datetime-local

type="datetime-local"是一个时间输入框,让用户输入年月日和时分,格式为yyyy-MM-ddThh:mm。注意,该控件不支持秒。在支持的浏览器激活时打开日期选择器或年月日的数字滚轮。

<input type="datetime-local" id="meeting-time"
       name="meeting-time" value="2018-06-12T19:30"
       min="2018-06-07T00:00" max="2018-06-14T00:00">

该类型有以下配套属性。

  • max:允许的最晚时间,格式为yyyy-MM-ddThh:mm
  • min:允许的最早时间,格式为yyyy-MM-ddThh:mm
  • step:步长值,单位为秒,默认值是60。

<button>

<button>标签会生成一个可以点击的按钮,没有默认行为,通常需要用type属性或脚本指定按钮的功能。

<button>点击</button>

上面代码会产生一个按钮,上面的文字就是“点击”。

<button>内部不仅放置文字,还可以放置图像,这可以形成图像按钮。

<button name="search" type="submit">
  <img src="search.gif">搜索
</button>

<button>具有以下属性。

  • autofocus:布尔属性,表示网页加载时,焦点就在这个按钮。网页里面只能有一个元素,具有这个属性。
  • disabled:布尔属性,表示按钮不可用,会导致按钮变灰,不可点击。
  • name:按钮的名称(与value属性配合使用),将以name=value的形式,随表单一起提交到服务器。
  • value:按钮的值(与name属性配合使用),将以name=value的形式,随表单一起提交到服务器。
  • type:按钮的类型,可能的值有三种:submit(点击后将数据提交给服务器),reset(将所有控件的值重置为初始值),button(没有默认行为,由脚本指定按钮的行为)。
  • form:指定按钮关联的<form>表单,值为<form>id属性。如果省略该属性,默认关联按钮所在父表单。
  • formaction:数据提交到服务器的目标 URL,会覆盖<form>元素的action属性。
  • formenctype:数据提交到服务器的编码方式,会覆盖<form>元素的enctype属性。可能的值有三种:application/x-www-form-urlencoded(默认值),multipart/form-data(只用于文件上传),text/plain
  • formmethod:数据提交到服务器使用的 HTTP 方法,会覆盖<form>元素的method属性,可能的值为postget
  • formnovalidate:布尔属性,数据提交到服务器时关闭本地验证,会覆盖<form>元素的novalidate属性。
  • formtarget:数据提交到服务器后,展示服务器返回数据的窗口,会覆盖<form>元素的target属性。可能的值有_self(当前窗口),_blank(新的空窗口)、_parent(父窗口)、_top(顶层窗口)。

<select>

<select>标签用于生成一个下拉菜单。

<label for="pet-select">宠物:</label>

<select id="pet-select" name="pet-select">
  <option value="">--请选择一项--</option>
  <option value="dog">狗</option>
  <option value="cat">猫</option>
  <option value="others">其他</option>
</select>

上面代码中,<select>生成一个下拉菜单,菜单标题是“–请选择一项–”,最右侧有一个下拉箭头。点击下拉箭头,会显示三个菜单项,供用户点击选择。

下拉菜单的菜单项由<option>标签给出,每个<option>代表可以选择的一个值。选中的<option>value属性,就是<select>控件发送的服务器的值。

<option>有一个布尔属性selected,一旦设置,就表示该项是默认选中的菜单项。

<select name="choice">
  <option value="first">First Value</option>
  <option value="second" selected>Second Value</option>
  <option value="third">Third Value</option>
</select>

上面代码中,第二项Second Value是默认选中的。页面加载的时候,会直接显示在下拉菜单上。

<select>有如下属性。

  • autofocus:布尔属性,页面加载时是否自动获得焦点。
  • disabled:布尔属性,是否禁用当前控件。
  • form:关联表单的id属性。
  • multiple:布尔属性,是否可以选择多个菜单项。默认情况下,只能选择一项。一旦设置,多数浏览器会显示一个滚动列表框。用户可能需要按住Shift或其他功能键,选中多项。
  • name:控件名。
  • required:布尔属性,是否为必填控件。
  • size:设置了multiple属性时,页面显示时一次可见的行数,其他行需要滚动查看。默认值为 0。

<option><optgroup>

<option>标签用在<select><optgroup><datalist>里面,表示一个菜单项,参见<select>的示例。

它有如下属性。

  • disabled:布尔属性,是否禁用该项。
  • label:该项的说明。如果省略,则等于该项的文本内容。
  • selected:布尔属性,是否为默认值。显然,一组菜单中,只能有一个菜单项设置该属性。
  • value:该项提交到服务器的值。如果省略,则等于该项的文本内容。

<optgroup>表示菜单项的分组,通常用在<select>内部。

<label>宠物:
  <select name="pets" multiple size="4">
    <optgroup label="四条腿的宠物">
      <option value="dog">狗</option>
      <option value="cat">猫</option>
    </optgroup>
    <optgroup label="鸟类">
      <option value="parrot">鹦鹉</option>
      <option value="thrush">画眉</option>
    </optgroup>
  </select>
</label>

上面代码中,<select>是一个下拉菜单,它的内部使用<optgroup>将菜单项分成两组。每组有自己的标题,会加粗显示,但是用户无法选中。

它的属性如下。

  • disabled:布尔设置,是否禁用该组。一旦设置,该组所有的菜单项都不可选。
  • label:菜单项分组的标题。

<datalist>

<datalist>标签是一个容器标签,用于为指定控件提供一组相关数据,通常用于生成输入提示。它的内部使用<option>,生成每个菜单项。

<label for="ice-cream-choice">冰淇淋:</label>
<input type="text" list="ice-cream-flavors" id="ice-cream-choice" name="ice-cream-choice">

<datalist id="ice-cream-flavors">
  <option value="巧克力">
  <option value="椰子">
  <option value="薄荷">
  <option value="草莓">
  <option value="香草">
</datalist>

上面代码中,<input>生成一个文本输入框,用户可以输入文本。<input>list属性指定关联的<datalist>id属性。<datalist>的数据列表用于输入建议,用户点击输入框的时候,会显示一个下拉菜单,里面是建议的输入项。并且还会自动匹配用户已经输入的字符,缩小可选的范围,比如用户输入“香”,则只会显示“香草”这一项。

注意,<option>在这里可以不需要闭合标签。

<option>标签还可以加入label属性,作为说明文字。Chrome 浏览器会将其显示在value的下一行。

<datalist id="ide">
  <option value="Brackets" label="by Adobe">
  <option value="Coda" label="by Panic">
</datalist>

上面代码的渲染结果是,Chrome 浏览器会在下拉列表显示value值(比如Brackets),然后在其下方以小字显示label值(比如by Adobe)。

<textarea>

<textarea>是一个块级元素,用来生成多行的文本框。

<textarea id="story" name="story"
          rows="5" cols="33">
这是一个很长的故事。
</textarea>

上面代码会生成一个长度为5行,宽度为33个字符的文本框。

该标签有如下属性。

  • autofocus:布尔属性,是否自动获得焦点。
  • cols:文本框的宽度,单位为字符,默认值为20。
  • disabled:布尔属性,是否禁用该控件。
  • form:关联表单的id属性。
  • maxlength:允许输入的最大字符数。如果未指定此值,用户可以输入无限数量的字符。
  • minlength:允许输入的最小字符数。
  • name:控件的名称。
  • placeholder:输入为空时显示的提示文本。
  • readonly:布尔属性,控件是否为只读。和 disabled 属性不同的是,这个能让用户点击和选择元素内的文本。如果在表单里,这个元素的值还是会跟随表单一起提交。
  • required:布尔属性,控件是否为必填。
  • rows:文本框的高度,单位为行。
  • spellcheck:是否打开浏览器的拼写检查。可能的值有true(打开),default(由父元素或网页设置决定),false(关闭)。
  • wrap:输入的文本是否自动换行。可能的值有hard(浏览器自动插入换行符CR + LF,使得每行不超过控件的宽度),soft(输入内容超过宽度时自动换行,但不会加入新的换行符,并且浏览器保证所有换行符都是CR + LR,这是默认值),off(关闭自动换行,单行长度超过宽度时,会出现水平滚动条)。

<output>

<output>标签是一个行内元素,用于显示用户操作的结果。

<input type="number" name="a" value="10"> +
<input type="number" name="b" value="10"> =
<output name="result">20</output>

该标签有如下属性。

  • for:关联控件的id属性,表示为该控件的操作结果。
  • form:关联表单的id属性。
  • name:控件的名称。

<progress>

<progress>标签是一个行内元素,表示任务的完成进度。浏览器通常会将显示为进度条。

<progress id="file" max="100" value="70"> 70% </progress>

该标签有如下属性。

  • max:进度条的最大值,应该是一个大于0的浮点数。默认值为1。
  • value:进度条的当前值。它必须是0max属性之间的一个有效浮点数。如果省略了max属性,该值则必须在01之间。如果省略了value属性,则进度条会出现滚动,表明正在进行中,无法知道完成的进度。

<meter>

<meter>标签是一个行内元素,表示指示器,用来显示已知范围内的一个值,很适合用于任务的当前进度、磁盘已用空间、充电量等带有比例性质的场合。浏览器通常会将其显示为一个不会滚动的指示条。

<p>烤箱的当前温度是<meter min="200" max="500"
  value="350"> 350 度</meter>。</p>

上面代码会显示一个指示条,左侧表示200,右侧表示500,当前位置停留在350

注意,<meter>元素的子元素,正常情况下不会显示。只有在浏览器不支持<meter>时才会显示。

该标签有如下属性。

  • min:范围的下限,必须小于max属性。如果省略,则默认为0
  • max:范围的上限,必须大于min属性。如果省略,则默认为1
  • value:当前值,必须在min属性和max属性之间。如果省略,则默认为0
  • low:表示“低端”的上限门槛值,必须大于min属性,小于high属性和max属性。如果省略,则等于min属性。
  • high:表示“高端”的下限门槛值,必须小于max属性,大于low属性和min属性。如果省略,则等于max属性。
  • optimum:指定最佳值,必须在min属性和max属性之间。它应该与low属性和high属性一起使用,表示最佳范围。如果optimum小于low属性,则表示“低端”是最佳范围;如果大于high属性,则表示“高端”是最佳范围;如果在lowhigh之间,则表示“中间地带”是最佳范围。如果省略,则等于minmax的中间值。
  • form:关联表单的id属性。

Chrome 浏览器使用三种颜色,表示指示条所处的位置。较好情况时,当前位置为绿色;一般情况时,当前位置为黄色;较差情况时,当前位置为红色。

<meter id="fuel" name="fuel"
       min="0" max="100"
       low="33" high="66" optimum="80"
       value="50">
    at 50/100
</meter>

上面代码中,指示条可以分成三段:0 ~ 32,33 ~ 65,66 ~ 100。由于optimum属性是80,因此66 ~ 100是较好情况,33 ~ 65是一般情况,0 ~ 32是较差情况。浏览器因此会根据value属性,将当前位置显示为不同颜色,小于33时显示红色,大于65时显示绿色,两者之间显示黄色。

其他标签

<dialog>

基本用法

<dialog>标签表示一个可以关闭的对话框。

<dialog>
  Hello world
</dialog>

上面就是一个最简单的对话框。

默认情况下,对话框是隐藏的,不会在网页上显示。如果要让对话框显示,必须加上open属性。

<dialog open>
  Hello world
</dialog>

上面代码会在网页显示一个方框,内容是Hello world

<dialog>元素里面,可以放入其他 HTML 元素。

<dialog open>
  <form method="dialog">
    <input type="text">
    <button type="submit" value="foo">提交</button>
  </form>
</dialog>

上面的对话框里面,有一个输入框和提交按钮。

注意,上例中<form>method属性设为dialog,这时点击提交按钮,对话框就会消失。但是,表单不会提交到服务器,浏览器会将表单元素的returnValue属性设为 Submit 按钮的value属性(上例是foo)。

JavaScript API

<dialog>元素的 JavaScript API 提供Dialog.showModal()Dialog.close()两个方法,用于打开/关闭对话框。

const modal = document.querySelector('dialog');

// 对话框显示,相当于增加 open 属性
modal.showModal();

// 对话框关闭,相当于移除 open 属性
modal.close();

开发者可以提供关闭按钮,让其调用Dialog.close()方法,关闭对话框。

Dialog.close()方法可以接受一个字符串作为参数,用于传递信息。<dialog>接口的returnValue属性可以读取这个字符串,否则returnValue属性等于提交按钮的value属性。

modal.close('Accepted');
modal.returnValue // "Accepted"

Dialog.showModal()方法唤起对话框时,会有一个透明层,阻止用户与对话框外部的内容互动。CSS 提供了一个 Dialog 元素的::backdrop伪类,用于选中这个透明层,因此可以编写样式让透明层变得可见。

dialog {
  padding: 0;
  border: 0;
  border-radius: 0.6rem;
  box-shadow: 0 0 1em black;
}

dialog::backdrop {
  /* make the backdrop a semi-transparent black */
  background-color: rgba(0, 0, 0, 0.4);
}

上面代码不仅为<dialog>指定了样式,还将对话框的透明层变成了灰色透明。

<dialog>元素还有一个Dialog.show()方法,也能唤起对话框,但是没有透明层,用户可以与对话框外部的内容互动。

事件

<dialog>元素有两个事件,可以监听。

  • close:对话框关闭时触发
  • cancel:用户按下esc键关闭对话框时触发

如果希望用户点击透明层,就关闭对话框,可以用下面的代码。

modal.addEventListener('click', (event) => {
  if (event.target === modal) {
    modal.close('cancelled');
  }
});

<details><summary>

基本用法

<details>标签用来折叠内容,浏览器会折叠显示该标签的内容。

<details>
这是一段解释文本。
</details>

上面的代码在浏览器里面,会折叠起来,显示Details,前面有一个三角形,就像下面这样。

▶ Details

用户点击这段文本,折叠的文本就会展开,显示详细内容。

▼ Details
这是一段解释文本。

再点击一下,展开的文本又会重新折叠起来。

<details>标签的open属性,用于默认打开折叠。

<details open>
这是一段解释文本。
</details>

上面代码默认打开折叠。

<summary>标签用来定制折叠内容的标题。

<details>
  <summary>这是标题</summary>
  这是一段解释文本。
</details>

上面的代码显示结果如下。

▶ 这是标题

点击后,展示的效果如下。

▼ 这是标题
这是一段解释文本。

通过 CSS 设置summary::-webkit-details-marker,可以改变标题前面的三角箭头。

summary::-webkit-details-marker {
  background: url(https://example.com/foo.svg);
  color: transparent;
}

下面的样式是另一种替换箭头的方法。

summary::-webkit-details-marker {
  display: none;
}
summary:before {
  content: "\2714";
  color: #696f7c;
  margin-right: 5px;
}

JavaScript API

Details元素的open属性返回<details>当前是打开还是关闭。

const details = document.querySelector('details');

if (detail.open === true) {
  // 展开状态
} else {
  // 折叠状态
}

Details元素有一个toggle事件,打开或关闭折叠时,都会触发这个事件。

details.addEventListener('toggle', event => {
  if (details.open) {
    /* 展开状况 */
  } else {
    /* 折叠状态 */
  }
});

HTML 自定义元素

浏览器对待自定义元素,就像对待标准元素一样,只是没有默认的样式和行为。

浏览器提供了一个HTMLUnknownElement对象,所有自定义元素都是该对象的实例。

HTML import

自定义元素,就可以写出语义性非常好的 HTML 代码。

<share-buttons>
  <social-button type="weibo">
    <a href="...">微博</a>
  </social-button>
  <social-button type="weixin">
    <a href="...">微信</a>
  </social-button>
</share-buttons>

如果将<share-buttons>元素的样式与脚本,封装在一个 HTML 文件share-buttons.html之中,这个元素就可以复用了。

使用的时候,先引入share-buttons.html

<link rel="import" href="share-buttons.html">

浏览器

浏览器的主要功能是将用户选择得 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中。

主流浏览器是有一定市场份额且有自己独立研发内核的浏览器,也叫原生浏览器。常见的有 Chrome,Safari,Firefox,IE/Edge,Opera。qq浏览器、uc浏览器等壳浏览器是精简、包装的原生浏览器。

根据市场份额重点需要学习的是 Chrome 浏览器。

浏览器工作原理

User Interface

包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分。

Browser engine

用来查询及操作渲染引擎的接口。

Rendering engine

即浏览器内核,也叫渲染引擎、排版引擎、解释引擎。用来显示请求的内容,负责解析 HTML、CSS。渲染引擎是浏览器兼容性问题出现的根本原因。常见浏览器的内核如下:

浏览器 内核
chrome Webkit -> Blink
Safari Webkit
Firefox Gecko
IE -> Edge Trident -> EdgeHTML
Opera Presto -> Blink

Chrome为每个Tab分配了各自的渲染引擎实例,每个Tab就是一个独立的进程。

浏览器渲染过程

网页的生成过程,大致可以分成五步:

  1. HTML代码转化成DOM Tree
  2. CSS代码转化成CSSOM Tree(CSS Object Model)
  3. 结合DOM和CSSOM,生成一棵渲染树Render Tree
  4. 生成布局(flow),将所有渲染树进行平面合成
  5. 将布局绘制(paint)在屏幕上

这五步里面,第一步到第三步都非常快,耗时的是 “生成布局”(flow)和"绘制"(paint)这两步,这两步合称为"渲染"(render)。

网页生成的时候,至少会渲染一次。用户访问的过程中,还会不断重新渲染。会导致网页重新渲染的情况:

  • 修改DOM
  • 修改样式表
  • 用户事件(比如鼠标悬停、页面滚动、输入框键入文字、改变窗口大小等等)

重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow)(也叫回流),后者叫做"重绘"(repaint)。

需要注意的是,“重绘"不一定需要"重排”,比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。但是,“重排"必然导致"重绘”,比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。

提高网页性能,就是要降低"重排"和"重绘"的频率和成本,尽量少触发重新渲染。一般的规则是:

  • 样式表越简单,重排和重绘就越快。
  • 重排和重绘的DOM元素层级越高,成本就越高。
  • table元素的重排和重绘成本,要高于div元素

重排和重绘的DOM元素层级越高,成本就越高。所以重排肯定也是哪块的布局改变了重排哪里,而不是每次整个页面都重排。

Networking

用来完成网络调用。

JavaScript Interpreter

JavaScript ( JS ) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。虽然它是作为开发Web 页面的脚本语言而出名的,但是它也被用到了很多非浏览器环境中,如 Node.js。JavaScript 是一种基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

常见浏览器的 JS 引擎如下:

浏览器 JS 引擎
chrome V8
Safari Nitro(4-)
Firefox SpiderMonkey(1.0-3.0)/ TraceMonkey(3.5-3.6)/ JaegerMonkey(4.0-)
IE -> Edge JScript(IE3.0-IE8.0) / Chakra(IE9+)
Opera Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-)

UI Backend

用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。

Data Persistence

属于持久层,浏览器需要在硬盘中保存类似 cookie、sessionStorage 的各种数据。

消除浏览器默认样式

针对同一个类型的 HTML 标签,不同的浏览器往往有不同的表现,所以在网站制作的时候,开发者通常都是需要将这些浏览器的默认样式清除,让网页在不同的浏览器上能够保持一致。

针对清除浏览器默认样式这件事,在很早之前 CSS 大师 Eric A. Meyer 就干过。它就是写一堆通用的样式用来重置浏览器默认样式,这些样式通常会放到一个命名为 reset.css 文件中。比如大师的 reset.css 。他的这份 reset.css 据说是被使用最广泛的重设样式的方案了。

除了 reset.css 外,后来又出现了 Normalize.css 。关于 Normalize.css, 其作者 necolas 专门写了一篇文章介绍了它,并谈到了它和 reset.css 的区别。翻译版:让我们谈一谈 Normalize.css

文章介绍到:Normalize.css 只是一个很小的CSS文件,但它在默认的 HTML 元素样式上提供了跨浏览器的高度一致性。相比于传统的 CSS reset,Normalize.css 是一种现代的、为 HTML5 准备的优质替代方案,现在已经有很多知名的框架和网站在使用它了。

区别于 reset.css,Normalize.css 有如下特点:

  • reset.css 几乎为所有标签都设置了默认样式,而 Normalize.css 则是有选择性的保护了部分有价值的默认值;
  • 修复了很多浏览器的 bug,而这是 reset.css 没做到的;
  • 不会让你的调试工具变的杂乱,相反 reset.css 由于设置了很多默认值,所以在浏览器调试工具中往往会看到一大堆的继承样式,显得很杂乱;
  • Normalize.css 是模块化的,所以可以选择性的去掉永远不会用到的部分,比如表单的一般化;

Emmet

Emmet是一个编辑器插件,官方网站提供多编辑器支持。它提供一种简练的语法规则提高 html 和 css 的编写效率。

Emmet的基本用法:先写简写形式,然后用"快捷键"将其转成HTML代码。不同的编辑器有不同的转化键。

html:5

To

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	
</body>
</html>

Emmet支持的简写规则(大写的E代表一个HTML标签)

块级元素会换行,内联级元素会同一行显示。

  • html:5 生成 HTML 骨架。
  • E 代表HTML标签。
  • E#id 代表id属性。
  • E.class 代表class属性。
  • E[attr1=foo1 attr2=foo2] 代表某一个特定属性。
  • E{foo} 代表标签包含的内容是foo。
  • E>N 代表N是E的子元素。
  • E+N 代表N是E的同级元素。
  • E^N 代表N是E的上级元素。
  • E*N 连写
  • E$*N 自动编号

HTTP

HTTP/0.9

HTTP (Hyper Text Transfer Protocol)是基于 TCP/IP 协议的应用层协议。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。

最早版本是1991年发布的0.9版。该版本极其简单,只有一个命令GET。

GET /index.html

上面命令表示,TCP 连接(connection)建立后,客户端向服务器请求(request)网页index.html

协议规定,服务器只能回应HTML格式的字符串,不能回应别的格式。

<html>
  <body>Hello World</body>
</html>

服务器发送完毕,就关闭TCP连接。

HTTP/1.0

简介

1996年5月,HTTP/1.0 版本发布,内容大大增加。

首先,任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。

其次,除了GET命令,还引入了POST命令和HEAD命令,丰富了浏览器与服务器的互动手段。

再次,HTTP请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。

其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

请求格式

下面是一个1.0版的HTTP请求的例子。

GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

可以看到,这个格式与0.9版有很大变化。

第一行是请求命令,必须在尾部添加协议版本(HTTP/1.0)。后面就是多行头信息,描述客户端的情况。

回应格式

服务器的回应如下。

HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
  <body>Hello World</body>
</html>

回应的格式是"头信息 + 一个空行(\r\n) + 数据"。其中,第一行是"协议版本 + 状态码(status code) + 状态描述"。

Content-Type 字段

关于字符的编码,1.0版规定,头信息必须是 ASCII 码,后面的数据可以是任何格式。因此,服务器回应的时候,必须告诉客户端,数据是什么格式,这就是Content-Type字段的作用。

下面是一些常见的Content-Type字段的值。

  • text/plain
  • text/html
  • text/css
  • image/jpeg
  • image/png
  • image/svg+xml
  • audio/mp4
  • video/mp4
  • application/javascript
  • application/pdf
  • application/zip
  • application/atom+xml

这些数据类型总称为MIME type,每个值包括一级类型和二级类型,之间用斜杠分隔。

除了预定义的类型,厂商也可以自定义类型。

application/vnd.debian.binary-package

上面的类型表明,发送的是Debian系统的二进制数据包。

MIME type还可以在尾部使用分号,添加参数。

Content-Type: text/html; charset=utf-8

上面的类型表明,发送的是网页,而且编码是UTF-8。

客户端请求的时候,可以使用Accept字段声明自己可以接受哪些数据格式。

Accept: */*

上面代码中,客户端声明自己可以接受任何格式的数据。

MIME type不仅用在HTTP协议,还可以用在其他地方,比如HTML网页。

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><!-- 等同于 --><meta charset="utf-8" /> 

Content-Encoding 字段

由于发送的数据可以是任何格式,因此可以把数据压缩后再发送。Content-Encoding字段说明数据的压缩方法。

Content-Encoding: gzip
Content-Encoding: compress
Content-Encoding: deflate

客户端在请求时,用Accept-Encoding字段说明自己可以接受哪些压缩方法。

Accept-Encoding: gzip, deflate

缺点

HTTP/1.0 版的主要缺点是,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。

TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。所以,HTTP 1.0版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。

为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段。

Connection: keep-alive

这个字段要求服务器不要关闭TCP连接,以便其他请求复用。服务器同样回应这个字段。

Connection: keep-alive

一个可以复用的TCP连接就建立了,直到客户端或服务器主动关闭连接。但是,这不是标准字段,不同实现的行为可能不一致,因此不是根本的解决办法。

HTTP/1.1

1997年1月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年。它进一步完善了 HTTP 协议,一直用到了20年后的今天,直到现在还是最流行的版本。

持久连接

1.1 版的最大变化,就是引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive

客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。

Connection: close

目前(2016),对于同一个域名,大多数浏览器允许同时建立6个持久连接。

管道机制

1.1 版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。

Content-Length 字段

一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是Content-length字段的作用,声明本次回应的数据长度。

Content-Length: 3495

上面代码告诉浏览器,本次回应的长度是3495个字节,后面的字节就属于下一个回应了。

在1.0版中,Content-Length字段不是必需的,因为浏览器发现服务器关闭了TCP连接,就表明收到的数据包已经全了。

分块传输编码

使用Content-Length字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。

对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用"流模式"(stream)取代"缓存模式"(buffer)。

因此,1.1版规定可以不使用Content-Length字段,而使用“分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。

Transfer-Encoding: chunked

每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了。下面是一个例子。

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

25
This is the data in the first chunk

1C
and this is the second one

3
con

8
sequence

0

其他功能

1.1版还新增了许多动词方法:PUTPATCHHEADOPTIONSDELETE

另外,客户端请求的头信息新增了Host字段,用来指定服务器的域名。

Host: www.example.com

有了Host字段,就可以将请求发往同一台服务器上的不同网站,为虚拟主机的兴起打下了基础。

缺点

虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为“队头堵塞”(Head-of-line blocking)。

为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。如果HTTP协议设计得更好一些,这些额外的工作是可以避免的。

SPDY 协议

2009年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。

这个协议在Chrome浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。

HTTP/2

2015年,HTTP/2 发布。它不叫 HTTP/2.0,是因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。

二进制协议

HTTP/1.1 版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧"(frame):头信息帧和数据帧。

二进制协议的一个好处是,可以定义额外的帧。HTTP/2 定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。

多工

HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。

举例来说,在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。

这样双向的、实时的通信,就叫做多工(Multiplexing)。

数据流

因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。

HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。

数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。

客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。

头信息压缩

HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如CookieUser Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzipcompress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

服务器推送

HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。

常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。

HTTPS

SSL/TLS协议运行机制的概述

互联网的通信安全,建立在SSL/TLS协议之上。

作用

不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。

(1) 窃听风险(eavesdropping):第三方可以获知通信内容。

(2) 篡改风险(tampering):第三方可以修改通信内容。

(3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。

SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

(1) 所有信息都是加密传播,第三方无法窃听。

(2) 具有校验机制,一旦被篡改,通信双方会立刻发现。

(3) 配备身份证书,防止身份被冒充。

互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。

历史

互联网加密通信协议的历史,几乎与互联网一样长。

  • 1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。
  • 1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。
  • 1996年,SSL 3.0版问世,得到大规模应用。
  • 1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版(Transport Layer Security)。
  • 2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版

目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。

TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。

基本的运行过程

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。

但是,这里有两个问题。

(1)如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

(2)公钥加密计算量太大,如何减少耗用的时间?

解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。

因此,SSL/TLS协议的基本过程是这样的:

(1) 客户端向服务器端索要并验证公钥。

(2) 双方协商生成"对话密钥"。

(3) 双方采用"对话密钥"进行加密通信。

上面过程的前两步,又称为"握手阶段"(handshake)。

握手阶段的详细过程

“握手阶段"涉及四次通信,我们一个个来看。需要注意的是,“握手阶段"的所有通信都是明文的。

客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。

在这一步,客户端主要向服务器提供以下信息。

(1) 支持的协议版本,比如TLS 1.0版。

(2) 一个客户端生成的随机数,稍后用于生成"对话密钥”。

(3) 支持的加密方法,比如RSA公钥加密。

(4) 支持的压缩方法。

这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。

对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。

服务器回应(SeverHello)

服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

(1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。

(2) 一个服务器生成的随机数,稍后用于生成"对话密钥”。

(3) 确认使用的加密方法,比如RSA公钥加密。

(4) 服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

(1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。

(2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。

至于为什么一定要用三个随机数,来生成"会话密钥",dog250解释得很好:

“不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。”

此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。

服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。

(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。

DH算法的握手阶段

整个握手阶段都不加密(也没法加密),都是明文的。因此,如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。

虽然理论上,只要服务器的公钥足够长(比如2048位),那么Premaster secret可以保证不被破解。但是为了足够安全,我们可以考虑把握手阶段的算法从默认的RSA算法,改为 Diffie-Hellman算法(简称DH算法)。

采用DH算法后,Premaster secret不需要传递,双方只要交换各自的参数,就可以算出这个随机数。

Diffie-Hellman密钥交换算法原理

DH方法针对的是以下困难的局面:Alice和Bob 想共有一个密钥,用于对称加密。但是他们之间的通信渠道是不安全的。所有经过此渠道的信息均会被敌对方Eve看到。为了防止密钥泄露,Diffie与Hellman提出以下密钥交换协议:

  1. Alice和Bob先对p和g达成一致,而且公开出来。Eve也就知道它们的值了。
  2. Alice取一个私密的整数a,不让任何人知道,发给Bob 计算结果:A=ga mod p. Eve 也看到了A的值。
  3. 类似,Bob 取一私密的整数b,发给Alice计算结果B=gb mod p.同样Eve也会看见传递的B是什么。
  4. Alice 计算出K=Ba mod p=(gb)a mod p=gab mod p.
  5. Bob 也能计算出K=Ab mod p=(ga)b mod p=gab mod p.
  6. Alice 和 Bob 现在就拥有了一个共用的密钥K.
  7. 虽然Eve看见了p,g, A and B, 但是鉴于计算离散对数的困难性,他无法知道a和b 的具体值,所以Eve就无从知晓密钥K是什么了。

session的恢复

握手阶段用来建立SSL连接。如果出于某种原因,对话中断,就需要重新握手。

这时有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。

session ID的思想很简单,就是每一次对话都有一个编号(session ID)。如果对话中断,下次重连的时候,只要客户端给出这个编号,且服务器有这个编号的记录,双方就可以重新使用已有的"对话密钥",而不必重新生成一把。

session ID是目前所有浏览器都支持的方法,但是它的缺点在于session ID往往只保留在一台服务器上。所以,如果客户端的请求发到另一台服务器,就无法恢复对话。session ticket就是为了解决这个问题而诞生的,目前只有Firefox和Chrome浏览器支持(2014)。

上图中,客户端不再发送session ID,而是发送一个服务器在上一次对话中发送过来的session ticket。这个session ticket是加密的,只有服务器才能解密,其中包括本次对话的主要信息,比如对话密钥和加密方法。当服务器收到session ticket以后,解密后就不必重新生成对话密钥了。

互联网协议

互联网的核心是一系列协议,总称为"互联网协议"(Internet Protocol Suite)。它们对电脑如何连接和组网,做出了详尽的规定。理解了这些协议,就理解了互联网的原理。

概述

五层模型

互联网的实现,分成好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。

用户接触到的,只是最上面的一层,根本没有感觉到下面的层。要理解互联网,必须从最下层开始,自下而上理解每一层的功能。

如何分层有不同的模型,有的模型分七层,有的分四层。我觉得,把互联网分成五层,比较容易解释。

实体层又叫物理层,链路层又叫数据链路层。越下面的层,越靠近硬件;越上面的层,越靠近用户。

它们叫什么名字,其实并不重要。只需要知道,互联网分成若干层就可以了。

层与协议

每一层都是为了完成一种功能。为了实现这些功能,就需要大家都遵守共同的规则。

大家都遵守的规则,就叫做"协议"(protocol)。

互联网的每一层,都定义了很多协议。这些协议的总称,就叫做"互联网协议"(Internet Protocol Suite)。它们是互联网的核心,下面介绍每一层的功能,主要就是介绍每一层的主要协议。

实体层

电脑要组网,第一件事要干什么?当然是先把电脑连起来,可以用光缆、电缆、双绞线、无线电波等方式。

这就叫做"实体层",它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。

链接层

定义

单纯的0和1没有任何意义,必须规定解读方式:多少个电信号算一组?每个信号位有何意义?

这就是"链接层"的功能,它在"实体层"的上方,确定了0和1的分组方式。

以太网协议

早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做“以太网”(Ethernet)的协议,占据了主导地位。

以太网规定,一组电信号构成一个数据包,叫做"帧"(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。

“标头"包含数据包的一些说明项,比如发送者、接受者、数据类型等等;“数据"则是数据包的具体内容。

“标头"的长度,固定为18字节。“数据"的长度,最短为46字节,最长为1500字节。因此,整个"帧"最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。

MAC地址

上面提到,以太网数据包的"标头”,包含了发送者和接受者的信息。那么,发送者和接受者是如何标识呢?

以太网规定,连入网络的所有设备,都必须具有"网卡"接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。

每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。

前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。

广播

定义地址只是第一步,后面还有更多的步骤。

首先,一块网卡怎么会知道另一块网卡的MAC地址?

回答是有一种ARP协议,可以解决这个问题。这个留到后面介绍,这里只需要知道,以太网数据包必须知道接收方的MAC地址,然后才能发送。

其次,就算有了MAC地址,系统怎样才能把数据包准确送到接收方?

回答是以太网采用了一种很"原始"的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机发送,让每台计算机自己判断,是否为接收方。

上图中,1号计算机向2号计算机发送一个数据包,同一个子网络的3号、4号、5号计算机都会收到这个包。它们读取这个包的"标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做"广播”(broadcasting)。

有了数据包的定义、网卡的MAC地址、广播的发送方式,“链接层"就可以在多台计算机之间传送数据了。

网络层

网络层的由来

以太网协议,依靠MAC地址发送数据。理论上,单单依靠MAC地址,上海的网卡就可以找到洛杉矶的网卡了,技术上是可以实现的。

但是,这样做有一个重大的缺点。以太网采用广播方式发送数据包,所有成员人手一"包”,不仅效率低,而且局限在发送者所在的子网络。也就是说,如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理的,否则互联网上每一台计算机都会收到所有包,那会引起灾难。

互联网是无数子网络共同组成的一个巨型网络,很像想象上海和洛杉矶的电脑会在同一个子网络,这几乎是不可能的。

因此,必须找到一种方法,能够区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用"路由"方式发送。(“路由"的意思,就是指如何向不同的子网络分发数据包。)遗憾的是,MAC地址本身无法做到这一点。它只与厂商有关,与所处网络无关。

这就导致了"网络层"的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做"网络地址”,简称"网址”。

于是,“网络层"出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一起。

网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。

IP协议

规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。

目前,广泛采用的是IP协议第四版,简称IPv4。这个版本规定,网络地址由32个二进制位组成。

习惯上,我们用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。

互联网上的每一台计算机,都会分配到一个IP地址。这个地址分成两个部分,前一部分代表网络,后一部分代表主机。比如,IP地址172.16.254.1,这是一个32位的地址,假定它的网络部分是前24位(172.16.254),那么主机部分就是后8位(最后的那个1)。处于同一个子网络的电脑,它们IP地址的网络部分必定是相同的,也就是说172.16.254.2应该与172.16.254.1处在同一个子网络。

但是,问题在于单单从IP地址,我们无法判断网络部分。还是以172.16.254.1为例,它的网络部分,到底是前24位,还是前16位,甚至前28位,从IP地址上是看不出来的。

那么,怎样才能从IP地址,判断两台计算机是否属于同一个子网络呢?这就要用到另一个参数"子网掩码”(subnet mask)。

所谓"子网掩码",就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.254.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

知道"子网掩码",我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。

比如,已知IP地址172.16.254.1和172.16.254.233的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,结果都是172.16.254.0,因此它们在同一个子网络。

总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。

IP数据包

根据IP协议发送的数据,就叫做IP数据包。不难想象,其中必定包括IP地址信息。

但是前面说过,以太网数据包只包含MAC地址,并没有IP地址的栏位。那么是否需要修改数据定义,再添加一个栏位呢?

回答是不需要,我们可以把IP数据包直接放进以太网数据包的"数据"部分,因此完全不用修改以太网的规格。这就是互联网分层结构的好处:上层的变动完全不涉及下层的结构。

具体来说,IP数据包也分为"标头"和"数据"两个部分。

“标头"部分主要包括版本、长度、IP地址等信息,“数据"部分则是IP数据包的具体内容。它放进以太网数据包后,以太网数据包就变成了下面这样。

IP数据包的"标头"部分的长度为20到60字节,整个数据包的总长度最大为65,535字节。因此,理论上,一个IP数据包的"数据"部分,最长为65,515字节。前面说过,以太网数据包的"数据"部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送了。

ARP协议

因为IP数据包是放在以太网数据包里发送的,所以我们必须同时知道两个地址,一个是对方的MAC地址,另一个是对方的IP地址。通常情况下,对方的IP地址是已知的(DNS),但是我们不知道它的MAC地址。

所以,我们需要一种机制,能够从IP地址得到MAC地址。

这里又可以分成两种情况。第一种情况,如果两台主机不在同一个子网络,那么事实上没有办法得到对方的MAC地址,只能把数据包传送到两个子网络连接处的"网关”(gateway),让网关去处理。

第二种情况,如果两台主机在同一个子网络,那么我们可以用ARP协议,得到对方的MAC地址。ARP协议也是发出一个数据包(包含在以太网数据包中),其中包含它所要查询主机的IP地址,在对方的MAC地址这一栏,填的是FF:FF:FF:FF:FF:FF,表示这是一个"广播"地址。它所在子网络的每一台主机,都会收到这个数据包,从中取出IP地址,与自身的IP地址进行比较。如果两者相同,都做出回复,向对方报告自己的MAC地址,否则就丢弃这个包。

总之,有了ARP协议之后,我们就可以得到同一个子网络内的主机MAC地址,可以把数据包发送到任意一台主机之上了。

传输层

传输层的由来

有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。

接下来的问题是,同一台主机上有许多程序都需要用到网络,比如,你一边浏览网页,一边与朋友在线聊天。当一个数据包从互联网上发来的时候,你怎么知道,它是表示网页的内容,还是表示在线聊天的内容?

也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做"端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

“端口"是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。不管是浏览网页还是在线聊天,应用程序会随机选用一个端口,然后与服务器的相应端口联系。

**“传输层"的功能,就是建立"端口到端口"的通信。相比之下,“网络层"的功能是建立"主机到主机"的通信。只要确定主机和端口,我们就能实现程序之间的交流。**因此,Unix系统就把主机+端口,叫做"套接字”(socket)。有了它,就可以进行网络应用程序开发了。

UDP协议

现在,我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。

UDP数据包,也是由"标头"和"数据"两部分组成。

“标头"部分主要定义了发出端口和接收端口,“数据"部分就是具体的内容。然后,把整个UDP数据包放入IP数据包的"数据"部分,而前面说过,IP数据包又是放在以太网数据包之中的,所以整个以太网数据包现在变成了下面这样:

UDP数据包非常简单,“标头"部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

TCP协议

UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。

为了解决这个问题,提高网络可靠性,TCP协议就诞生了。这个协议非常复杂,但可以近似认为,它就是有确认机制的UDP协议,每发出一个数据包都要求确认。如果有一个数据包遗失,就收不到确认,发出方就知道有必要重发这个数据包了。

因此,TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。

TCP数据包和UDP数据包一样,都是内嵌在IP数据包的"数据"部分。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

应用层

应用程序收到"传输层"的数据,接下来就要进行解读。由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。

“应用层"的作用,就是规定应用程序的数据格式。

举例来说,TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了"应用层”。

这是最高的一层,直接面对用户。它的数据就放在TCP数据包的"数据"部分。因此,现在的以太网的数据包就变成下面这样。

至此,整个互联网的五层结构,自下而上全部讲完了。这是从系统的角度,解释互联网是如何构成的。

用户的上网设置

静态IP地址

你买了一台新电脑,插上网线,开机,这时电脑能够上网吗?

通常你必须做一些设置。有时,管理员(或者ISP)会告诉你下面四个参数,你把它们填入操作系统,计算机就能连上网了:

  • 本机的IP地址
  • 子网掩码
  • 网关的IP地址
  • DNS的IP地址

这四个参数缺一不可。由于它们是给定的,计算机每次开机,都会分到同样的IP地址,所以这种情况被称作"静态IP地址上网”。

但是,这样的设置很专业,普通用户望而生畏,而且如果一台电脑的IP地址保持不变,其他电脑就不能使用这个地址,不够灵活。出于这两个原因,大多数用户使用"动态IP地址上网”。

动态IP地址

所谓"动态IP地址”,指计算机开机后,会自动分配到一个IP地址,不用人为设定。它使用的协议叫做DHCP协议

这个协议规定,每一个子网络中,有一台计算机负责管理本网络的所有IP地址,它叫做"DHCP服务器”。新的计算机加入网络,必须向"DHCP服务器"发送一个"DHCP请求"数据包,申请IP地址和相关的网络参数。

前面说过,如果两台计算机在同一个子网络,必须知道对方的MAC地址和IP地址,才能发送数据包。但是,新加入的计算机不知道这两个地址,怎么发送数据包呢?

DHCP协议做了一些巧妙的规定。

DHCP协议

首先,它是一种应用层协议,建立在UDP协议之上。

(1)最前面的"以太网标头”,设置发出方(本机)的MAC地址和接收方(DHCP服务器)的MAC地址。前者就是本机网卡的MAC地址,后者这时不知道,就填入一个广播地址:FF-FF-FF-FF-FF-FF。

(2)后面的"IP标头",设置发出方的IP地址和接收方的IP地址。这时,对于这两者,本机都不知道。于是,发出方的IP地址就设为0.0.0.0,接收方的IP地址设为255.255.255.255。

(3)最后的"UDP标头",设置发出方的端口和接收方的端口。这一部分是DHCP协议规定好的,发出方是68端口,接收方是67端口。

这个数据包构造完成后,就可以发出了。以太网是广播发送,同一个子网络的每台计算机都收到了这个包。因为接收方的MAC地址是FF-FF-FF-FF-FF-FF,看不出是发给谁的,所以每台收到这个包的计算机,还必须分析这个包的IP地址,才能确定是不是发给自己的。当看到发出方IP地址是0.0.0.0,接收方是255.255.255.255,于是DHCP服务器知道"这个包是发给我的",而其他计算机就可以丢弃这个包。

接下来,DHCP服务器读出这个包的数据内容,分配好IP地址,发送回去一个"DHCP响应"数据包。这个响应包的结构也是类似的,以太网标头的MAC地址是双方的网卡地址,IP标头的IP地址是DHCP服务器的IP地址(发出方)和255.255.255.255(接收方),UDP标头的端口是67(发出方)和68(接收方),分配给请求端的IP地址和本网络的具体参数则包含在Data部分。

新加入的计算机收到这个响应包,于是就知道了自己的IP地址、子网掩码、网关地址、DNS服务器等等参数。

一个实例:访问网页

本机参数

我们假定,用户设置好了自己的网络参数:

  • 本机的IP地址:192.168.1.100
  • 子网掩码:255.255.255.0
  • 网关的IP地址:192.168.1.1
  • DNS的IP地址:8.8.8.8

然后他打开浏览器,想要访问Google,在地址栏输入了网址:www.google.com。

这意味着,浏览器要向Google发送一个网页请求的数据包。

DNS协议

我们知道,发送数据包,必须要知道对方的IP地址。但是,现在,我们只知道网址www.google.com,不知道它的IP地址。

DNS协议可以帮助我们,将这个网址转换成IP地址。已知DNS服务器为8.8.8.8,于是我们向这个地址发送一个DNS数据包(53端口)。

然后,DNS服务器做出响应,告诉我们Google的IP地址是172.194.72.105。于是,我们知道了对方的IP地址。

子网掩码

接下来,我们要判断,这个IP地址是不是在同一个子网络,这就要用到子网掩码。

已知子网掩码是255.255.255.0,本机用它对自己的IP地址192.168.1.100,做一个二进制的AND运算(两个数位都为1,结果为1,否则为0),计算结果为192.168.1.0;然后对Google的IP地址172.194.72.105也做一个AND运算,计算结果为172.194.72.0。这两个结果不相等,所以结论是,Google与本机不在同一个子网络。

因此,我们要向Google发送数据包,必须通过网关192.168.1.1转发,也就是说,接收方的MAC地址将是网关的MAC地址。

应用层协议

浏览网页用的是HTTP协议。

HTTP部分的内容,类似于下面这样:

GET / HTTP/1.1
Host: www.google.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1) ......
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
Cookie: ... ...

我们假定这个部分的长度为4960字节,它会被嵌在TCP数据包之中。

TCP协议

TCP数据包需要设置端口,接收方(Google)的HTTP端口默认是80,发送方(本机)的端口是一个随机生成的1024-65535之间的整数,假定为51775。

TCP数据包的标头长度为20字节,加上嵌入HTTP的数据包,总长度变为4980字节。

IP协议

然后,TCP数据包再嵌入IP数据包。IP数据包需要设置双方的IP地址,这是已知的,发送方是192.168.1.100(本机),接收方是172.194.72.105(Google)。

IP数据包的标头长度为20字节,加上嵌入的TCP数据包,总长度变为5000字节。

以太网协议

最后,IP数据包嵌入以太网数据包。以太网数据包需要设置双方的MAC地址,发送方为本机的网卡MAC地址,接收方为网关192.168.1.1的MAC地址(通过ARP协议得到)。

以太网数据包的数据部分,最大长度为1500字节,而现在的IP数据包长度为5000字节。因此,IP数据包必须分割成四个包。因为每个包都有自己的IP标头(20字节),所以四个包的IP数据包的长度分别为1500、1500、1500、560。

服务器端响应

经过多个网关的转发,Google的服务器172.194.72.105,收到了这四个以太网数据包。

根据IP标头的序号,Google将四个包拼起来,取出完整的TCP数据包,然后读出里面的"HTTP请求",接着做出"HTTP响应",再用TCP协议发回来。

本机收到HTTP响应以后,就可以将网页显示出来,完成一次网络通信。

MIME

概述

MIME的全称是"Multipurpose Internet Mail Extensions",中译为"多用途互联网邮件扩展",指的是一系列的电子邮件技术规范。

顾名思义,MIME是对传统电子邮件的一个扩展,现在已经成为电子邮件实际上的标准。

传统的电子邮件

传统的电子邮件是1982年定下技术规范的,文件是RFC 822。

它的一个重要特点,就是规定电子邮件只能使用ASCII字符。这导致了三个结果:1)非英语字符都不能在电子邮件中使用;2)电子邮件中不能插入二进制文件(如图片);3)电子邮件不能有附件。

这实际上无法接受的,因此到了1992年,工程师们决定扩展电子邮件的技术规范,提出一系列补充规范,这就是MIME的由来。

下面是一封传统的电子邮件。

From: "Tommy Lee" <lee@example.com>
To: "Jack Zhang" <zhang@example.com>
Subject: Test
Date: Wed, 17 May 2000 19:08:29 -0400
Message-ID: <NDBBIAKOPKHFGPLCODIGIEKBCHAA.lee@example.com>

Hello World.

从上面可以看出,这封信的发信人地址是lee@example.com,收信人地址是zhang@example.com,邮件主题是Test,发送时间是2000年5月17日,邮件内容是"Hello World."。

在结构上,这封信分为三个部分:首先是信件头,然后是一个空行,最后是信件内容。

MIME

MIME对传统电子邮件的扩展,表现在它在信件头部分添加了几条语句,主要有三条。

第一条是:

MIME-Version: 1.0

这条语句是必须的,而且1.0这个版本值是不变的,即使MIME本身已经升级了好几次。

有了这条语句,收信端就知道这封信使用了MIME规范。

第二条语句是:

Content-Type: text/plain; charset="ISO-8859-1"

这一行是极端重要的,它表明传递的信息类型和采用的编码。

Content-Type表明信息类型,缺省值为" text/plain"。它包含了主要类型(primary type)和次要类型(subtype)两个部分,两者之间用"/“分割。主要类型有9种,分别是application、audio、example、image、message、model、multipart、text、video。

每一种主要类型下面又有许多种次要类型,常见的有:

  • text/plain:纯文本,文件扩展名.txt
  • text/html:HTML文本,文件扩展名.htm和.html
  • image/jpeg:jpeg格式的图片,文件扩展名.jpg
  • image/gif:GIF格式的图片,文件扩展名.gif
  • audio/x-wave:WAVE格式的音频,文件扩展名.wav
  • audio/mpeg:MP3格式的音频,文件扩展名.mp3
  • video/mpeg:MPEG格式的视频,文件扩展名.mpg
  • application/zip:PK-ZIP格式的压缩文件,文件扩展名.zip

详细的Content-Type列表,可以查看这里

如果信息的主要类型是"text”,那么还必须指明编码类型"charset",缺省值是ASCII,其他可能值有"ISO-8859-1"、“UTF-8”、“GB2312"等等。

整个Content-Type这一行,不仅使用在电子邮件,后来也被移植到了HTTP协议中,所以现在只要是在网上传播的HTTP信息,都带有Content-Type头,以表明信息类型。

编码转换方式

前面已经说过,电子邮件的传统格式不支持非ASCII编码和二进制数据。因此MIME规定了第三条语句:

Content-transfer-encoding: base64

这条语句指明了编码转换的方式。Content-transfer-encoding的值有5种——“7bit”、“8bit”、“binary”、“quoted-printable"和"base64”——其中"7bit"是缺省值,即不用转化的ASCII字符。真正常用是"quoted-printable"和"base64"两种。

实例

下面是一封我收到的邮件的源码:

Date: Wed, 18 Jun 2008 18:07:51 +0800 (CST)
From: xxx <xxx@163.com>
To: yifeng.ruan@gmail.com
Message-ID: <14410503.1073611213783671983.JavaMail.coremail@bj163app54.163.com>
Subject: =?gbk?B?xOO6ww==?=
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary=&quot;----=_Part_287491_22998031.1213783671982&quot;

------=_Part_287491_22998031.1213783671982
Content-Type: text/plain; charset=gbk
Content-Transfer-Encoding: base64

IAq4+b7dsr+209PQudi55raoo6yyu7XD1Nq12Le9yM66zs341b7Jz7nSz+DTprXEtqvO96Osx+vE
49TaxOO1xLKpv83W0AogIArW0Ln6yr2x6tPvIC0gyO7Su7fltcTN+MLnyNXWvgoKtcS12jEy1cXN
vMasyb6z/aOst/HU8s7Sw8fXt76/xOO1xM/gudjU8MjOoaPQu9C7us/X96OhtMvNvMas1Nq4vbz+
wO/D5g==
------=_Part_287491_22998031.1213783671982
Content-Type: text/html; charset=gbk
Content-Transfer-Encoding: quoted-printable

<DIV>&amp;nbsp;</DIV>
<DIV>=B8=F9=BE=DD=B2=BF=B6=D3=D3=D0=B9=D8=B9=E6=B6=A8=A3=AC=B2=BB=B5=C3=D4=
=DA=B5=D8=B7=BD=C8=CE=BA=CE=CD=F8=D5=BE=C9=CF=B9=D2=CF=E0=D3=A6=B5=C4=B6=AB=
=CE=F7=A3=AC=C7=EB=C4=E3=D4=DA=C4=E3=B5=C4=B2=A9=BF=CD=D6=D0</DIV>
<DIV>&amp;nbsp;
......

可以看到这封信的MIME语句是:

MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_287491_22998031.1213783671982"

“Content-Type: multipart/alternative;“表明这封信的内容,是纯文本和HTML文本的混合。另两个可能的值是multipart/mixed和multipart/related,分别表示"信件内容中有二进制内容"和"信件带有附件”。

“boundary="—-=_Part_287491_22998031.1213783671982” “表明不同信件内容的分割线是”—-=_Part_287491_22998031.1213783671982”,它通常是一个很长的随机字符串。

信件内容部分又有两个子信件头:

Content-Type: text/plain; charset=gbk
Content-Transfer-Encoding: base64

Content-Type: text/html; charset=gbk
Content-Transfer-Encoding: quoted-printable

它们表明,第一个部分是gbk编码的纯文本,编码转换格式是base64。第二个部分是gbk编码的HTML文本,编码转化格式是quoted-printable。

Base64

概述

MIME主要使用两种编码转换方式—-Quoted-printable和Base64—-将8位的非英语字符转化为7位的ASCII字符。

虽然这样的初衷,是为了满足电子邮件中不能直接使用非ASCII码字符的规定,但是也有其他重要的意义:

a)所有的二进制文件,都可以因此转化为可打印的文本编码,使用文本软件进行编辑;

b)能够对文本进行简单的加密。

Quoted-printable

首先,简单介绍一下Quoted-printable编码转换方式。它主要用于ACSII文本中夹杂少量非ASCII码字符的情况,不适合于转换纯二进制文件。

它规定将每一个8位的字节,转换为3个字符。

第一个字符是”=“号,这是固定不变的。

后面二个字符是二个十六进制数,分别代表了这个字节前四位和后四位的数值。

举例来说,ASCII码中"换页键”(form feed)是12,二进制形式是00001100,写成十六进制就是0C,因此它的编码值为"=0C"。

“=“号的ASCII值是61,二进制形式是00111101,因为它的编码值是”=3D”。

除了可打印的ASCII码以外,所有其他字符都必须用这种方式进行转换。

所有可打印的ASCII码字符(十进制值从33到126)都保持原样不变,"="(十进制值61)除外。

Base64

所谓Base64,就是说选出64个字符—-小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"(再加上作为垫字的"=",实际上是65个字符)—-作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。

具体来说,转换方式可以分为四步。

  • 第一步,将每三个字节作为一组,一共是24个二进制位。
  • 第二步,将这24个二进制位分为四组,每个组有6个二进制位。
  • 第三步,在每组前面加两个00,扩展成32个二进制位,即四个字节。
  • 第四步,根据下表,得到扩展后的每个字节的对应符号,这就是Base64的编码值。(因为前两位是0,那么总共有6个有效位,即转为10进制最大 26-1=63)
0 A  17 R   34 i   51 z

1 B  18 S   35 j   52 0

2 C  19 T   36 k   53 1

3 D  20 U   37 l   54 2

4 E  21 V   38 m   55 3

5 F  22 W   39 n   56 4

6 G  23 X   40 o   57 5

7 H  24 Y   41 p   58 6

8 I   25 Z   42 q   59 7

9 J  26 a   43 r   60 8

10 K  27 b   44 s   61 9

11 L  28 c   45 t   62 +

12 M  29 d   46 u   63 /

13 N  30 e   47 v

14 O  31 f   48 w   

15 P  32 g   49 x

16 Q  33 h   50 y

因为,Base64将三个字节转化成四个字节,因此Base64编码后的文本,会比原文本大出三分之一左右。

如果字节数不足三,则这样处理:

a)二个字节的情况:将这二个字节的一共16个二进制位,每个组有6个二进制位,转成三组,最后一组除了前面加两个0以外,后面也要加两个0。这样得到一个三位的Base64编码,再在末尾补上一个"=“号。

比如,“Ma"这个字符串是两个字节,可以转化成三组00010011、00010110、00010000以后,对应Base64值分别为T、W、E,再补上一个”=“号,因此"Ma"的Base64编码就是TWE=。

b)一个字节的情况:将这一个字节的8个二进制位,每个组有6个二进制位,转成二组,最后一组除了前面加二个0以外,后面再加4个0。这样得到一个二位的Base64编码,再在末尾补上两个”=“号。

比如,“M"这个字母是一个字节,可以转化为二组00010011、00010000,对应的Base64值分别为T、Q,再补上二个”=“号,因此"M"的Base64编码就是TQ==。

实例

举一个具体的实例,演示英语单词Man如何转成Base64编码。

Text contentMan
ASCII7797110
Bit pattern010011010110000101101110
Index1922546
Base64-EncodedTWFu
  • 第一步,“M”、“a”、“n"的ASCII值分别是77、97、110,对应的二进制值是01001101、01100001、01101110,将它们连成一个24位的二进制字符串010011010110000101101110。
  • 第二步,将这个24位的二进制字符串分成4组,每组6个二进制位:010011、010110、000101、101110。
  • 第三步,在每组前面加两个00,扩展成32个二进制位,即四个字节:00010011、00010110、00000101、00101110。它们的十进制值分别是19、22、5、46。
  • 第四步,根据上表,得到每个值对应Base64编码,即T、W、F、u。

因此,Man的Base64编码就是TWFu。

再举一个中文的例子,汉字"严"如何转化成Base64编码?

这里需要注意,汉字本身可以有多种编码,比如gb2312、utf-8、gbk等等,每一种编码的Base64对应值都不一样。下面的例子以utf-8为例。

首先,“严"的utf-8编码为E4B8A5,写成二进制就是三字节的"11100100 10111000 10100101”。将这个24位的二进制字符串,转换成四组一共32位的二进制值"00111001 00001011 00100010 00100101”,相应的十进制数为57、11、34、37,它们对应的Base64值就为5、L、i、l。

所以,汉字"严”(utf-8编码)的Base64值就是5Lil。

WebSocket

WebSocket 是一种网络通信协议,很多高级功能都需要它。

为什么需要 WebSocket?

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

ws://example.com:80/some/path

客户端的简单示例

WebSocket 的用法相当简单。

下面是一个网页脚本的例子(点击这里看运行结果),基本上一眼就能明白。

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};      

客户端的 API

WebSocket 客户端的 API 如下。

WebSocket 构造函数

WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。

var ws = new WebSocket('ws://localhost:8080');

执行上面语句之后,客户端就会与服务器进行连接。

实例对象的所有属性和方法清单,参见这里

webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种。

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

下面是一个示例。

switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

webSocket.onopen

实例对象的onopen属性,用于指定连接成功后的回调函数。

ws.onopen = function () {  ws.send('Hello Server!');}

如果要指定多个回调函数,可以使用addEventListener方法。

ws.addEventListener('open', function (event) {  ws.send('Hello Server!');});

webSocket.onclose

实例对象的onclose属性,用于指定连接关闭后的回调函数。

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

webSocket.onmessage

实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。

ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。

ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。

// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

webSocket.send()

实例对象的send()方法用于向服务器发送数据。

发送文本的例子。

ws.send('your message');

发送 Blob 对象的例子。

var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子。

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

webSocket.onerror

实例对象的onerror属性,用于指定报错时的回调函数。

socket.onerror = function(event) {
  // handle error event
};

socket.addEventListener("error", function(event) {
  // handle error event
});

服务端的实现

WebSocket 服务器的实现,可以查看维基百科的列表

WebSocketd

推荐一款非常特别的 WebSocket 服务器:Websocketd

它的最大特点,就是后台脚本不限语言,标准输入(stdin)就是 WebSocket 的输入,标准输出(stdout)就是 WebSocket 的输出。

Server-Sent Events

服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。

SSE 的本质

严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

SSE 的特点

SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。

总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。

但是,SSE 也有自己的优点。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

因此,两者各有特点,适合不同的场合。

客户端 API

EventSource 对象

SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。

if ('EventSource' in window) {
  // ...
}

使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。

var source = new EventSource(url);

上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。

var source = new EventSource(url, { withCredentials: true });

EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。

  • 0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
  • 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
  • 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。

基本用法

连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。

source.onopen = function (event) {
  // ...
};

// 另一种写法
source.addEventListener('open', function (event) {
  // ...
}, false);

客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。

source.onmessage = function (event) {
  var data = event.data;
  // handle message
};

// 另一种写法
source.addEventListener('message', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,事件对象的data属性就是服务器端传回的数据(文本格式)。

如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。

source.onerror = function (event) {
  // handle error event
};

// 另一种写法
source.addEventListener('error', function (event) {
  // handle error event
}, false);

close方法用于关闭 SSE 连接。

source.close();

自定义事件

默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。

source.addEventListener('foo', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,浏览器对 SSE 的foo事件进行监听。如何实现服务器发送foo事件,请看下文。

服务器实现

数据格式

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam

每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。

[field]: value\n

上面的field可以取四个值。

  • data
  • event
  • id
  • retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。

: This is a comment

下面是一个例子。

: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

data 字段

数据内容用data字段表示。

data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。

data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id 字段

数据标识符用id字段表示,相当于每一条数据的编号。

id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。

event 字段

event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。

event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。

下面是另一个例子。

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

retry 字段

服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。

retry: 10000\n

两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。