Skip to content

2.1 HTML:搭建页面的结构

HTML 是伴随着浏览器一同出现的超文本标记语言,严格来说,它并不是一种编程语言。然而我们肉眼在浏览器上看到的所有信息(文字、图片、视频等),都是基于 HTML 搭建的。HTML 的标签就像是积木一样,它们之间通过任意嵌套和排列组合,可以搭建出各种各样的网页。

HTML 的基本结构是这样的:

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Hello World!</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

HTML 在历史发展中也存在过多个版本,不过目前使用的都是最新的 HTML5,上面代码就是标准的 HTML5 结构。我们看到,开头的是一个 <!DOCTYPE html>,标记了文档类型是 HTML。

可能有同学会有疑问:为什么需要这个标记呢?其实我也有过这个疑问,细查之下,原来这也是历史原因导致的。在最早期的浏览器有 Navigator 和 IE 两个版本,它们之间差异很大。后来 W3C 联盟创立了通用标准,为了兼容以前的标准,就通过 DOCTYPE 来表示使用哪种标准渲染网页。

<!DOCTYPE html> 是 W3C 规定的通用标准,现代网页都是这个标准。如果没有这个标识,浏览器会用老旧的渲染模式渲染你的网页,我们称之为“怪异模式“,这种模式下可能会颠覆你对 HTML 的认识。所以这一行标记至关重要,一定不能缺少。

标记下面就是表示 HTML 总体结构三个核心元素:

  • <html>:应用的根元素,用来包裹所有元素。
  • <head>:该元素的内容对用户不可见,主要包含文档的配置信息。
  • <body>:所有可见元素的父元素,也就是我们看到的页面。

2.1.1 核心 DOM 体系

HTML 是由元素组成的,下面介绍元素的结构。

以 p 元素为例,左边是开始标签(<p>),右边是结束标签(</p>),中间是元素的内容。一对标签再加上中间的内容,经过浏览器渲染,就变成一个元素(Element)。

除了包含标签和内容,元素还可以指定属性(Attribute)。属性的作用是为元素添加额外的信息。例如,常用的 idclass 就是元素的属性,可以依据属性在 CSS 中修饰样式,也可以在 JavaScript 中获取元素。

元素的结构如图 2-1 所示。

图2-1

当元素被渲染后,JavaScript 中会有一套 Web API 来访问这些元素,这套 API 被称为 DOM(Document Object Model,文档对象模型)。DOM 会将 HTML 文档的每个元素解析为节点和对象,最终将其组合成一棵 DOM 树,这棵 DOM 树的结构与 HTML 文档的结构一一映射。

DOM 是对 HTML 文档结构化的表述,并且提供了一套标准的 API 操作元素,包括添加、修改和删除等,这样就可以通过 JavaScript 操作元素,使页面发生变化。

DOM 不仅是一套接口,更是一套规范。DOM 作为 W3C 规范的一部分,约束了浏览器中 JavaScript 与 HTML 之间的交互方式,因此程序员才有机会用同一套 API 操作 HTML,而不必关心浏览器底层差异。

DOM 树的解析

DOM 以树的形态存在,树中的最小单位是节点(Node)。在 DOM 中一切都是节点,文本是节点,属性是节点,注释也是节点。当然,上面提到的元素自然也是节点。

DOM 中主要有 4 种类型的节点:

  • Document:整个 DOM 树。
  • Element:单个元素。
  • Text:元素内的纯文本。
  • Attribute:元素的属性。

一份 HTML 文档会被浏览器解析成各种节点,这些节点组成 DOM 树。

前面介绍的 HTML 的基本结构可以解析成如图 2-2 所示的 DOM 树。

图2-2

可以看出,DOM 树的节点之间或是平级关系或是嵌套关系,所以可以把 DOM 树中节点之间的关系分为两大类:

  • 父子节点:节点之间是嵌套关系。
  • 兄弟节点:节点之间是平级关系。

这两种关系与后面要介绍的组件之间的关系基本上是一致的。节点之间的关系如图 2-3 所示:

图2-3

下面介绍使用 DOM API 操作节点的方法。假设要获取页面上的<div>元素,那么可以按照如下形式操作:

js
var div = document.getElementById("div");
var div = document.querySelector("#div");
var div = document.getElementsByTagName("div")[0];

上述 3 个方法都通过 DOM API 获取了一个 div 元素。在获取 div 元素后,可以直接对其进行修改或删除,具体如下:

js
// 修改属性
div.style.width = "300px";
// 修改元素内容
div.innerHTML = "我的div的内容";
// 带标签的元素内容
div.innerHTML = "<span>我的div的内容</span>";
// 删除元素
div.remove();

除了获取已经存在的元素进行操作,还可以创建一个新元素,具体如下:

js
// 首先获取父节点
var parent = document.getElementById("parent");
// 创建新节点
var span = document.createElement("span");
// 设置 span 节点的内容
span.innerHTML = "hello world";
// 把新创建的元素塞进父节点里去
parent.appendChild(span);

上述这些都是最基本的 DOM 操作。

目前,大多数程序员基本上都在使用 Vue.js 框架和 React 框架,很少直接执行 DOM 操作。但是页面更新的本质就是元素发生变化,只不过是框架做了修改 DOM 的事情,程序员可以专注于数据。DOM 操作是前端基本功,前端程序员必须要掌握。

head 元素的解析

<head> 元素规定了文档相关的配置信息(元数据),包括文档的标题、引用的文档样式和脚本等,要求至少包含一个 title 元素用来指定文档的标题。

head 元素通常包含以下 4 个子元素:

  • <title>:用于设置文档标题。
  • <link>:用于引入外部资源,通常引入的是 CSS 和图标。
  • <script>:用于引入 JavaScript 文件或执行 JavaScript 脚本。
  • <meta>:用于配置元数据。

其中,比较重要的是 link 元素和 meta 元素。在一些大型项目中这两个元素会被多次使用,这是为什么呢?

(1) link 元素通过 rel 属性来指定加载什么类型的资源,通过 href 属性指定加载的资源的地址,具体如下:

js
// 加载网页的 icon 图标
<link rel="icon" href="xxx.ico"/>
// 加载 CSS 文件
<link rel="stylesheet" href="xxx.css"/>
// 加载 iOS 的 icon 图标
<link rel="apple-touch-icon" href="xxx.png" />
// 应用被安装到桌面时加载的配置文件
<link rel="manifest" href="xxx.json"/>

有的读者或许会对上述代码中的 manifest 有些陌生。其实,manifest 的作用是当网页变成 PWA 渐进式应用时,加载和读取指定的配置文件。

在做前端响应式布局时,通常会在 CSS 中编写媒体查询,即满足某个条件后使用某个样式。例如,正常网页的背景色是灰色的,如果要在打印时变成白色,一般的做法就是在 CSS 中添加媒体查询代码,具体如下:

css
@media print {
  body {
    background: #fff;
  }
}

其实, link 元素也提供了这样的功能,即通过提供 media 属性来指定媒体类型,只有媒体类型匹配才会加载资源。

上面在 CSS 中编写的打印样式与下面使用 link 元素实现的效果是一样的。

js
<link rel="stylesheet" media="print" href="./print.css" />
// print.css
body {
  background: #fff;
}

(2) meta 元素用于配置元数据,在 HTML 基本结构中就有一个简单的 meta 元素:

html
<meta charset="utf-8" />

这个元数据指定了网页的字符编码是 UTF-8。当然元数据不止这一个,它可以表示的内容非常丰富。大多是通过 name 和 content 两个属性来指定的,比如为了网站的 SEO 会添加下面的关键字和描述信息:

html
<!-- 为了更好的SEO -->
<meta name="author" content="杨成功" />
<meta name="keywords" content="HTML,CSS,JavaScript,AJAX" />
<meta name="description" content="这里是最齐全的前端学习教程" />

有了这些标识,网页会更容易地被搜索引擎抓取,从而展现在搜索的结果页中。

对移动端而言至关重要的属性是 viewport,使用该属性可以控制页面的大小等。viewport 被译为视口,视口又分为布局视口(Layout Viewport)与视觉视口(Visual Viewport)。布局视口与视觉视口的差别如图 2-4 所示。

图2-4

可以看到,布局视口代表屏幕宽度,视觉视口代表网页宽度。如果这两种宽度不一致,就会出现屏幕只显示网页的一部分,或者网页没有将屏幕撑满。因此在移动端,需要设置两种视口的宽度一致,且不缩放,标准代码是这样:

html
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, user-scalable=0;"
/>

这样就做到了移动端最基本的适配。当然,<head>元素还有许多有用的设置,感兴趣的读者可以自行查阅相关资料。

2.1.2 语义化元素

所有用户在网页上可见的元素,都需要作为子元素添加在<body>元素中。 body 元素可以包含任意内容(如标题、段落、图片、视频和表格等),不同的内容使用不同的元素来表示。

假设需要添加一段文字和一张图片,可以使用如下代码:

js
<p>本书是前端开发实战派</p>
<img src="xxx.logo.png"/>

元素的分类

可以将 body 元素中的子元素分为以下两类。

  • 内容元素:如文字、图片等用于展现内容的元素。
  • 布局元素:不直接展示内容,而是将内容元素更好地排列布局。

内容元素包括内容展示元素和内容操作元素,具体示例如下:

js
// 标题
<h1>一级标题</h1>
<h2>二级标题</h2>

// 段落和文本
<p>这里可以写一段很长的文本,特殊字用 <span>span</span> 来包裹</p>

// 图片和链接
<img src="./logo.png"/>
<a href="http://www.baidu.com">链接</a>

// 按钮
<button>按钮</button>

// 输入框
<input type="text" value="我是可编辑的内容"/>
<textarea value="我是可编辑的大段内容"/>

// 选择框
<select>
  <option>选项1</option>
  <option>选项2</option>
  <option>选项3</option>
</select>

从代码中也可以看出,内容元素一般就是行内元素和表单,是网页内容的最小单元。

最经典的布局元素就是<div>元素,该元素可以装载万物。例如,将上述内容元素放到 div 元素中,并指定不同的类名和样式,就能把想要的网页布局搭建出来。

早期的前端页面基本上都采用 DIV + CSS 的布局方式,不同的布局区域全靠类名进行区分。虽然能实现目的,但是并不推荐采用这种方式,主要原因如下。

  • 如果全部使用 DIV 布局,代码结构看上去就会很混乱,可读性比较差。
  • 开发者难以区分代码结构,浏览器自然也无法区分,这就会导致 SEO 的效果很糟糕。

语义化的布局

下面引入一个全部使用 div 元素布局页面的示例,代码如下:

js
<div class="head">
  <span>我是标题</span>
</div>
<div class="nav">
  <a href="/html">HTML</a> |
  <a href="/css">CSS</a>
</div>
<div class="box">
  <div class="menu">
    <span>侧边栏</span>
  </div>
  <div class="content">
    <span>主内容区域<span>
    <div class="text-area">
      <p>具体的文章内容</p>
      <img src="xx.png"/>
    </div>
  </div>
</div>
<div class="foot">
  <p>这是一个尾部</p>
</div>

上述代码的类名比较规范,虽然能通过类名进行简单区分,但是无法解决根本问题。还有更好的方案吗?其实很简单,就是使用更符合语义化的布局元素。

什么是语义化?说白了就是能立刻看得懂。例如,网页的头部可以用 div 元素,但是用<header>元素是不是更直观?语义化就是用不同含义的元素代替清一色的 div 元素。

将上述 div 元素布局改造成符合语义化的布局结构:

js
// 语义化的布局结构
<header>
  <h1>我是标题</h1>
</header>
<nav>
  <a href="/html">HTML</a> |
  <a href="/css">CSS</a>
</nav>
<section>
  <aside>
    <span>侧边栏</span>
  </aside>
  <main>
    <h2>主内容区域</h2>
    <article>
      <p>具体的文章内容</p>
      <img src="xx.png"/>
    </article>
  </main>
</section>
<footer>
  <p>这是一个尾部</p>
</footer>

这样就会非常直观,并且一目了然。

代码中的语义化元素是 HTML5 新增的,其具体含义如下:

  • <header>:网页的头部区域。
  • <nav>:导航区域,展示页面切换导航。
  • <section>:页面中的一块子区域。
  • <aside>:侧边栏,一般是侧边菜单。
  • <main>:页面内容区域,不包括导航、菜单、侧边栏、头部和尾部等部分。
  • <article>:文章区域,一般在 main 元素中。
  • <footer>:网页的尾部区域。

其中 <header>,<nav>,<aside>,<main>,<footer> 五个元素,建议每个页面只出现一次,因为多次出现是不符合语义的。

在浏览器解析到这类元素时,重点从 nav 元素和 header 元素中抓取关键字。如果都是 div 元素,浏览器就无法判断哪部分是关键区域,这也是语义化能实现更好的 SEO 的原因。

2.1.3 了解 HTML5

HTML 5 作为下一代 HTML 标准,有许多新特性,前面用到的语义化元素就是其中的一部分。HTML 5 的新特性主要包括以下几点:

  • 增加了音频元素<audio>和视频元素<video>
  • 增加了绘画元素<canvas><svg>
  • 增强了对表单的支持。
  • 引入了本地存储机制。
  • 支持地理定位和拖放。
  • 支持 WebWorkers。
  • 支持 WebSocket。

1. 认识音/视频元素

音视频元素是 HTML 多媒体能力的极大突破,以前需要 Flash 才能播放音视频,现在一个标签就搞定了。

音/视频元素主要有 3 个:<audio>是音频元素;<video>是视频元素;<source>元素包裹在 audio 元素和 video 元素中,主要用来指定音/视频类型和资源地址。

引入一个简单的音频播放器的代码如下:

html
<audio controls>
  <source src="test.mp3" type="audio/mpeg" />
  <span>您的浏览器不支持 audio 标签</span>
</audio>

将代码在浏览器中运行,看到的效果如图 2-5 所示:

图2-5

这是一个音频控制条,像一个小型的 mp3 播放器。如果不想展示 UI,只做背景音乐的话,将 controls 属性去掉即可。

audio 支持的音频类型有三种,对应到 source 标签的 type 值如下:

  • MP3:audio/mpeg。
  • Ogg:audio/ogg。
  • Wav:audio/wav。

这里最常用的还是 MP3,几乎所有浏览器都支持。不同类型的文件必须指定正确的 type。

实际场景中最常用的还是播放视频,引入一个基本的视频播放器的代码如下:

html
<video id="video1" controls>
  <source src="test.mp4" type="video/mp4" />
  <span>您的浏览器不支持 video 标签</span>
</video>

video 元素中有多个属性可以配置视频如何播放,常用的几个如下:

  • poster:视频封面,没有播放时显示的图片。
  • autoplay:自动播放。
  • loop:循环播放。
  • controls:显示视频控制条。
  • muted:是否禁音。

2. 使用 JavaScript 操作视频

除了使用 controls 属性显示视频控制条,还可以通过 DOM API 来操作视频,示例代码如下:

html
<button onclick="toPlay">暂停/播放</button>
<button onclick="setVolume">设置音量</button>
<button onclick="forward">快进15s</button>
<button onclick="goback">快退15s</button>

<script>
  var video = document.getElementById("video1");
  // 播放/暂停
  function toPlay() {
    if (video.paused) {
      video.play(); // 播放
    } else {
      video.pause(); // 暂停
    }
  }
  // 设置音量,音量范围为0~1
  function setVolume() {
    video.volume = 0.3; // 30%
    video.volume = 0.0; // 静音
  }
  // 快进15s
  function forward() {
    // video.duration  表示视频总时长,单位秒
    // video.currentTime  表示视频已播放时间,单位秒
    let long = 15;
    if (video.duration > video.currentTime + long) {
      video.currentTime = video.currentTime + long;
    } else {
      video.currentTime = video.duration;
    }
  }
  // 快退15s
  function goback() {
    let long = 15;
    if (video.currentTime - long > 0) {
      video.currentTime = video.currentTime - long;
    } else {
      video.currentTime = 0;
    }
  }
</script>

在网页中,常见的场景是,在 Banner 图下面放一段循环播放的小视频作为背景。只要掌握了上面的视频属性,这个功能实现就很简单,具体如下:

html
<video id="video2" loop muted autoplay>
  <source src="test.mp4" type="video/mp4" />
</video>

除了正常的视频播放,音视频元素还可以实现直播。这个可以用 B 站开源的 flv.js 实现,有兴趣的可以自行查阅。

2.1.4 表单与验证

HTML 5 在原有表单元素的基础上进行了丰富的扩展,主要表现为添加了许多新的属性,使之前需要用 JavaScript 才能实现的效果,现在用原生标签就可以轻松实现。

1. input 元素的新功能

表单元素中最具有代表性的是<input>元素,该元素增加了许多新的 type 属性,具体如下:

js
// 选择日期
<input type="date"/>
// 选择时间
<input type="time"/>
// 选择日期时间
<input type="datetime-local"/>
// 选择月份
<input type="month"/>
// 选择颜色
<input type="color"/>
// 数字输入框
<input type="number" min="1" max="10"/>
// 邮箱输入框
<input type="email"/>
// 滑动条
<input type="range" min="1" max="10"/>

上面这些是最常用的,且都是 Chrome 浏览器支持的 type 值。像选择日期,时间,数字输入框等,在前端表单中经常用到,以前要加一个这样的组件还得引用一些第三方框架,现在直接使用就可以了。

除了带来新功能的 type 属性,input 还增加了非常多有用的其他属性。这些属性扩展了 input 的能力,使表单提交越来越满足我们多样化的需求。新加的常用属性如下:

  • autofocus:自动聚焦。
  • autocomplete:自动填充。
  • max/min:最大最小值。
  • maxlength:最大字符长度。
  • disabled:禁用元素。
  • readonly:元素只读。
  • form:指定所属表单。
  • required:必填。
  • pattern:自定义验证规则。
  • novalidate:提交表单时不验证。

这些属性中,autocomplete 设置是否自动填充,在登录页面中我们通常需要设置账户密码自动填充,验证码不需要自动填充。max/min 属性只有在 type="number" 的时候生效,设置输入数值的最大值和最小值。maxlength 是对普通字符串输入框的限制,规定最多能输入几个字。

至于 required,pattern,novalidate 这些属性,只有当元素作为表单项,也就是 form 元素的子项时才会是有用的规则,这些规则的验证会在表单提交时自动触发,验证不通过则阻止表单提交。

pattern 属性很有用,用正则表达式来自定义输入值的规则。比如要输入手机号,那么就可以写一个手机号的正则表达式赋值给 pattern,这样在表单提交时就会验证输入值是不是一个手机号。这个字段使表单项验证的灵活性大大提高。

form 属性的作用是,指定当前元素属于哪个表单。比如一个 input 不在某个 form 标签的包裹之内,默认情况下这个输入框和 form 表单无关,更不会执行该表单的提交和验证规则。但你可以为这个 input 指定 form 属性,值为表单的 id,手动将输入框绑定到这个表单之上。

提示:required,pattern,form 这些属于表单项的属性,不仅仅适用于 input,其他能作为表单项的元素,比如 select,button 也是通用的。

2.为表单提交添加验证

接下来实现添加验证的基本表单功能,代码如下:

js
<form id="form1">
  <input type='text' name="name" placeholder="输入姓名" maxlength="5" required/>
  <input type='number' name="age" placeholder="输入年龄" min="15" max="65" required/>
  <input type='text'name="sex" placeholder="输入性别" required disabled/>
  <input type="submit" value="提交">
</form>
<input form="form1" name="other" placeholder="输入额外信息" required>

当点击提交的时候,首先第一个 input 的验证被触发了,如图 2-6 所示。

图2-6

form 验证的逻辑是按照子元素的顺序验证,第一个表单项验证通过之后,才会验证下一个。maxlength 不需要点提交就会直接限制输入字符,超过 5 个字符再输入无效。

下面验证数值文本框。当单击“提交”按钮时,触发最大/最小值的验证,如图 2-7 所示。

图2-7

性别输入验证这块有点特殊,即要求必填,又规定了 disabled。试验结果是当元素被设为 disabled 时,表单的验证就会失效,将 disabled 换为 readonly 之后,效果也一样。说明在表单项只有可编辑的时候才会有表单验证,否则无效,这也是符合实际情况的。

disabled 和 readonly 这两个属性非常相似,很多人都不清楚有什么区别,下面我们列一下:

  • disabled 对所有表单类元素有用,readonly 只对文本,密码输入框有用。
  • 设置 disabled 后 JS 获取不到该元素,设置 readonly 则可以。
  • 设置 disabled 后表单数据不会传输,设置 readonly 则依然可以传输。
  • disabled 和 readonly 都会使表单验证失效。

所以,在元素被设置了 disabled 属性或 readonly 属性后,相当于同时设置了 novalidate 属性。

我们再看额外信息的部分,它不在 form 包裹之内。当前面验证通过之后,这个元素的验证也被触发了,如图 2-8 所示。

图2-8

在一些复杂的页面场景当中,有时候你的输入框可能并不会包裹在 form 元素之内,这个时候你就可以用 form 属性来为输入框绑定表单,和将其放置到 form 标签之内是一样的效果。

最后一步,当所有验证通过之后,表单的逻辑是将数据提交到某个地址,此时会刷新页面,这不是我们想要的。在前后端分离的开发模式中,我们更希望的是只获取验证之后的输入值,不刷新页面,拿到值之后自行处理,这该如何实现呢?

其实很简单,第一步,在 form 标签上加一个 onsubmit 事件:

js
<form id="form1" onsubmit="onSubmit(this);return false;">
  ...
</form>

这里首先调用一个 onSubmit() 方法,参数 this 代表 form 元素。这里最关键的是在最后面加一个 return false,它可以阻止默认的页面刷新。

onSubmit() 方法只会在表单验证通过之后调用,所以我们不用考虑未验证通过的情况。只需要在这个方法之内获取到每个表单项的 name 和 value,组成一个我们需要的数据对象即可:

js
function onSubmit(e) {
  let form_data = {};
  Array.from(e.children)
    .filter((el) => el.name)
    .forEach((el) => {
      form_data[el.name] = el.value;
    });
  console.log(form_data);
}

这个代码中有两个部分需要注意:一是将 e.children 这个类数组转换成数组。第二是要用 fliter 过滤一下没有提供 name 属性的表单项,最后组合而成的就是我们想要的数组。

2.1.5 画布与图像

Canvas 是一个非常强大的标签,主要用来绘制图像。在传统的前端中,我们要写一些动画,主要是靠 CSS 或者 JavaScript 来操控。如果动画的复杂性特别高,那么高频的 DOM 操作会给浏览器带来很大的性能消耗,结果就是动画卡顿不流畅。

Canvas 则另辟蹊径,只提供了一个 canvas 标签,后面全靠强大的 JavaScript Canvas 2D API 手动画图,完全不涉及到 DOM 操作。Canvas 2D API 极其强大的绘图能力,使在浏览器上做一些小型互动游戏有了可能,今天的 canvas 在数据可视化方面的应用更加广泛。

话不多说,我们先来写一个 Hello World 试试手:

html
<canvas id="canvas"></canvas>
<script>
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  // ctx 就是 Canvas 2D API 实例对象

  ctx.fillStyle = "blue"; // 填充颜色
  ctx.fillRect(10, 10, 100, 100); // 填充矩形

  ctx.strokeStyle = "#fff"; // 线条颜色
  ctx.strokeText("Hello World", 30, 60); // 添加文字
</script>

代码中的 ctx.fillRect 函数有四个参数 (x, y, width, height),前两个参数表示到画布左边和上边的距离,后两个参数表示绘制图形的宽和高。Canvas API 中的函数参数大多都属于这四类,要么表示坐标,要么表示宽高。

这个简单的 Hello World,首先绘制了一个蓝色背景的矩形,然后在上面加了一行白色文字,效果如图 2-9 所示:

图2-9

写习惯了 CSS 的前端同学,第一次看到这里的 API 可能有点懵。这里的 fillstroke 是什么意思?大家首先想到的可能是用 background 和 color 去实现上图中的样式。

但 canvas 不是普通元素,首先我们要抛弃在 CSS 中写样式的思想。我们应该想的是,在真实的世界中,假设你的面前就是画板和画笔,你怎么去画一个图形?是不是可以先在脑海中勾勒一个轮廓,先用铅笔画出来大致形状,然后再决定是用画笔把这个形状涂满,还是只描一个边儿?

在 canvas 中,所有绘图的动作,可以分为 “填充”“描绘” 两类。填充就是 fill,意味着填充某一个形状;描绘就是 stroke,意味着画一些线条;可以理解的再直白一点:fill 想象成用颜料涂,stroke 则是用彩笔画。

理解到这里,我们再看如何分别 “填充” 和 “描绘” 一个矩形:

html
<canvas id="canvas"></canvas>
<script>
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  // ctx 就是 Canvas 2D API 实例对象

  ctx.fillStyle = "blue"; // 填充颜色
  ctx.fillRect(10, 10, 100, 100); // 填充矩形

  ctx.strokeStyle = "red"; // 线条颜色
  ctx.strokeRect(130, 10, 100, 100); // 描绘矩形
</script>

在浏览器中效果如图 2-10 所示:

图2-10

Canvas 中第三个需要理解的关键词是“路径”。路径是什么意思呢?就是你拿起一支笔,随便怎么画,最后画出来的线条轨迹一定是明确的。这根线条从你开始画的第一个点,到结束时的最后一个点,两点之间的轨迹就是你画出来的“路径”。

路径可以看作是由无数的点(短线)组合而成的线条,我们需要关注的是,第一个点在哪里?然后接着怎么走,左拐还是右拐,走多远(线条多长)。

一句话总结:“从哪开始,走多远,拐几次”。

从哪开始通过 moveTo(x,y) 设置,走多远用 lineTo(x1, y1) 决定。接下来如果要沿着上一次的位置继续走,那么继续用 lineTo(x2, y2) 指向下一个位置,拐弯的坐标差就是 [x2-x1, y2-y1]

如果画完一笔,要换一个起点继续画,那么就设置 moveTo(x,y) 到一个新位置,接着用 lineTo(x1, y1) 继续画。按照这个思路,我们假设用笔写一个 “大” 字:

html
<canvas id="canvas"></canvas>
<script>
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  // ctx 就是 Canvas 2D API 实例对象

  ctx.fillStyle = "#ddd"; // 填充颜色
  ctx.fillRect(10, 10, 400, 100); // 填充矩形

  ctx.beginPath(); // 开始画路径
  ctx.lineWidth = "2";

  // 横
  ctx.moveTo(60, 55);
  ctx.lineTo(90, 55);

  // 撇
  ctx.moveTo(75, 40);
  ctx.lineTo(74, 55);
  ctx.lineTo(72, 66);
  ctx.lineTo(68, 73);
  ctx.lineTo(60, 80);

  // 捺
  ctx.moveTo(74, 55);
  ctx.lineTo(77, 66);
  ctx.lineTo(82, 74);
  ctx.lineTo(89, 80);

  ctx.stroke(); // 描绘路径
</script>

绘制结果如图 2-11 所示:

图2-11

当然了写的比较粗糙,但是依然能看出来对 “拐弯” 处的一些细节处理,有点手写的意思。

如果理解了上述的 “填充、描绘、路径” 三个概念,那么下一步就可以照着 API 文档绘制你喜欢的图形了。