PHP、MySQL 和 JavaScript 学习指南第六版(九)
原文:
zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f译者:飞龙
第二十三章:介绍 jQuery Mobile
现在你已经意识到了可以从 jQuery 中节省的时间和可以利用的巨大力量,正如在第二十二章中讨论的那样,我认为你会很高兴地发现你可以使用 jQuery Mobile 库做更多的事情。
为了配合 jQuery,jQuery Mobile 要求在网页中同时包含 jQuery 和 jQuery Mobile(还需要一个 CSS 文件和相关图像),以便在手机和其他移动设备上查看时转变为完全互动的体验。
jQuery Mobile 库使你能够通过所谓的渐进增强技术(首先使基本浏览器功能显示良好,然后在浏览器能力增强时逐步添加更多功能)将普通网页适配为移动网页。它还具备所谓的响应式 Web 设计(使网页在各种设备和窗口或屏幕尺寸上显示良好)的特性。
本章的重点并不是要教会你关于 jQuery Mobile 的所有知识——那本身就可以写成一本完整的书!相反,我想给你足够的信息,让你能够重新设计任何不太大的网页集合,使其成为一个连贯、快速且美观的 Web 应用程序,具备现代触摸设备所期望的所有页面滑动和其他过渡效果,以及更大更易用的图标、输入字段和其他增强的输入和导航功能。
为此,我介绍了 jQuery Mobile 的一些主要功能,这些功能可以让你快速上手,获得一个在桌面和移动平台上都运行良好的清晰且可操作的解决方案。在此过程中,我指出了一些你在将网页适配到移动端时可能会遇到的陷阱以及如何避免它们。一旦掌握了使用 jQuery Mobile,你将很容易通过在线文档找到适合你自己项目需求的功能。
注意
除了根据浏览器的能力逐步增强 HTML 显示外,jQuery Mobile 还会根据使用的标签和一组自定义数据属性逐步增强常规的 HTML 标记。某些元素可以自动增强,无需任何数据属性(例如,select 元素会自动升级为菜单),而其他元素则需要存在数据属性才能进行增强。支持的完整数据属性列表可以在API 文档中查看。
包括 jQuery Mobile
有两种方法可以在你的网页中包含 jQuery Mobile。首先,你可以访问下载页面,选择你需要的版本,将文件下载到你的 Web 服务器(包括与库配套的样式表和附带的图像),然后从那里提供它们。
例如,如果你已经下载了 jQuery Mobile 1.4.5(我撰写时的当前版本)及其 CSS 文件到服务器的文档根目录,你可以包含它们以及相应的 jQuery JavaScript 文件,我写作时必须是 2.2.4 版本。我应该指出,自 jQuery Mobile 最后更新以来已经有一段时间了,我想知道其他技术是否会很快超越它:
<link href="http://myserver.com/jquery.mobile-1.4.5.min.css" rel="stylesheet">
<script src='http://myserver.com/jquery-2.2.4.min.js'></script>
<script src='http://myserver.com/jquery.mobile-1.4.5.min.js'></script>
或者,就像 jQuery 一样,你可以利用免费的内容交付网络(CDN),简单地链接到你需要的版本。有三个主要的 CDN 可供选择(Max CDN,Google CDN 和 Microsoft CDN),你可以按以下方式从中获取你需要的文件:
<!-- Retrieving jQuery & Mobile via Max CDN -->
<link rel="stylesheet"
href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css">
<script src="http://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js">
</script>
<!-- Retrieving jQuery & Mobile via Google CDN -->
<link rel="stylesheet" href=
"http://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
<script src=
"http://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src=
"http://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js">
</script>
<!-- Retrieving jQuery & Mobile via Microsoft CDN -->
<link rel="stylesheet" href=
"http://ajax.aspnetcdn.com/ajax/jquery.mobile/1.4.5/jquery.mobile-1.4.5.min.css">
<script src=
"http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.4.min.js"></script>
<script src=
"http://ajax.aspnetcdn.com/ajax/jquery.mobile/1.4.5/jquery.mobile-1.4.5.min.js">
</script>
你可能希望将这些语句中的一组放置在页面的<head>部分。
注意
为了确保你可以在离线状态下使用这些示例,我已经下载了所有所需的 jQuery 文件,并将它们与示例文件的归档一起包含在内,你可以从GitHub免费下载。因此,所有这些示例都显示本地提供的文件。
入门指南
让我们立即深入了解一个 jQuery Mobile 网页的一般外观,参见示例 23-1。这实际上非常简单,如果你快速浏览一下,它将有助于快速理解本章的其余内容。
示例 23-1. jQuery Mobile 单页面模板
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Single page template</title>
<link rel="stylesheet" href="jquery.mobile-1.4.5.min.css">
<script src="jquery-2.2.4.min.js"></script>
<script src="jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
<div data-role="page">
<div data-role="header">
<h1>Single page</h1>
</div>
<div data-role="content">
<p>This is a single page boilerplate template</p>
</div>
<div data-role="footer">
<h4>Footer content</h4>
</div>
</div>
</body>
</html>
仔细检查前面的示例,你会发现它从你期望的标准 HTML5 元素开始。你可能会注意到的第一个不寻常的项目是在<head>部分,即在<meta>标签内的viewport设置。如今,大多数用户将在移动设备上浏览,因此这个标签应该出现在你的所有网页中。
这行代码告诉移动浏览器将显示文档的宽度设置为与 Web 浏览器相同,并且开始时不缩放文档。在高度大于宽度的浏览器中显示时,页面看起来像图 23-1 所示。
图 23-1. 显示 jQuery Mobile 单页面模板
在指定页面标题后,加载了 jQuery Mobile 的 CSS,然后是 jQuery 2.2.4 库和 jQuery Mobile 1.4.5 库。如“包含 jQuery Mobile”中所解释的,如果你愿意,这些文件都可以从 CDN 下载。
注意
本章的示例包含一个名为images的文件夹,其中包含 CSS 所需的所有图标和其他图片。如果您使用 CDN 下载 CSS 和 JavaScript 库文件,可能不需要在您自己的项目中包含此文件夹,因为 CDN 的images文件夹应该被使用。
转到<body>部分,您会注意到主网页包含在一个<div>元素内,该元素具有 jQuery Mobile data-role属性值为page,并包括进一步的三个<div>元素,用于页面的头部、内容和页脚,每个元素都有相应的data-role属性。
现在您已经了解了 jQuery Mobile 网页的基本结构。当您在页面之间进行链接时,新页面将通过异步通信加载并添加到 DOM 中。一旦加载完成,页面就可以通过多种方式进行过渡显示,包括即时替换、淡入、溶解、滑动等。
注意
由于网页的异步加载,您应始终在 Web 服务器上测试您的 jQuery Mobile 代码,而不是在本地文件系统上。这是因为 Web 服务器知道如何处理网页的异步加载,这对通信的正确工作是必要的。AMPPS 系统足以做到这一点,只要您使用localhost://或http://127.0.0.1/访问文件。
链接页面
使用 jQuery Mobile,您可以以正常方式链接到页面,并且它将自动处理这些页面请求的异步加载(在可能的情况下),以确保应用所选的任何过渡效果。
这使您可以专注于简单创建您的网页,并让 jQuery Mobile 处理使它们看起来好看并且显示迅速和专业。
为了启用动画页面过渡效果,所有指向外部页面的链接将被异步加载。jQuery Mobile 通过将所有<a href...>链接转换为异步通信(也称为 Ajax)请求,并在请求完成时显示加载旋转器来实现这一点。显然,这仅适用于内部页面链接。
注意
当您点击链接时,jQuery Mobile 实现页面过渡的方式是通过“劫持”点击事件,并访问event.preventDefault事件,然后提供其特殊的 jQuery Mobile 代码。
如果请求成功,新页面内容将添加到 DOM 中,然后使用默认页面过渡或您选择的任何过渡效果将所选的新页面动画显示出来。
如果异步请求失败,会显示一个小巧而不显眼的错误消息让您知晓,但不会干扰导航流程。
同步链接
链接指向其他域或具有rel="external"、data-ajax="false"或target属性将同步加载,导致页面完全刷新而无动画过渡。
rel="external" 和 data-ajax="false" 具有相同的效果,但前者用于链接到另一个站点或域,而后者用于防止任何页面异步加载。
由于安全限制,jQuery Mobile 会同步加载所有外部域的页面。
注意
在使用 HTML 文件上传时,您需要禁用异步页面加载,因为这种获取网页的方式与 jQuery Mobile 接收上传文件的能力冲突。对于这种情况,最好的方法可能是在 <form> 元素中添加 data-ajax="false" 属性,如下所示:
<form `data-ajax=``'false'` method='post'
action='*`dest_file`*' enctype='multipart/form-data'>
在多页文档中链接到内部位置
单个 HTML 文档可以包含一个或多个页面。后者涉及堆叠多个 data-role 为 page 的 <div> 元素,这允许您在一个单一 HTML 文档中构建一个小站点或应用程序;jQuery Mobile 将简单地在加载页面时显示源顺序中找到的第一个页面。
如果多页文档中的链接指向锚点(例如 #page2),框架将查找带有 data-role 属性为 page 和给定 ID(id="page2")的页面包装 <div>。如果找到,则将新页面过渡到视图中。
用户可以在 jQuery Mobile 中无缝导航到所有类型的网页(无论是内部、本地还是外部)。对于最终用户来说,所有页面看起来都一样,只是在加载外部页面时会显示 Ajax 加载指示器,而加载的外部页面将替换当前页面,而不是像内部页面那样插入 DOM 以保留所有 jQuery Mobile 功能。在所有情况下,jQuery Mobile 更新页面的 URL 哈希以启用后退按钮支持。这也意味着 jQuery Mobile 页面可以被搜索引擎索引,并且不会像原生应用程序一样封闭在某个地方。
警告
当从异步加载的移动页面链接到包含多个内部页面的页面时,必须在链接中添加rel="external"或data-ajax="false"以强制进行完整页面重新加载,清除掉 URL 中的异步哈希。异步页面使用哈希(#)来跟踪其历史记录,而多个内部页面使用此符号来指示内部页面。
页面过渡
通过使用 CSS 过渡效果,jQuery Mobile 可以对任何页面链接或表单提交应用效果,只要使用异步导航(默认情况)。
要应用过渡效果,可以在 <a> 或 <form> 标签中使用 data-transition 属性,如下所示:
<a `data-transition=``"slide"` href="destination.html">Click me</a>
此属性支持值 fade(自版本 1.1 起的默认值),pop,flip,turn,flow,slidefade,slide(版本 1.1 之前的默认值),slideup,slidedown 和 none。
例如,值 slide 使新页面从右侧滑入,同时当前页面从左侧滑出。其他值的效果类似明显。
将页面加载为对话框
你可以使用 data-rel 属性并将其值设为 dialog,将新页面显示为对话框窗口,就像这样:
<a `data-rel=``"dialog"` href="dialog.html">Open dialog</a>
示例 23-2 演示了如何将各种页面过渡效果应用于页面加载和对话框加载,本地加载 jQuery 库而不是通过 CDN。它由一个简单的表格组成,有两列,第一列用于加载对话框,另一列用于加载新页面。列出了每种可用的过渡类型。为了将链接显示为按钮,我为每个链接的 data-role 属性提供了值 button(按钮的样式见“按钮样式”)。
示例 23-2. jQuery Mobile 页面过渡
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Transitions</title>
<link rel="stylesheet" href="jquery.mobile-1.4.5.min.css">
<script src="jquery-2.2.4.min.js"></script>
<script src="jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
<div data-role="page">
<div data-role="header">
<h1>jQuery Mobile Page Transitions</h1>
</div>
<div data-role="content"><table>
<tr><th><h3>fade</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="fade" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="fade"
data-role='button'>page</a></td>
</tr><tr><th><h3>pop</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="pop" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="pop"
data-role='button'>page</a></td>
</tr><tr><th><h3>flip</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="flip" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="flip"
data-role='button'>page</a></td>
</tr><tr><th><h3>turn</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="turn" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="turn"
data-role='button'>page</a></td>
</tr><tr><th><h3>flow</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="flow" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="flow"
data-role='button'>page</a></td>
</tr><tr><th><h3>slidefade</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="slidefade" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="slidefade"
data-role='button'>page</a></td>
</tr><tr><th><h3>slide</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="slide" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="slide"
data-role='button'>page</a></td>
</tr><tr><th><h3>slideup</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="slideup" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="slideup"
data-role='button'>page</a></td>
</tr><tr><th><h3>slidedown</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="slidedown" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="slidedown"
data-role='button'>page</a></td>
</tr><tr><th><h3>none</h3></th>
<td><a href="page-template.html" data-rel="dialog"
data-transition="none" data-role='button'>dialog</a></td>
<td><a href="page-template.html" data-transition="none"
data-role='button'>page</a></td></tr></table>
</div>
<div data-role="footer">
<h4><a href="http://tinyurl.com/jqm-trans">Official Demo</a></h4>
</div>
</div>
</body>
</html>
图 23-2 展示了加载此示例(保存为 transitions.html 文件名)到浏览器的结果,而 图 23-3 展示了翻转过渡的实际效果。顺便提一下,如果你点击示例页脚中的链接,将带你到官方演示站点,你可以更详细地了解这些效果。
图 23-2. 对页面和对话框应用过渡效果
图 23-3. 翻转过渡进行中
按钮样式
你可以轻松地将简单链接显示为按钮,而无需添加自定义 CSS。你所需做的就是向元素的 data-role 属性提供值 button,就像这样:
<a `data-role=``"button"` href="news.html">Latest news</a>
你可以决定是否使按钮展开到窗口的整个宽度(默认),就像 <div> 元素一样,或者以行内显示,就像 <span> 元素一样。要行内显示按钮,请向 data-inline 属性提供值 true,就像这样:
<a data-role="button" `data-inline=``"true"` href="news.html">Latest news</a>
无论你是从链接创建按钮还是从表单中使用按钮,你都可以通过选择圆角(默认)或直角,并使其具有阴影(默认)或无阴影的方式来修改其显示方式。通过分别向 data-corners 和 data-shadow 属性提供值 false 来关闭这些功能,就像这样:
<a data-role="button" data-inline="true" `data-corners=``"false"`
`data-shadow=``"false"` href="news.html">Latest news</a>
此外,你甚至可以选择使用 data-icon 属性为按钮添加图标,就像这样:
<a data-role="button" data-inline="true" `data-icon=``"home"`
href="home.html">Home page</a>
有超过 50 种现成的图标可供选择。它们全部使用一种称为可缩放矢量图形(SVG)的强大图形语言创建,并针对不支持 SVG 的设备回退到 PNG 格式,使得图标在视网膜显示屏上表现出色。查看图标演示以了解现有的图标。
默认情况下,图标显示在按钮文本的左侧,但你可以选择将它们放置在文本的右侧、上方、下方,或者通过向 data-iconpos 属性应用值 right、top、bottom 或 notext 来移除任何文本,就像这样:
<a data-role="button" data-inline="true" data-icon="home"
`data-iconpos=``"right"` href="home.html">Home page</a>
如果选择不显示任何按钮文字,则默认显示圆形按钮。
最后,在这个简短的按钮样式概述中,您可以通过向data-mini属性提供true值来选择显示较小的按钮(包括按钮文本),如下所示:
<a data-role="button" data-inline="true" data-icon="home"
`data-mini=``"true"` href="home.html">Home page</a>
示例 23-3 演示了使用这些按钮样式创建多种按钮的选择(为简洁起见没有href属性),如图 23-4 所示。
示例 23-3. 各种按钮元素
<a data-role="button">Default</a>
<a data-role="button" data-inline="true">In-line</a>
<a data-role="button" data-inline="true"
data-corners="false">Squared corners</a>
<a data-role="button" data-inline="true"
data-shadow="false">Unshadowed</a>
<a data-role="button" data-inline="true" data-corners="false"
data-shadow="false">Both</a><br>
<a data-role="button" data-inline="true"
data-icon="home">Left icon</a>
<a data-role="button" data-inline="true" data-icon="home"
data-iconpos="right">Right icon</a>
<a data-role="button" data-inline="true" data-icon="home"
data-iconpos="top">Top icon</a>
<a data-role="button" data-inline="true" data-icon="home"
data-iconpos="bottom">Bottom icon</a><br>
<a data-role="button" data-mini="true">Default Mini</a>
图 23-4. 各种按钮样式的组合
实际上,您可以使用按钮进行更多的样式设置,您可以在按钮演示中找到所需的所有详细信息。但现在,这个介绍将对您有所帮助。
列表处理
在处理列表时,jQuery Mobile 确实为您提供了一系列易于使用的功能,所有这些功能都可以通过将data-role属性设置为listview来访问<ul>或<ol>元素。
因此,例如,要创建简单的无序列表,您可以使用以下代码:
<ul `data-role=``"listview"`>
<li>Broccoli</li>
<li>Carrots</li>
<li>Lettuce</li>
</ul>
对于有序列表,只需将<ul>的开放和闭合标签替换为<ol>,列表将被编号。
列表中的任何链接将自动嵌入箭头图标并显示为按钮。您还可以通过给data-inset属性赋值true来将列表插入到页面上的其他内容中进行混合。
示例 23-4 演示了这些不同特性在实践中的应用;结果显示在图 23-5 中。
示例 23-4. 各种列表选择
<ul data-role="listview">
<li>An</li>
<li>Unordered</li>
<li>List</li>
</ul><br><br>
<ol data-role="listview">
<li>An</li>
<li>Ordered</li>
<li>List</li>
</ol><br>
<ul data-role="listview" data-inset="true">
<li>An</li>
<li>Inset Unordered</li>
<li>List</li>
</ul>
<ul data-role="listview" data-inset="true">
<li><a href='#'>An</a></li>
<li><a href='#'>Inset Unordered</a></li>
<li><a href='#'>Linked List</a></li>
</ul>
图 23-5. 有序和无序的普通和插入列表
可过滤列表
您可以通过将data-filter属性设置为true使列表可过滤,这将在列表上方放置一个搜索框,并且当用户输入时,将自动隐藏任何不匹配搜索词的列表元素。您还可以将data-filter-reveal设置为true,以便在至少输入一个字符的过滤输入之前不显示任何字段,然后仅显示与输入匹配的字段。
示例 23-5 演示了这两种类型的过滤列表的使用,仅通过向后者添加data-filter-reveal="true"来区分。
示例 23-5. 过滤和过滤显示列表
<ul data-role="listview" data-filter="true"
data-filter-placeholder="Search big cats..." data-inset="true">
<li>Cheetah</li>
<li>Cougar</li>
<li>Jaguar</li>
<li>Leopard</li>
<li>Lion</li>
<li>Snow Leopard</li>
<li>Tiger</li>
</ul>
<ul data-role="listview" data-filter="true" `data-filter-reveal=``"true"`
data-filter-placeholder="Search big cats..." data-inset="true">
<li>Cheetah</li>
<li>Cougar</li>
<li>Jaguar</li>
<li>Leopard</li>
<li>Lion</li>
<li>Snow Leopard</li>
<li>Tiger</li>
</ul>
注意使用data-filter-placeholder属性在输入字段为空时为用户提供提示。
在图 23-6 中,您可以看到前一种列表类型已经输入了字母a,因此当前只显示具有a的字段,而第二个列表中没有显示任何字段,因为过滤字段尚未输入任何内容。
图 23-6. 显示过滤和过滤显示列表
列表分隔符
为了增强列表的显示方式,您还可以在其中放置手动或自动分隔符。通过为具有data-role属性值为list-divider的列表元素提供支持,您可以创建手动列表分隔符,如例 23-6 所示,显示为图 23-7。
例 23-6. 手动列表分隔符
<ul data-role="listview" data-inset="true">
`<li` `data-role=``"list-divider"``>``Big Cats``</li>`
<li>Cheetah</li>
<li>Cougar</li>
<li>Jaguar</li>
<li>Lion</li>
<li>Snow Leopard</li>
`<li` `data-role=``"list-divider"``>``Big Dogs``</li>`
<li>Bloodhound</li>
<li>Doberman Pinscher</li>
<li>Great Dane</li>
<li>Mastiff</li>
<li>Rottweiler</li>
</ul>
图 23-7. 列表按类别划分
为了让 jQuery Mobile 以方便的方式确定分隔,您可以为data-autodividers属性提供值true,如例 23-7 所示,它按字母顺序划分字段并显示为图 23-8。
例 23-7. 使用自动分隔符
<ul data-role="listview" data-inset="true" data-autodividers="true">
<li>Cheetah</li>
<li>Cougar</li>
<li>Jaguar</li>
<li>Leopard</li>
<li>Lion</li>
<li>Snow Leopard</li>
<li>Tiger</li>
</ul>
图 23-8. 自动按字母顺序划分列表
与按钮一样(请参见“样式化按钮”),您还可以使用data-icon属性为链接列表字段添加图标,并提供一个表示要显示的图标的值,就像这样:
<li `data-icon=``"gear"`><a href="settings.html">Settings</a></li>
在这个示例中,链接列表的默认右尖括号将被您选择的图标替换(在本例中是齿轮图标)。
除了所有这些出色的功能外,您还可以在列表字段中添加图标和缩略图,并且它们将被缩放以在显示时呈现良好。有关如何执行此操作以及关于许多其他列表功能的详细信息,请参阅官方文档。
下一步是什么?
正如我在开头提到的,本章的目的是快速使您熟悉 jQuery Mobile,以便您可以轻松地将网站重新打包成在所有设备上(无论是桌面还是移动设备)看起来都很好的 Web 应用程序。
为此,我仅介绍了 jQuery Mobile 非常好的和最重要的功能,所以我只是初步介绍了您可以利用它做的事情的皮毛。例如,您可以以多种方式增强和使表单在移动设备上运行良好。您可以构建响应式表格,创建可折叠内容,调用弹出窗口,设计自己的主题等等。
注意
您可能会对知道您可以与 Apache 的产品Cordova结合使用 jQuery Mobile 来构建适用于 Android 和 iOS 的独立应用程序感兴趣。这并不是完全简单明了的事情,这超出了本书的范围,但大部分艰苦工作已经为您完成。
一旦您掌握了本章的所有内容,如果您想了解 jQuery Mobile 还能为您提供什么帮助,我建议您查看官方演示和网站文档。
此外,第二十九章的示例社交网络应用程序在接近真实世界的情景中应用了许多这些功能,这是了解如何使您的网页移动化的绝佳方法。在此之前,我们将看看最受欢迎且快速增长的 JavaScript 框架之一,React。
问题
-
使用 CDN 将 jQuery Mobile 传送到 Web 浏览器有哪些主要好处和一个缺点?
-
你会使用什么 HTML 来定义 jQuery Mobile 的内容页面?
-
组成 jQuery 页面的三个主要部分是什么,并且它们如何表示?
-
如何在 HTML 文档中放置多个 jQuery Mobile 页面?
-
如何阻止网页异步加载?
-
如何将锚点的页面转换设置为翻转,而不是使用默认的淡入效果?
-
如何加载页面,使其显示为对话框而不是网页?
-
如何轻松地使锚点链接显示为按钮?
-
如何使 jQuery Mobile 元素像
<span>元素一样内联显示,而不是像<div>元素一样全宽? -
如何为按钮添加图标?
参见“第二十三章答案”,在附录 A 中找到这些问题的答案。
第二十四章:React 简介
当使用 JavaScript、HTML 和 CSS 构建动态网站时,随着时间推移,处理网站和应用程序前端所需代码的创建可能会变得冗长且啰嗦,从而降低项目开发速度,并可能引入难以找到的错误。
这就是框架的用武之地。当然,自 2006 年以来,我们有 jQuery 来帮助我们,因此它被安装在绝大多数的生产网站上,尽管如今 JavaScript 在范围和灵活性上已经足够成熟,程序员不再需要过度依赖像 jQuery 这样的框架。随着技术的不断进步,现在还有许多其他出色的选择,比如 Angular 和我在这里讨论的我的首选,React。
jQuery 的设计目的是简化 HTML DOM 树的遍历和操作,以及事件处理、CSS 动画和 Ajax,但是一些程序员,比如谷歌的开发团队,认为它仍然不够强大,因此他们在 2010 年推出了 Angular JS,这在 2016 年演变为 Angular。
Angular 将组件层次结构作为其主要的架构特征,而不是像 Angular 那样使用“scope”或控制器。谷歌庞大的 AdWords 平台以及 Forbes、Autodesk、Indiegogo、UPS 等公司都在使用 Angular,它确实非常强大。
另一方面,Facebook 则有不同的愿景,推出了 React(也称为 React JS)作为其开发单页或移动应用程序的框架,并围绕 JSX 扩展(代表 JavaScript XML)构建。React 库(首次开发于 2012 年)将网页划分为单一组件,简化了开发所需的接口,以服务于 Facebook 所有的广告及更多内容,现在它被网上的各种平台广泛采用,如 Dropbox、Cloudflare、Airbnb、Netflix、BBC、PayPal 等众多知名企业。
显然,Angular 和 React 在它们的创作和设计过程中都受到了坚实的商业决策的驱动,并且它们都是为了处理极高流量的网页而构建的,而 jQuery 在开发者寻求的功能方面显得力不从心。
因此,如今,除了要了解 JavaScript、HTML、CSS 等核心技术,还应该掌握一些 jQuery 以及至少了解 Angular 和 React 其中之一(如果不是两者都要),以及可能其他一些也有自己追随者的框架,这对程序员来说可能会很有用。
然而,基于易用性、学习曲线不陡、普遍实施以及因 Google 趋势显示它是三者中最受欢迎的框架(见图 24-1),我决定向你介绍 React 更为重要。顺便说一句,请不要将同名的 ReactPHP 与 JavaScript 的 React 混淆,它们是完全独立且不相关的项目。
React 究竟有什么意义?
React 允许开发者创建能够轻松处理和改变数据而无需重新加载网页的大型 Web 应用程序。它的主要存在理由是速度、可扩展性以及简化处理单页 Web 和移动应用程序的视图层。它还能创建可重用的 UI 组件,并管理虚拟 DOM 以提升性能。有人曾说过,你可以将其用作将应用程序分成三个组件(模型、视图、控制器)中的 V(视图)。
开发者无需费力找出各种描述接口交易的方式,只需描述最终状态下的接口,使得当交易发生在该状态时,React 会自动更新 UI。其结果是更快、更少 bug 的开发;速度、可靠性和可扩展性。因为 React 是一个库而不是框架,学习它也很快,只需掌握几个函数。之后,一切都取决于你的 JavaScript 技能。
那么,让我们开始学习如何访问 React 文件。
图 24-1. 根据 Google 趋势,最近 jQuery、Angular 和 React 的受欢迎程度
注意
我觉得 jQuery 很棒,但我也发现 React 非常易于使用,我认为时间会证明我对 React 最终能否取代 jQuery 成为主导框架(尤其是处理 UI 方面)的猜测是否正确。即使不是这样,学习 React 仍将使你掌握一种非常强大的新工具,许多顶级公司都会在你的简历上看重它。同时也不要忽视 Angular。如果这本书的篇幅足够,我也会介绍 Angular,因为了解它对于调试和维护现有代码非常重要,即使你不使用它开发。你可以在 angular.io 找到关于 Angular 的所有信息(这也会让你的简历更上一层楼)。
访问 React 文件
像 jQuery 和 Angular 一样,React 是开源的,完全免费使用。也像其他框架一样,在网上有许多服务可以免费为你提供最新(或任何版本)的文件,因此使用它可以像在你的网页中添加几行额外的代码一样简单。
在探讨 React 的功能和如何使用它之前,先了解如何将其包含在网页中,从 unpkg.com 拉取文件:
<script
src="https://unpkg.com/react@17/umd/react.development.js">
</script>
<script
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js">
</script>
这些行理想情况下应放在页面的 <head>...</head> 部分,以确保它们在 body 部分加载之前加载。它们加载了 React 和 React DOM 的开发版本,帮助你进行开发和调试。在生产网站上,你应该将这些 URL 中的单词 development 替换为 production,为了加快传输速度,你甚至可以将 development 改为 production.min,这样会调用压缩版本的文件,像这样:
<script
src="https://unpkg.com/react@17/umd/react.production.min.js">
</script>
<script
src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js">
</script>
为了方便访问并尽可能简化代码,我已经从本书示例的伴随存档中下载了最新的(我写作时是版本 17)未压缩开发文件的版本(在GitHub上),因此所有示例都可以本地加载,看起来像这样:
<script src="react.development.js"></script>
<script src="react-dom.development.js"></script>
现在你可以使用 React 编写代码了,接下来呢?虽然这不是必需的,我们接下来引入 Babel JSX 扩展,这样你就可以直接在 JavaScript 中包含 XML 文本,使你的生活变得更加轻松。
包含 babel.js
Babel JSX 扩展允许你直接在 JavaScript 中使用类似 HTML 的 XML,而不需要每次都调用一个函数。此外,在早期版本的 ECMAScript(JavaScript 的官方标准)低于 6 的浏览器上,Babel 将其升级以处理 ES6 语法,因此一举提供了两大好处。
你可以再次从 unpkg.com 服务器上获取所需的文件,就像这样:
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
你只需要在开发或生产服务器上获取一个精简版本的 Babel 代码。为了方便起见,我还从示例文件的伴随存档中下载了最新版本,因此本书中的示例可以本地加载,看起来像这样:
<script src="babel.min.js"></script>
现在我们可以访问 React 文件了,让我们开始做些实际的事情。
注意
本章旨在教你使用 React 的基础知识,让你清楚地理解它的工作原理和意义,并为你提供一个良好的起点,以便进一步发展你的 React 开发。事实上,本章中的一些示例基于(或类似于)你可以在 reactjs.org 官方文档中找到的示例,因此,如果你希望更深入地学习 React,可以访问该网站,将会有一个良好的起步。
我们的第一个 React 项目
不要在实际编码之前教你所有有关 React 和 JSX 的知识,让我们换个角度,通过直接跳入我们的第一个 React 项目来展示一切有多简单,如示例 24-1 所示,其结果仅在浏览器中显示文本“By Jeeves, it works!”。
示例 24-1. 我们的第一个 React 项目
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>First React Project</title>
<script src='react.development.js' ></script>
<script src='react-dom.development.js'></script>
<script src="babel.min.js"> </script>
<script type="text/babel">
`class` `One` `extends` `React``.``Component`
`{`
`render``(``)`
`{`
`return` `<``p``>``By` `Jeeves``,` `it` `works``!``<``/``p``>`
`}`
`}`
`ReactDOM``.``render``(``<``One` `/``>``,` `document``.``getElementById``(``'div1'``)``)`
</script>
</head>
<body>
<div id="div1" style='font-family:monospace'></div>
</body>
</html>
这是一个标准的 HTML5 文档,在打开内联脚本之前加载了两个 React 脚本和 Babel 脚本。这里是我们首次需要注意的地方,因为 script 标签不是不指定类型,也不是使用 type=application/javascript,而是给定了 type=text/babel。这告诉浏览器允许 Babel 预处理器运行脚本,必要时添加 ES6 功能,并将遇到的任何 XML 替换为 JavaScript 函数调用,然后再将脚本内容作为 JavaScript 运行。
在脚本中,创建了一个名为One的新类,扩展了React.component类。在这个类内部,创建了一个render方法,它返回以下 XML(不是字符串):
<p>By Jeeves, it works!</p>
最后,在脚本中调用了 One 类的 render 函数,向它传递了文档主体中唯一 <div> 元素的 ID,该元素的 ID 被命名为 div1。结果是将 XML 渲染到 div 中,这会导致浏览器自动更新并显示内容,效果如下所示:
By Jeeves, it works!
立即你应该看到在 JavaScript 中包含 XML 如何使得编写代码更加简单和快速,并且使其更容易理解。如果没有 JSX 扩展,你将不得不使用一系列 JavaScript 函数调用来完成所有这些工作。
注意
React 将以小写字母开头的组件视为 DOM 标签。因此,例如,<div /> 表示 HTML 的 <div> 标签,但 <One /> 表示一个组件,并且需要 One 在作用域内 —— 你不能像之前的例子中使用小写字母 one 并期望你的代码能工作,因为组件的名称必须以大写字母开头,任何对它的引用也是如此。
使用函数而不是类
如果你愿意,并且作为越来越普遍的做法,你可以使用一个函数来编写你的代码,而不是将它放在带有 render 函数的类中,就像在 Example 24-2 中一样。你可能更喜欢这样做的主要原因是简单性、易用性和更快的开发速度。
Example 24-2. 使用函数而不是类
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>First React Project</title>
<script src='react.development.js' ></script>
<script src='react-dom.development.js'></script>
<script src="babel.min.js"> </script>
<script type="text/babel">
`function` `Two``(``)`
`{`
`return` `<``p``>``And` `this``,` `by` `Jove``!``<``/``p``>`
`}`
ReactDOM.render(`<``Two` `/``>`, document.getElementById(`'div2'`))
</script>
</head>
<body>
<div id="div2" style='font-family:monospace'></div>
</body>
</html>
在浏览器中显示的结果是:
And this, by Jove!
注意
从这一点开始的示例中,为了简洁起见,我将只显示 Babel 脚本的内容和文档主体,好像它们都在主体中(这样也同样有效),但附带存档中的示例 将 是完整的。因此,从现在开始它们看起来是这样的:
<script type="text/babel">
function Two()
{
return <p>And this, by Jove!</p>
}
ReactDOM.render(<Two />,
document.getElementById('div2'))
</script>
<div id="div2"></div>
纯函数与不纯函数的黄金法则
当你写一个普通的 JavaScript 函数时,可以写出 React 称为 pure 或 impure 的代码。纯函数代码不会改变其输入,就像下面的这个例子,它返回从其参数计算得到的值:
function mult(m1, m2)
{
return m1 * m2
}
然而,这个函数被认为是不纯的,因为它修改了一个参数,在使用 React 时绝对不允许:
function assign(obj, val)
{
obj.value = val
}
以黄金规则的形式表达,这意味着所有 React 组件必须在其 props 方面像纯函数一样操作,正如在“属性和组件”中解释的那样。
同时使用类和函数
当然,你可以几乎可以互换地使用函数和类(尽管它们之间有一些区别,稍后在“使用类和函数的区别”中会进行解释),就像在示例 24-3 中一样。
示例 24-3. 同时使用类和函数
<script type="text/babel">
class One extends React.Component
{
render()
{
return <p>By Jeeves, it works!</p>
}
}
function Two()
{
return <p>And this, by Jove!</p>
}
`doRender``(``<``One` `/``>``,` `'div1'``)`
`doRender``(``<``Two` `/``>``,` `'div2'``)`
`function` `doRender``(``elem``,` `dest``)`
{
ReactDOM.render(`elem`, document.getElementById(`dest`)) `}`
</script>
<div id="div1" style="font-family:monospace"></div>
<div id="div2" style="font-family:monospace"></div>
这里有一个名为One的类和一个名为Two的函数,它们与前两个示例中的相同。不过,这里还有一个额外的区别——创建了一个称为doRender的新函数——它大大简化了呈现 XML 块所需的语法。运行结果在浏览器中显示为:
By Jeeves, it works!
And this, by Jove!
注意
除了以下示例中的周围 HTML 代码,我也将不再重复出现doRender函数的代码。因此,当你在这些示例中看到对doRender函数的调用时,请记住它不是 React 的内置函数,而是包含在配套档案中的函数。
属性和组件
介绍 React 称为props和components的一种绝佳方式,是构建一个简单的欢迎页面,其中将一个名称传递给脚本,然后显示出来。示例 24-4 是实现这一目标的一种方式。组件允许您将 UI 拆分为单独的、可重用的部分,并在隔离的环境中处理每个部分。它们类似于 JavaScript 函数,接受称为props的任意输入,返回描述元素在浏览器中如何显示的 React 元素。
示例 24-4. 将props传递给函数
<script type="text/babel">
function Welcome(props)
{
return <h1>Hello, {props.name}</h1>
}
doRender(<Welcome name='Robin' />, 'hello')
</script>
<div id="hello" style='font-family:monospace'></div>
在这个例子中,Welcome函数接收一个props参数,代表属性,在其 JSX 的return语句中有一个花括号内的部分,从props对象中获取了name属性,像这样:
return <h1>Hello, {props.name}</h1>
props是 React 中的一个对象,接下来你将看到一种将其填充为属性的方法。
注意
使用花括号是在 JSX 中嵌入表达式的方法。实际上,你几乎可以将任何 JavaScript 表达式放在这些花括号中进行评估(除了for和if语句,这些不能被评估)。
因此,在这个例子中,你可以输入76 / 13或"decode".substr(-4)来代替props.name(这将评估为字符串"code")。但是,在这种情况下,属性name是从props对象中获取并返回的。
最后,doRender函数(你会记得它是一个调用ReactDOM.render函数的简写)被传入了Welcome函数的名称,接着将字符串值'Robin'分配给它的name属性,就像这样:
doRender(<Welcome name='Robin' />, 'hello')
然后 React 调用Welcome函数(也称为组件),将{name: 'Robin'}传递给它的props。然后Welcome评估并返回<h1>Hello, Robin</h1>作为其结果,然后渲染到名为hello的div中,并在浏览器中呈现如下:
Hello, Robin
要使您的代码更整洁,如果愿意,您还可以首先创建一个包含 XML 的元素传递给doRender的方法,就像这样:
const elem = <Welcome name='Robin' />
doRender(elem, 'hello')
使用类和函数的区别
在 React 中使用类和函数最明显的区别是语法。函数是简单的 JavaScript(可能包含 JSX),可以接受 props 参数并返回一个组件元素。
然而,类是从React.Component扩展的,需要一个render方法来返回一个组件。但是这些额外的代码确实带来了好处,因为类允许你在组件中使用setState,从而实现(例如)使用定时器和其他状态特性。在 React 中,函数被称为功能状态无关组件。此外,类允许您使用 React 称为生命周期挂钩和方法。在接下来的部分中详细讨论所有这些内容。
本质上,现在你可以使用函数在 React 中做几乎所有的事情。
React 状态和生命周期
假设您希望在网页上显示一个滴答作响的时钟(出于简单起见,使用普通数字时钟)。如果您使用无状态的代码,这不是一件容易的事情,但如果您设置代码以保留其状态,那么时钟计数器可以每秒更新一次,并且时间也可以同样频繁地渲染。这就是您在 React 中使用类而不是函数的地方。因此,让我们构建这样一个时钟:
<script type="text/babel">
class Clock extends React.Component
{
constructor(props)
{
super(props)
this.state = {date: new Date()}
}
render()
{
return <span> {this.state.date.toLocaleTimeString()} </span>
}
}
doRender(<Clock />, 'the_time')
</script>
<p style='font-family:monospace'>The time is: <span id="the_time"></span></p>
此代码将从调用Date函数返回的结果分配给构造函数的this对象的state属性,即props。只要调用了类的render函数,JSX 内容就会被渲染,只会使用类的单个实例,只要它被渲染到相同的 DOM 节点。
注意
您是否看到构造函数开头的super调用?通过将其传递给props,现在可以在构造函数内使用this关键字引用props,否则无法使用。
然而,就目前而言,时间将仅显示一次,然后代码将停止运行。因此,现在我们需要设置一些中断驱动的代码来保持date属性的更新,通过添加一个生命周期方法到类中,通过使用componentDidMount挂载一个定时器,就像这样:
componentDidMount()
{
this.timerID = setInterval(() => this.tick(), 1000)
}
我们还没有完成,因为我们仍然需要编写 tick 函数,但首先,解释前述内容:*mounting* 是 React 用来描述将节点添加到 DOM 的操作。如果组件成功挂载,类的 componentDidMount 方法将始终被调用,因此这是设置中断的理想位置,在前述代码中,this.timerID 被赋予调用 SetInterval 函数返回的 ID,传递给 this.tick 方法,以便每隔 1,000 毫秒(或每秒一次)调用它。
当安装定时器时,我们还必须提供一个方法来 卸载 它,以防止浪费中断周期。在这种情况下,当 Clock 生成的 DOM 被移除(即组件被卸载)时,我们用于停止中断的方法和代码如下所示:
componentWillUnmount()
{
clearInterval(this.timerID)
}
在这里,当 DOM 被移除时 React 会调用 componentWillUnmount;因此,我们在这里放置清除 this.timerID 中存储的间隔的代码,然后将所有这些时间片段返回给系统,因为清除间隔会立即停止 tick 的调用。
最后一个谜题的一部分是每隔 1,000 毫秒调用一次的中断驱动代码,它位于 tick 方法中:
tick()
{
this.setState({date: new Date()})
}
这里调用了 React 的 setState 函数,以更新 state 属性的值,该值是调用 Date 函数的最新结果,每秒钟调用一次。
让我们一起看一个示例中的所有代码,如 示例 24-5 所示。
示例 24-5. 在 React 中构建时钟
<script type="text/babel">
class Clock extends React.Component
{
constructor(props)
{
super(props)
this.state = {date: new Date()}
}
componentDidMount()
{
this.timerID = setInterval(() => this.tick(), 1000)
}
componentWillUnmount()
{
clearInterval(this.timerID)
}
tick()
{
this.setState({date: new Date()})
}
render()
{
return <span> {this.state.date.toLocaleTimeString()} </span>
}
}
doRender(<Clock />, 'the_time')
}
</script>
<p style='font-family:monospace'>The time is: <span id="the_time"></span></p>
现在 Clock 类已经完成,包括构造函数、中断启动和停止器、使用中断更新 state 属性的方法以及 render 函数,现在唯一需要做的就是调用 doRender,使整个时钟运行如图所示在浏览器中:
The time is: 12:17:21
每次调用 setState 函数时,时钟都会自动更新到屏幕,因为组件通过这个函数重新渲染,所以您不必担心在代码中执行此操作。
注意
在初始状态设置后,setState 是更新状态的唯一合法方式,因为简单地直接修改状态不会导致组件重新渲染。请记住,您唯一可以在构造函数中分配 this.state 的地方。React 可能会将多个 setState 调用捆绑成单个更新。
使用 Hooks(如果您使用 Node.js)
如果您使用 Node.js(参见 nodejs.org),您可以使用 hooks 而不必过多依赖类。Node.js 是一个在服务器上直接运行 JavaScript(和 React)的开源服务器环境,这是一个需要多个章节才能适当记录的技术,但如果您已经在使用它,我想告诉您,您也可以利用 React 的新 hooks。
Hooks 是 React 16.8 的一个新加入的功能,它支持在不使用类的情况下访问状态。它们易于使用,并且正在成为 React 的一个增长点。如果你想了解如何使用它们,你可以在线获取更多信息。
React 中的事件
在 React 中,事件使用驼峰命名,并且你使用 JSX 来将函数作为事件处理程序传递,而不是字符串。此外,React 事件的工作方式与原生 JavaScript 事件并不完全相同,因为你的处理程序是通过称为syntheticEvent的浏览器本地事件的跨浏览器包装实例传递的。这样做的原因是 React 标准化事件以在不同浏览器中具有一致的属性。然而,如果你需要访问浏览器事件,你可以始终使用nativeEvent属性来获取它。
为了说明在 React 中使用事件的用法,示例 24-6 是一个简单的onClick事件的例子,当点击时移除或重新显示一些文本。
示例 24-6. 设置一个事件
<script type="text/babel">
class Toggle extends React.Component
{
constructor(props)
{
super(props)
this.state = {isVisible: true}
this.handleClick = this.handleClick.bind(this)
}
handleClick()
{
this.setState(state => ({isVisible: !state.isVisible}))
}
render()
{
const show = this.state.isVisible
return (
<div>
<button onClick={this.handleClick}>
{show ? 'HIDE' : 'DISPLAY'}
</button>
<p>{show ? 'Here is some text' : ''}</p>
</div>
)
}
}
doRender(<Toggle />, 'display')
</script>
<div id="display" style="font-family:monospace"></div>
在一个名为Toggle的新类的构造函数中,设置了一个名为isVisible的属性为true并分配给this.state,如下所示:
this.state = {isVisible: true}
然后,一个名为handleClick的事件处理程序使用bind方法附加到this:
this.handleClick = this.handleClick.bind(this)
构造函数完成后,接下来是handleClick事件处理程序。这有一个单行命令来在isVisible之间切换true和false的状态:
this.setState(state => ({isVisible: !state.isVisible}))
最后,有一个调用render方法的地方,它返回两个元素包装在<div>中。这样做的原因是 render 方法只能返回单个组件(或 XML 标签),所以这两个元素被包装成一个单一元素以满足该要求。
返回的元素是一个按钮,如果以下文本当前隐藏(即isVisible设置为false),则显示文本 DISPLAY,否则如果isVisible设置为true且文本当前可见,则显示文本 HIDE。在此按钮之后,如果isVisible为true,则显示一些文本,否则不显示任何内容(实际上返回空字符串,这是相同的事情)。
为了决定显示什么按钮文本,或者是否显示文本,使用了三元运算符,你可以回忆起其语法如下:
expression ? return this if true : or this if false.
这是通过单词表达式show(其值从this.state.isVisible获取)完成的。如果评估为true,则按钮显示 HIDE 并显示文本,否则按钮显示 DISPLAY 并且文本不显示。加载到浏览器中时,结果如下(其中[HIDE]和[DISPLAY]是按钮):
[HIDE]
Here is some text
当按钮被按下时,它只改变为以下内容:
[DISPLAY]
在多行上使用 JSX
尽管你可以将你的 JSX 跨多行拆分以提高可读性,就像前面的例子一样,但你不能将跟随return命令的括号移到下一行(或任何其他地方)。它必须留在跟在return后面的位置,否则将报告语法错误。然而,关闭括号可以出现在你希望的位置。
内联 JSX 条件语句
在 JSX 中,如果条件为true,则只返回 XML,从而实现条件渲染。这是因为true && expression评估为expression,而false && expression评估为false。
因此,例如,Example 24-7 将两个变量设置为游戏的一部分。this.highScore设置为 90,this.currentScore设置为 100。
Example 24-7. 一个条件性的 JSX 语句
<script type="text/babel">
class Setup extends React.Component
{
constructor(props)
{
super(props)
this.highScore = 90
this.currentScore = 100
}
render()
{
return (
<div>
{
`this``.``currentScore` `>` `this``.``highScore` `&&`
<h1>New High Score</h1>
}
</div>
)
}
}
doRender(<Setup />, 'display')
</script>
<div id='display' style='font-family:monospace'></div>
在这种情况下,如果this.currentScore大于this.highScore,则返回h1元素;否则返回false。代码的结果在浏览器中看起来像这样:
New High Score
当然,在实际游戏中,你将继续将this.highScore设置为this.currentScore的值,并可能在返回游戏代码之前执行一些其他操作。
因此,无论何时只有在条件为true时才显示某些内容,&&运算符都是实现此目的的好方法。当然,你刚才在“React 事件”末尾看到了如何在 JSX 中使用三元(?:)表达式创建if...then...else块。
使用列表和键
使用 React 显示列表非常简单。在 Example 24-8 中,数组cats包含四种类型的猫的列表。然后,在下一行代码中使用map函数提取这些,该函数迭代数组,依次返回每个项目到变量cat中。这导致每次迭代都嵌入在一对<li>...</li>标签中,然后附加到listofCats字符串中。
Example 24-8. 显示列表
<script type="text/babel">
const cats = ['lion', 'tiger', 'cheetah', 'lynx']
const listofCats = `cats``.``map`((cat) => <li>{cat}</li>)
doRender(`<``ul``>``{``listofCats``}``<``/``ul``>`, 'display')
</script>
<div id='display' style='font-family:monospace'></div>
最后,调用doRender,将listofCats嵌入<ul>...</ul>标签对中,结果显示如下:
• lion
• tiger
• cheetah
• lynx
唯一键
如果你在运行 Example 24-8 时打开了 JavaScript 控制台(通常通过按下 Ctrl Shift J 或在 Mac 上按 Option Command J),你可能会注意到警告消息“列表中的每个子元素都应有一个唯一的‘key’属性。”
尽管不是必需的,但当你为每个兄弟列表项提供唯一键时,React 的工作效果最佳,这有助于它找到对应 DOM 节点的引用,并在进行小改动时允许对 DOM 进行微调,而不需要重新渲染更大的部分。所以 Example 24-9 就是提供这样一个唯一键的示例。
Example 24-9. 使用唯一键
<script type="text/babel">
`var` `uniqueId` `=` `0`
const cats = ['lion', 'tiger', 'cheetah', 'lynx']
const listofCats = cats.map((cat) => <li `key``=``{``uniqueId``++``}`>{cat}</li>)
doRender(<ul>{listofCats}</ul>, 'display')
</script>
<div id='display' style='font-family:monospace'></div>
在这个示例中,创建了一个uniqueId变量,每次使用时都会递增,因此,例如,第一个键将变为1。显示的输出与前一个示例相同,但是如果您想查看生成的键(仅出于兴趣),可以将li元素的内容从{cat}更改为{uniqueId - 1 + ' ' + cat},然后您将看到以下内容显示(使用- 1是因为在引用它时uniqueId已经递增了,所以我们需要看到递增之前的值):
• 0 lion
• 1 tiger
• 2 cheetah
• 3 lynx
但是,您可能会问,这有什么意义呢?好吧,考虑以下列表结构的情况:
<ul> // Cities In Europe
<li>Birmingham</li>
<li>Paris</li>
<li>Milan</li>
<li>Vienna</li>
</ul>
<ul> // Cities in the USA
<li>Cincinnati</li>
<li>Paris</li>
<li>Chicago</li>
<li>Birmingham</li>
</ul>
这里有两组列表,每组都有四个唯一的兄弟姐妹,但在列表之间,包括“伯明翰”和“巴黎”这两个城市名称在同一层次的嵌套中是共享的。当 React 执行某些协调操作(例如重新排序或修改元素后),存在一些情况可以实现速度增益,并且在同一级别的兄弟列表项共享相同值时可能避免问题。为此,您可以为所有兄弟姐妹提供唯一的键,对于 React 而言,可能如下所示:
<ul> // Cities In Europe
<li key = "1">Birmingham</li>
<li key = "2">Paris</li>
<li key = "3">Milan</li>
<li key = "4">Vienna</li>
</ul>
<ul> // Cities in the USA
<li key = "5">Cincinnati</li>
<li key = "6">Paris</li>
<li key = "7">Chicago</li>
<li key = "8">Birmingham</li>
</ul>
现在不可能将欧洲的巴黎与美国的巴黎混淆(或者至少需要更加努力来定位并且可能重新渲染正确的 DOM 节点),因为每个数组元素对于 React 都有一个不同的唯一 ID。
注意
不要过于担心为什么要创建这些唯一键。只需记住,在这样做时 React 表现最佳,一个好的经验法则是在map调用中的元素将需要键。此外,您可以为不相关的不同兄弟集合重复使用您的键。然而,您可能不必创建自己的键,因为您正在处理的数据可能会为您提供它们,例如书籍 ISBN 号码。作为最后的手段,您可以简单地使用项目的索引作为其键,但重新排序可能会很慢,并且您可能会遇到其他问题,因此通常创建自己的键以便控制它们包含的内容是最佳选择。
处理表单
在 React 中,<input type='text'>,<textarea>和<select>都以类似的方式工作,因为 React 的内部状态成为所谓的“真实数据源”,因此这些组件被称为受控组件。
对于受控组件,输入的值始终由 React 状态驱动。这确实意味着您需要在 React 中编写更多的代码,但最终的好处是您可以将值传递给其他 UI 元素或从事件处理程序中访问它们。
通常,没有加载 React 或任何其他框架或库时,表单元素会维护自己的状态,该状态基于用户输入进行更新。在 React 中,可变状态通常保留在组件的state属性中,并且应仅使用setState函数更新它。
使用文本输入
让我们查看这三种输入类型,首先是简单的文本输入,就像这样:
<form>
Name: <input type='text' name='name'>
<input type='submit'>
</form>
该代码请求输入一串字符,然后在单击提交按钮(或按下 Enter 或 Return 键)时提交。现在让我们将其改为受控 React 组件在示例 24-10 中。
示例 24-10. 使用文本输入
<script type="text/babel">
class GetName extends React.Component
{
constructor(props)
{
super(props)
this.state = {value: ''}
this.onChange = this.onChange.bind(this)
this.onSubmit = this.onSubmit.bind(this)
}
onChange(event)
{
this.setState({value: event.target.value})
}
onSubmit(event)
{
alert('You submitted: ' + this.state.value)
event.preventDefault()
}
render()
{
return (
<form onSubmit={this.onSubmit}>
<label>
Name:
<input type="text" value={this.state.value}
onChange={this.onChange} />
</label>
<input type="submit" />
</form>
)
}
}
doRender(<GetName />, 'display')
</script>
<div id='display' style='font-family:monospace'></div>
让我逐步为你解释这一部分。首先我们创建一个名为GetName的新类,用于创建一个表单,提示输入名字。这个类包含两个事件处理器onChange和onSubmit。这些都是本地处理程序,通过在构造函数中使用bind调用来覆盖这些同名事件的标准 JavaScript 处理程序,同时在构造函数中也将value的值初始化为空字符串。
当被onChange中断调用时,新的onChange处理程序调用setState函数以更新value,以便始终保持value与输入字段中的内容保持同步。
当触发onSubmit事件时,它由新的onSubmit处理程序处理,在这种情况下,它发出一个弹出的alert窗口,以便我们可以看到它已经起作用。因为我们处理事件而不是系统,所以通过调用preventDefault来防止事件冒泡到系统。
最后,render方法包含所有要渲染到显示<div>中的 HTML 代码。当然,我们使用 XML 格式化的 HTML 来做这件事,因为这是 Babel 期望的(即 JSX 语法)。在这种情况下,它只需要额外的自闭合输入元素/>。
注意
我们并没有全局地覆盖onChange和onSubmit事件,因为我们仅将由渲染的代码发出的事件绑定到GetName类内部的本地事件处理程序,因此可以安全地使用相同的名称作为我们的事件处理程序,这有助于使我们的代码目的对其他开发人员更加明显。但如果可能会有任何疑问的可能性,你可能更喜欢为你的处理程序使用不同的名称,比如actOnSubmit等。
所以,正如你现在应该看到的那样,this.state.value将始终反映输入字段的状态,因为在受控组件中,value始终由 React 状态驱动。
使用文本区域
使用 React 的一个理念是在 DOM 上保持跨浏览器控制,以便快速简单地访问,并且简化开发流程。通过使用受控组件,我们始终控制并可以使所有类型的数据输入以类似的方式工作。
在示例 24-11 中,上一个示例已修改为使用<textarea>元素进行输入。
示例 24-11. 使用文本区域
<script type="text/babel">
class `GetText` extends React.Component
{
constructor(props)
{
super(props)
this.state = {value: ''}
this.onChange = this.onChange.bind(this)
this.onSubmit = this.onSubmit.bind(this)
}
onChange(event)
{
this.setState({value: event.target.value})
}
onSubmit(event)
{
alert('You submitted: ' + this.state.value)
event.preventDefault()
}
render()
{
return (
<form onSubmit={this.onSubmit}>
<label>
`Enter` `some` `text``:``<``br` `/``>`
`<``textarea` `rows``=``'5'` `cols``=``'40'` value={this.state.value}
onChange={this.onChange} />
</label>`<``br` `/``>`
<input type="submit" />
</form>
)
}
}
doRender(<`GetText` />, 'display')
function doRender(elem, dest)
{
ReactDOM.render(elem, document.getElementById(dest))
}
</script>
<div id='display' style='font-family:monospace'></div>
这段代码与文本输入示例非常相似,只有几个简单的更改:现在这个类被称为GetText,render方法中的文本输入被替换为一个设置为 40 列宽和 5 行高的<textarea>元素,并添加了一些<br>元素进行格式化。就是这样——没有其他改动就能让我们完全控制<textarea>输入字段。与前面的例子一样,this.state.value始终反映输入字段的状态。
当然,此类型的输入支持使用 Enter 或 Return 键输入换行符到字段中,因此现在只能通过单击按钮提交输入。
使用 select
在展示如何在 React 中使用<select>之前,让我们先看一下典型的 HTML 代码片段,在这个片段中,用户必须从几个国家中选择,其中 USA 是默认选择:
<select>
<option value="Australia">Australia</option>
<option value="Canada" >Canada</option>
<option value="UK" >United Kingdom</option>
<option selected value="USA" >United States</option>
</select>
在 React 中,这需要稍微处理一下,因为它在select元素上使用value属性而不是应用于option子元素的selected属性,如示例 24-12 中所示。
示例 24-12. 使用 select
<script type="text/babel">
class `GetCountry` extends React.Component
{
constructor(props)
{
super(props)
this.state = {`value``:` `'USA'`}
this.onChange = this.onChange.bind(this)
this.onSubmit = this.onSubmit.bind(this)
}
onChange(event)
{
this.setState({value: event.target.value})
}
onSubmit(event)
{
alert('You selected: ' + this.state.value)
event.preventDefault()
}
render()
{
return (
<form onSubmit={this.onSubmit}>
<label>
`Select` `a` `country`:
`<``select` `value``=``{``this``.``state``.``value``}`
`onChange``=``{``this``.``onChange``}``>`
`<``option` `value``=``"Australia"``>``Australia``<``/``option``>`
`<``option` `value``=``"Canada"` `>``Canada``<``/``option``>`
`<``option` `value``=``"UK"` `>``United` `Kingdom``<``/``option``>`
`<``option` `value``=``"USA"` `>``United` `States``<``/``option``>`
`<``/``select``>`
</label>
<input type="submit" />
</form>
)
}
}
doRender(<`GetCountry` />, 'display')
function doRender(elem, dest)
{
ReactDOM.render(elem, document.getElementById(dest))
}
</script>
<div id='display' style='font-family:monospace'></div>
再次看到,除了GetCountry的新类名之外,几乎没有改变,this.state.value被赋予了默认值'USA',并且输入类型现在是<select>但没有selected属性。
正如前两个例子一样,this.state.value始终反映输入的状态。
React Native
React 还有一个名为 React Native 的伴侣产品。通过它,您可以只使用扩展的 JSX JavaScript 语言创建适用于 iOS 和 Android 手机和平板电脑的完整应用程序,而无需了解 Java 或 Kotlin(用于 Android)或 Objective-C 或 Swift(用于 iOS)。
如何做到这一点并在各种移动设备上运行您的应用程序的完整详细信息和解释超出了本书的范围,但在本节中,我向您展示了获取所需软件和信息的位置。
创建 React Native 应用程序
要开发 React Native 应用程序,首先需要安装Android Studio和Java JDK。
在 Mac 上,您还需要从 App Store 安装 Xcode。Windows 用户除了棘手的虚拟化或“Hackintosh”外,没有简单的选项来开发 iOS 应用程序,因此真正最好的选择是拥有实际的 Mac(或托管服务)。
然后,您需要阅读Android Studio 文档,直到理解如何创建 Android 虚拟设备(AVD)并设置所需的各种环境变量,例如ANDROID_HOME,它应指向已安装的 JDK。现在您需要从nodejs.org安装 Node.JS。
一旦安装了 Node,如果你还不知道如何使用它,请阅读文档。现在你可以根据React Native 文档的建议安装 React Native。之后,你可以通过React 网站上的教程来学习,在构建 Windows 和 macOS 上的应用程序之间注意其中的差异。
一旦所有工作都正常运行(在完全理解并顺利运行之前可能需要一些时间),你现在可以使用 React JSX 代码(大部分情况下)同时为两个主要移动平台开发应用程序了!
进一步阅读
为了帮助你进行 React Native 开发,这里有一些在线教程,我认为它们清楚地解释了这个过程(感谢 Medium 上的 Pabasara Jayawardhana,Infinite Red Academy 上的 Kevin VanGelder,以及 Microsoft),并且在撰写时都是在线并且可用(如果不可用,则可以在archive.org找到)。当然,如果你仍然需要更多信息,你喜欢的搜索引擎将会给你所需的一切:
尽管这三个中的后者仅在 URL 中提到了 Windows,但指南也包括 macOS。
将 React 提升到下一个水平
现在你已经学会了如何设置和使用 React 的基础知识,还有很多其他内容可以做(特别是如果你打算用它构建 React Native 应用程序),这超出了本书的范围。因此,为了继续你的 React 之旅,我建议你访问Reactjs.org 网页,这是一个很好的起点,在这里你可以回顾一些在这里讨论的内容,然后进一步了解更强大的功能。
而且请记住,你可以从GitHub下载本章节(以及整本书)的所有示例。
因此,现在我们工具箱中有了 React(至少足够让我们启动和运行),让我们继续下一章,探索 HTML5 带给我们的所有好东西。
问题
-
有哪两种主要方法可以将 React 脚本整合到你的网页中?
-
XML 如何与 JavaScript 结合以在 React 中使用?
-
在 JSX JavaScript 代码中,你应该使用什么值来替换
<script type="application/javascript">中的type? -
有哪两种不同的方法可以将 React 扩展到你的代码中?
-
在 React 中,纯代码和非纯代码是什么意思?
-
React 如何跟踪状态?
-
如何在 JSX 代码中嵌入表达式?
-
一旦类构造完成,你如何改变一个值的状态?
-
在构造函数内使用
this关键字引用props之前,你首先必须做什么? -
如何在 JSX 中创建条件语句?
在附录 A 中查看 “第二十四章答案” 来获取这些问题的答案。
第二十五章:HTML5 简介
HTML5 代表了网页设计、布局和可用性的重大进步。它提供了一种简单的方法,在网页浏览器中操作图形,而无需依赖 Flash 等插件,提供了在网页中插入音频和视频的方法(同样无需插件),并消除了 HTML 演变过程中出现的几个令人讨厌的不一致之处。
此外,HTML5 还包括许多其他增强功能,如处理地理位置信息、Web Workers 管理后台任务、改进的表单处理以及访问本地存储捆绑(远远超出了 Cookie 的有限功能)。
不过,HTML5 的有趣之处在于它是一个持续发展的过程,各个浏览器在不同时间采纳了不同的特性。幸运的是,现在所有主流浏览器(市场份额超过 1%的浏览器,如 Chrome、Internet Explorer、Edge、Firefox、Safari、Opera 以及 Android 和 iOS 的浏览器)都已支持所有最重要和最受欢迎的 HTML5 新增功能。
画布
最初由苹果为其 Safari 浏览器的 WebKit 渲染引擎引入(该引擎本身起源于 KDE 的 HTML 布局引擎),canvas元素使我们能够在网页中绘制图形,而无需依赖 Java 或 Flash 等插件。在标准化后,所有其他浏览器都采纳了 canvas,并且它现在是现代 Web 开发的重要组成部分。
与其他 HTML 元素一样,canvas 只是网页中的一个元素,具有定义的尺寸,在其中可以使用 JavaScript 插入内容——在本例中,用于绘制图形。您通过使用<canvas>标签创建 canvas,必须为其分配一个 ID,以便 JavaScript 知道它正在访问哪个 canvas(因为页面上可以有多个 canvas)。
在示例 25-1 中,我创建了一个带有 ID mycanvas 的 canvas 元素,在不支持 canvas 的浏览器中仅显示一些文本。在下面的 JavaScript 部分中,绘制了日本国旗的代码(如图 25-1 所示)。
示例 25-1。使用 HTML5 canvas 元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Canvas</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='320' height='240'>
This is a canvas element given the ID <i>mycanvas</i>
This text is visible only in non-HTML5 browsers
</canvas>
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
context.fillStyle = 'red'
S(canvas).border = '1px solid black'
context.beginPath()
context.moveTo(160, 120)
context.arc(160, 120, 70, 0, Math.PI * 2, false)
context.closePath()
context.fill()
</script>
</body>
</html>
到这一点,不必详细说明正在发生的事情——我在第二十六章中会解释——但你应该已经看到使用画布并不难,尽管它确实需要学习一些新的 JavaScript 函数。请注意,此示例利用了OSC.js函数集来自第二十一章,以帮助保持代码整洁和紧凑。
图 25-1。使用 HTML5 canvas 绘制日本国旗
地理位置
使用 地理位置,你的浏览器可以向服务器返回关于你位置的信息。此信息可以来自你使用的计算机或移动设备中的 GPS 芯片,也可以来自你的 IP 地址,或通过分析附近的 WiFi 热点获得。为了安全起见,用户始终控制这些信息的提供,可以拒绝一次性提供这些信息,或者启用设置以永久性地阻止或允许所有网站或某个网站访问这些数据。
此技术有许多用途,包括提供逐步导航;提供本地地图;通知附近的餐馆、WiFi 热点或其他地方;告知你附近的朋友;指引你去最近的加油站;等等。
示例 25-2 将显示用户位置的谷歌地图,只要浏览器支持地理位置和用户授予位置数据访问权限(如 图 25-2 所示)。否则,它将显示一个错误。
示例 25-2. 显示用户位置的地图
<!DOCTYPE html>
<html>
<head>
<title>Geolocation Example</title>
</head>
<body>
<script>
if (typeof navigator.geolocation == 'undefined')
alert("Geolocation not supported.")
else
navigator.geolocation.getCurrentPosition(granted, denied)
function granted(position)
{
var lat = position.coords.latitude
var lon = position.coords.longitude
alert("Permission Granted. You are at location:\n\n"
+ lat + ", " + lon +
"\n\nClick 'OK' to load Google Maps with your location")
window.location.replace("https://www.google.com/maps/@"
+ lat + "," + lon + ",14z")
}
function denied(error)
{
var message
switch(error.code)
{
case 1: message = 'Permission Denied'; break;
case 2: message = 'Position Unavailable'; break;
case 3: message = 'Operation Timed Out'; break;
case 4: message = 'Unknown Error'; break;
}
alert("Geolocation Error: " + message)
}
</script>
</body>
</html>
再次强调,这里不是描述所有工作原理的地方;我会在 第二十八章 中详细说明。目前,这个示例展示了管理地理位置信息的简易性。
图 25-2. 使用用户位置显示地图
音频和视频
HTML5 的另一个重要新增是对浏览器内音频和视频的支持。尽管由于编码类型和许可证的多样性,播放这些媒体类型可能有些复杂,但 <audio> 和 <video> 元素提供了你展示可用媒体类型所需的灵活性。
在 示例 25-3 中,同一个视频文件已经被编码成不同格式,以确保所有主流浏览器都能播放。浏览器将简单地选择它们认识的第一个类型并播放,如 图 25-3 所示。
示例 25-3. 使用 HTML5 播放视频
<!DOCTYPE html>
<html>
<head>
<title>HTML5 Video</title>
</head>
<body>
<video width='560' height='320' controls>
<source src='movie.mp4' type='video/mp4'>
<source src='movie.webm' type='video/webm'>
<source src='movie.ogv' type='video/ogg'>
</video>
</body>
</html>
图 25-3. 使用 HTML5 显示视频
将音频插入到网页中同样简单,你将在 第二十七章 中了解到。
表单
正如你在 第十二章 中已经看到的,HTML5 表单正在被增强,但跨所有浏览器的支持仍然不完整。今天可以安全使用的部分已经在 第十二章 中详细说明了。
本地存储
使用本地存储,你可以在本地设备上保存的数据量和复杂性大大增加,远远超过 cookies 提供的微小空间。这打开了离线使用 Web 应用处理文档并在联网时与 Web 服务器同步的可能性。它还提升了本地存储小型数据库的前景,比如使用 WebSQL 保存音乐集合的细节或作为饮食或减肥计划的一部分保存所有个人统计数据。在第二十八章中,我向你展示如何在你的 Web 项目中充分利用这一新功能。
Web Workers
多年来,使用 JavaScript 在后台运行中断驱动应用程序是可能的,但只能通过笨拙且效率低下的过程。更明智的做法是让底层浏览器技术代表你运行后台任务,它可以比你连续中断浏览器检查进展做得更快。
相反,通过Web Workers,你设置好一切并将代码传递给网页浏览器,然后由浏览器运行。当发生重要事件时,你的代码只需通知浏览器,然后浏览器回报给主代码。与此同时,你的网页可以什么也不做或者进行多个其他任务,直到后台任务需要它时才知晓。
在第二十八章中,我展示了如何使用 Web Workers 创建一个简单的时钟并计算质数。
问题
-
哪个 HTML5 元素允许在网页中绘制图形?
-
访问许多高级 HTML5 功能需要哪种编程语言?
-
在网页中添加音频和视频,你会使用哪些 HTML5 标签?
-
HTML5 中哪个特性比 cookies 提供更强大的功能?
-
哪项 HTML5 技术支持运行后台 JavaScript 任务?
查看“第二十五章答案”,在附录 A 中有这些问题的答案。
第二十六章:HTML5 画布
虽然新网页技术的总称是HTML5,但它们并不全是简单的 HTML 标签和属性。canvas 元素就是一个例子。是的,你可以使用<canvas>标签创建一个画布,并且可能指定宽度和高度,并可以通过 CSS 进行一些修改,但要真正向画布写入(或从中读取),你必须使用 JavaScript。
幸运的是,你需要学习的 JavaScript 内容很少,而且非常容易实现,而且我已经在第二十一章(OSC.js文件中)提供了一套三个现成的函数,使得访问诸如 canvas 这样的对象更加简单。所以,让我们马上开始使用新的<canvas>标签。
创建和访问一个画布
在第二十五章中,我向你展示了如何绘制一个简单的圆来展示日本国旗,本章中再次使用。现在让我们来看看这里到底发生了什么。
示例 26-1 通过使用 canvas 显示日本国旗
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Canvas</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='320' height='240'>
This is a canvas element given the ID <i>mycanvas</i>
This text is only visible in non-HTML5 browsers
</canvas>
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
context.fillStyle = 'red'
S(canvas).border = '1px solid black'
context.beginPath()
context.moveTo(160, 120)
context.arc(160, 120, 70, 0, Math.PI * 2, false)
context.closePath()
context.fill()
</script>
</body>
</html>
当然,<!DOCTYPE html>声明告诉浏览器文档将使用 HTML5。接着,显示了一个标题,并加载了OSC.js文件中的三个函数。
文档的主体定义了一个 canvas 元素,赋予其 ID 为mycanvas,并设置宽度和高度为 320 × 240 像素。如前一章节所述,canvas 的文本在支持 canvas 元素的浏览器中不会显示,但在不支持 canvas 的老旧浏览器中会显示出来。
接着是一段 JavaScript 代码,用于样式化和在画布上绘制。我们首先通过调用O函数在画布元素上创建一个canvas对象。正如你所记得的,这会调用document.getElementById函数,因此是引用元素的一种更简短的方式。
这些都是你以前见过的东西,但接下来是一些新内容:
context = canvas.getContext('2d')
此命令调用新创建的canvas对象的getContext方法,通过传递值2d请求对画布进行二维访问。
注意
如果你想在画布上显示 3D 效果,可以自己做数学运算,在 2D 中“伪造”它,或者可以使用基于 OpenGL ES 的 WebGL,这种情况下你需要调用canvas.getContext('webgl')来为其创建一个context。这里没有更多的空间来进一步讨论这个主题,但你可以在https://webglfundamentals.org找到一个很棒的教程。或者,可以查看Three.jsJavaScript 库,它也使用 WebGL 提供 3D 功能。
有了对象context中的这个上下文,我们通过将context的fillStyle属性设置为red来准备后续的绘图命令:
context.fillStyle = 'red'
然后调用S函数来设置画布的border属性为 1 像素的实线黑色边框以勾勒出国旗图像:
S(canvas).border = '1px solid black'
准备好一切后,在上下文中打开路径,并将绘图位置移动到位置(160,120):
context.beginPath()
context.moveTo(160, 120)
在此之后,在该坐标中心绘制一个圆弧,半径为 70 像素,从角度为 0 度(即圆的右边缘)开始,以 2 × π 确定的弧度继续绕圆周绘制:
context.arc(160, 120, 70, 0, Math.PI * 2, false)
最后的 false 值指示顺时针方向绘制圆弧;true 值表示应以逆时针方向绘制。
最后,我们使用预先设置为 red 的 fillStyle 属性来关闭并填充路径:
context.closePath()
context.fill()
将此文档加载到网络浏览器中的结果类似于上一章节的图 25-1。
toDataURL 函数
在创建画布中的图像后,有时您可能希望复制它,也许是为了在网页的其他地方重复使用,用于动画目的,保存到本地存储或上传到 Web 服务器。这尤其方便,因为用户无法使用拖放来保存画布图像。
为了说明如何做到这一点,我在示例 26-2 中添加了几行代码(用粗体标出)。这些代码创建了一个新的 <img> 元素,带有 ID myimage,给它加上了一个纯黑色边框,然后将画布图像复制到 <img> 元素中(参见图 26-1)。
示例 26-2. 复制画布图像
<!DOCTYPE html>
<html>
<head>
<title>Copying a Canvas</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='320' height='240'> This is a canvas element given the ID <i>mycanvas</i> This text is only visible in non-HTML5 browsers </canvas>
`<img` `id=``'myimage'``>`
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
context.fillStyle = 'red'
S(canvas).border = '1px solid black'
context.beginPath()
context.moveTo(160, 120)
context.arc(160, 120, 70, 0, Math.PI * 2, false)
context.closePath()
context.fill()
`S``(``'myimage'``)``.``border` `=` `'1px solid black'`
`O``(``'myimage'``)``.``src` `=` `canvas``.``toDataURL``(``)`
</script>
</body>
</html>
图 26-1. 右侧图像是从左侧画布复制的
如果您自己尝试此代码,您会注意到,尽管您不能拖放左侧画布图像,但您可以对右侧图片进行拖放,您还可以使用适当的 JavaScript(以及服务器端的 PHP)将其保存到本地存储或上传到 Web 服务器。
指定图像类型
在从画布创建图像时,您可以指定要的图像类型,可以是 JPEG(.jpg 或 .jpeg 文件)或 PNG(.png 文件)。默认为 PNG(image/png),但如果有必要,您可以修改调用 toDataURL 的方式。同时,您还可以指定要使用的压缩量,介于 0(最低质量)和 1(最高质量)之间。以下使用了压缩值 0.4,应生成一个外观合理且文件大小较小的图像:
O('myimage').src = canvas.toDataURL('image/jpeg', 0.4)
警告
请记住,toDataURL 方法适用于 canvas 对象,而不适用于从该对象创建的任何上下文。
现在您知道如何创建画布图像,然后复制或以其他方式使用它们,是时候看看可用的绘图命令了,首先是矩形。
fillRect 方法
有三种不同的方法可以用来绘制矩形,第一种是fillRect。要使用它,只需提供矩形的左上角坐标,然后是宽度和高度(以像素为单位),像这样:
context.fillRect(20, 20, 600, 200)
默认情况下,矩形将填充为黑色,但你可以通过首先发出如下命令来使用任何其他你喜欢的颜色,其中参数可以是任何可接受的 CSS 颜色、名称或值:
context.fillStyle = 'blue'
clearRect 方法
你还可以绘制一个矩形,其中所有的颜色值(红色、绿色、蓝色和 alpha 透明度)都被设置为0,就像下面的例子一样,它使用了相同的坐标顺序和宽度高度参数:
context.clearRect(40, 40, 560, 160)
一旦应用了clearRect方法,新清除的矩形将从其覆盖的区域中除去所有颜色,只留下已应用到画布元素的任何底层 CSS 颜色。
strokeRect 方法
当你只想要一个轮廓矩形时,你可以使用如下命令,它将使用黑色或当前选择的描边颜色的默认值:
context.strokeRect(60, 60, 520, 120)
要改变使用的颜色,你可以首先发出如下命令,提供任何有效的 CSS 颜色参数:
context.strokeStyle = 'green'
结合这些命令
在示例 26-3 中,前面的绘制矩形命令已经组合起来显示了图 26-2 中显示的图像。
示例 26-3. 绘制几个矩形
<!DOCTYPE html>
<html>
<head>
<title>Drawing Rectangles</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='640' height='240'></canvas>
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
S(canvas).background = 'lightblue'
context.fillStyle = 'blue'
context.strokeStyle = 'green'
context.fillRect( 20, 20, 600, 200)
context.clearRect( 40, 40, 560, 160)
context.strokeRect(60, 60, 520, 120)
</script>
</body>
</html>
图 26-2. 绘制同心矩形
本章后面,你将看到如何通过改变描边类型和宽度来进一步修改输出,但首先,让我们通过应用渐变(作为 CSS 的一部分,已经在“渐变”中介绍过)来修改填充。
createLinearGradient 方法
有几种方法可以对填充应用渐变,但最简单的方法是使用createLinearGradient方法。你可以指定相对于画布(而不是被填充对象)的起始和结束x和y坐标。这允许更加微妙的效果。例如,你可以指定渐变从画布的最左侧开始,到最右侧结束,但仅在填充命令定义的区域内应用,如示例 26-4 所示。
示例 26-4. 应用渐变填充
gradient = context.createLinearGradient(0, 80, 640,80)
gradient.addColorStop(0, 'white')
gradient.addColorStop(1, 'black')
context.fillStyle = gradient
context.fillRect(80, 80, 480,80)
注意
为了简洁和清晰起见,在这个和许多后续示例中,只显示了代码的显著行。完整的例子,包括周围的 HTML、设置和其他代码部分,可以从GitHub免费下载。
在这个例子中,我们通过调用context对象的createLinearGradient方法创建了一个名为gradient的渐变填充对象。起始位置为(0, 80),位于左侧画布边缘的中间位置,而结束位置为(640, 80),位于右侧边缘的中间位置。
要创建您的渐变,确定您希望其流动的方向,然后找到两个点来表示开始和结束。无论您为这些点提供什么值,渐变都将平滑地过渡到给定的方向,即使这些点在填充区域之外。
接下来,提供了一对颜色停止,以指定渐变的第一个颜色是白色,最终颜色是黑色。然后,渐变将平滑地在画布上从左到右过渡这些颜色。
现在,gradient对象已准备就绪,它被应用于context对象的fillStyle属性,以便最终的fillRect调用可以使用它。在此调用中,填充仅应用于画布的中心矩形区域,因此尽管渐变从画布的最左边延伸到最右边,但其显示的部分仅从左上角的 80 像素处开始,宽度为 480 像素,深度为 80 像素。结果(当添加到先前的示例代码中时)看起来像图 26-3。
图 26-3. 中央矩形具有水平渐变填充
通过为渐变指定不同的起始和结束坐标,可以使其朝任何方向倾斜,正如示例 26-5 所演示的,并显示在图 26-4 中。
示例 26-5. 不同角度和颜色的多种渐变
gradient = context.createLinearGradient(0, 0, 160, 0)
gradient.addColorStop(0, 'white')
gradient.addColorStop(1, 'black')
context.fillStyle = gradient
context.fillRect(20, 20, 135, 200)
gradient = context.createLinearGradient(0, 0, 0, 240)
gradient.addColorStop(0, 'yellow')
gradient.addColorStop(1, 'red')
context.fillStyle = gradient
context.fillRect(175, 20, 135, 200)
gradient = context.createLinearGradient(320, 0, 480, 240)
gradient.addColorStop(0, 'green')
gradient.addColorStop(1, 'purple')
context.fillStyle = gradient
context.fillRect(330, 20, 135, 200)
gradient = context.createLinearGradient(480, 240, 640, 0)
gradient.addColorStop(0, 'orange')
gradient.addColorStop(1, 'magenta')
context.fillStyle = gradient
context.fillRect(485, 20, 135, 200)
图 26-4. 不同线性渐变的范围
在这个例子中,我选择直接将渐变放置在要填充的区域上方,以更清楚地显示从起始到结束的颜色最大变化。
详细介绍 addColorStop 方法
您可以在渐变中使用任意数量的颜色停止,不仅限于这些示例中到目前为止使用的两种起始和结束颜色。这使得几乎可以描述您可以想象到的任何类型的渐变效果。为此,必须指定每种颜色在渐变中所占百分比的浮点起始位置,分配在0到1之间的渐变范围内。您不输入颜色的结束位置,因为它是从下一个颜色停止的起始位置推导出来的,或者是您指定的最后一个位置的渐变结束。
在前面的示例中,仅选择了两个起始和结束值,但要创建彩虹效果,您可以设置如示例 26-6 所示的颜色停止,(显示在图 26-5 中)。
示例 26-6. 添加多个颜色停止
gradient.addColorStop(0.00, 'red')
gradient.addColorStop(0.14, 'orange')
gradient.addColorStop(0.28, 'yellow')
gradient.addColorStop(0.42, 'green')
gradient.addColorStop(0.56, 'blue')
gradient.addColorStop(0.70, 'indigo')
gradient.addColorStop(0.84, 'violet')
图 26-5. 具有七个停止颜色的彩虹效果
在 示例 26-6 中,所有颜色大致等间距分布(每种颜色占渐变的 14%,最后一种占 16%),但您不必局限于此;您可以将几种颜色挤在一起,同时将其他颜色间隔开。您可以完全自由地选择使用多少种颜色以及它们在渐变中的起始和结束位置。
createRadialGradient 方法
在 HTML 中,您不仅限于线性渐变;您也可以在画布上创建径向渐变。虽然比线性渐变复杂一点,但也不多。
你需要做的是将中心位置作为一对x和y坐标传递,并且附带一个像素半径。这些被用作渐变的起始点和外部圆周。然后你还需要传递另一组坐标和半径来指定渐变的结束。
因此,例如,要创建一个简单从圆的中心开始然后扩展出去的渐变,您可以发出类似于 示例 26-7 中的命令(显示在 Figure 26-6 中)。起始点和结束点的坐标相同,但起始点的半径为 0,结束点的半径则包含整个渐变。
示例 26-7. 创建一个径向渐变
gradient = context.createRadialGradient(320, 120, 0, 320, 120, 320)
图 26-6. 一个居中的径向渐变
或者您可以花哨一些,移动径向渐变的起始点和结束点的位置,就像 示例 26-8 中所示(并显示在 Figure 26-7 中),它从位置 (0, 120) 开始居中,半径为 0 像素,并以位置 (480, 120) 结束,半径为 480 像素。
示例 26-8. 拉伸径向渐变
gradient = context.createRadialGradient(0, 120, 0, 480, 120, 480)
图 26-7. 一个拉伸的径向渐变
注
通过操纵此方法提供的图表,您可以创建各种怪异而奇妙的效果——尝试使用提供的示例自己动手。
使用图案进行填充
类似于渐变填充,您也可以将图像应用为填充图案。这可以是当前文档中的任何图像,甚至是通过 toDataURL 方法从画布创建的图像(本章前面已经解释过)。
示例 26-9 加载一个 100 × 100 像素的图像(阴阳符号)到新的图像对象 image 中。接下来的语句将一个函数附加到 onload 事件,该函数为上下文的 fillStyle 属性创建一个重复图案。然后用这个图案填充画布中的一个 600 × 200 像素的区域,如图 Figure 26-8 所示。
示例 26-9. 使用图像作为图案填充
image = new Image()
image.src = 'image.png'
image.onload = function()
{
pattern = context.createPattern(image, 'repeat')
context.fillStyle = pattern
context.fillRect(20, 20, 600, 200)
}
图 26-8. 使用图像作为图案填充
我们通过使用 createPattern 方法来创建图案,该方法还支持非重复和仅在 x 轴或 y 轴上重复的图案。我们通过将其作为第二个参数传递给它来实现这一点,此参数为要使用的图像之后。
repeat
垂直和水平重复图片。
repeat-x
水平重复图片。
repeat-y
垂直重复图片。
no-repeat
不要重复图片。
填充图案基于整个画布区域,因此在设置填充命令仅应用于画布内较小区域时,图像在顶部和左侧会显得被截断。
警告
如果在此示例中没有使用 onload 事件,而是在遇到代码时直接执行,那么图像可能在网页显示时尚未加载完成,可能不会显示在显示器上。附加到此事件可以确保图像可用于在画布中使用,因为该事件仅在图像成功加载后触发。
将文本写入画布
就像你从一组图形特性中所期望的那样,用文本写入画布是完全受支持的,具备多种字体、对齐和填充方法。但是,当今在 CSS 中已经有如此好的支持 web 字体的情况下,为什么还要将文本写入画布呢?
假设你希望显示一个带有图形元素的图表或表格。你肯定也想为其部分标记标签。更重要的是,利用现有的命令,你可以生成的不仅仅是彩色字体。因此,让我们首先假设你被要求为一个名为 WickerpediA 的篮编网站创建页眉(尽管实际上已经有一个这样的网站,但我们继续进行)。
首先,你需要选择合适的字体并将其大小调整到适当的尺寸,可能如示例 26-10 中所示,选择了粗体风格、140 像素大小和 Times 字体。同时,还设置了 textBaseline 属性为 top,以便 strokeText 方法可以使用 (0, 0) 作为文本左上角的起点坐标,将其放置在画布的左上角。图 26-9 显示了其效果。
示例 26-10. 写入文本到画布
context.font = 'bold 140px Times'
context.textBaseline = 'top'
context.strokeText('WickerpediA', 0, 0)
图 26-9. 文本已写入画布
strokeText 方法
要将文本写入画布,你需要将文本字符串和一对坐标发送到 strokeText 方法,就像这样:
context.strokeText('WickerpediA', 0, 0)
提供的 x 和 y 坐标将被 textBaseline 和 textAlign 属性作为相对参考使用。
这种方法——使用线条绘制——只是绘制文本到画布的一种方式。因此,除了所有以下影响文本的属性外,如lineWidth(稍后在本章详细介绍),还会影响文本显示的线条绘制属性。
textBaseline 属性
textBaseline 属性可以使用以下任意值:
top
对齐到文本的顶部
middle
对齐到文本的中间
alphabetic
对齐到文本的字母基线
bottom
对齐到文本的底部
字体属性
字体样式可以是bold、italic或normal(默认),也可以是italic bold的组合,大小值可以使用em、ex、px、%、in、cm、mm、pt或pc等单位,与 CSS 类似。字体应该是当前浏览器可用的其中之一,通常为Helvetica、Impact、Courier、Times或Arial,或者你可以选择用户系统的默认Serif或Sans-serif字体。如果你确信另一种你想使用的字体在浏览器中可用,也可以指定它,但最好在之后至少包含一个更常见或默认的选项,以便在用户没有安装首选字体时能够优雅地回退样式。
警告
如果你想使用像 Times New Roman 这样的字体,其中包含空格,请将相关行更改为以下内容,外部引号与字体名称周围的引号不同:
context.font = 'bold 140px "Times New Roman"'
textAlign 属性
除了选择垂直对齐文本的方式外,还可以通过给 textAlign 属性指定以下值来指定水平对齐方式:
start
如果文档方向是从左到右,则将文本左对齐,否则右对齐。这是默认设置。
end
如果文档方向是从左到右,则将文本右对齐,否则左对齐。
left
将文本向左对齐。
right
将文本向右对齐。
center
将文本居中。
你可以像这样使用该属性:
context.textAlign = 'center'
在当前示例中,需要将文本左对齐,以便其与画布边缘整齐对齐,因此不使用 textAlign 属性,因此默认的左对齐效果发生。
fillText 方法
你还可以选择使用填充属性来填充画布文本,可以是纯色、线性或径向渐变,或图案填充。让我们尝试基于柳条篮子的纹理进行标题的图案填充,如示例 26-11,其结果显示在图 26-10 中。
示例 26-11. 用图案填充文本
image = new Image()
image.src = 'wicker.jpg'
image.onload = function()
{
pattern = context.createPattern(image, 'repeat')
context.fillStyle = pattern
context.fillText( 'WickerpediA', 0, 0)
context.strokeText('WickerpediA', 0, 0)
}
图 26-10. 文本现在具有图案填充
为了确保文本边缘有足够的定义,我还在这个示例中保留了strokeText调用;如果没有它,边缘的定义就不够了。
这里还可以使用各种其他填充类型或图案,并且画布的简单性使得进行实验变得容易。此外,如果你希望,一旦标题达到完美,你还可以选择通过调用toDataURL保存一个副本,正如本章前面详细介绍的那样。然后,你可以将图像用作上传到其他站点的标志,例如。
measureText 方法
在使用画布文本时,有时需要知道它将占用多少空间,以便最佳位置放置它。你可以使用measureText方法来实现这一点(假设在此时已经定义了各种文本属性),如下所示:
metrics = context.measureText('WickerpediA')
width = metrics.width
由于像素高度等于定义字体时的点大小,所以metrics对象不提供高度度量。
绘制线条
画布提供了丰富的线条绘制函数,以满足几乎所有需求,包括线条、线帽和连接的选择,以及各种类型的路径和曲线。但让我们从上一节中涉及的属性开始,介绍一下。
lineWidth 属性
所有用线条绘制的画布方法都使用了几个线条属性,其中最重要的之一是lineWidth。使用它就像简单地指定像素线宽一样简单,比如这样设置宽度为 3 像素:
context.lineWidth = 3
lineCap 和 lineJoin 属性
当你绘制的线条到达终点并且宽度超过一个像素时,你可以选择如何显示这个线帽,通过使用lineCap属性,它可以有值butt(默认)、round或square,例如:
context.lineCap = 'round'
此外,当你连接超过单像素宽度的线条时,指定它们如何相遇非常重要。你可以使用lineJoin属性来实现这一点,它可以具有值round、bevel或miter(默认值),例如:
context.lineJoin = 'bevel'
示例 26-12(因为有点复杂,这里完整显示)将每个属性的三个值结合起来应用,从而创建了你在图 26-11 中看到的结果。该示例使用的beginPath、closePath、moveTo和lineTo方法将在下面解释。
示例 26-12. 显示线帽和连接的组合
<!DOCTYPE html>
<html>
<head>
<title>Drawing Lines</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='535' height='360'></canvas>
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
S(canvas).background = 'lightblue'
context.fillStyle = 'red'
context.font = 'bold 13pt Courier'
context.strokeStyle = 'blue'
context.textBaseline = 'top'
context.textAlign = 'center'
context.lineWidth = 20
caps = [' butt', ' round', 'square']
joins = [' round', ' bevel', ' miter']
for (j = 0 ; j < 3 ; ++j)
{
for (k = 0 ; k < 3 ; ++k)
{
context.lineCap = caps[j]
context.lineJoin = joins[k]
context.fillText(' cap:' + caps[j], 88 + j * 180, 45 + k * 120)
context.fillText('join:' + joins[k], 88 + j * 180, 65 + k * 120)
context.beginPath()
context.moveTo( 20 + j * 180, 100 + k * 120)
context.lineTo( 20 + j * 180, 20 + k * 120)
context.lineTo(155 + j * 180, 20 + k * 120)
context.lineTo(155 + j * 180, 100 + k * 120)
context.stroke()
context.closePath()
}
}
</script>
</body>
</html>
这段代码设置了一些属性,然后嵌套了一对循环:一个用于线帽,一个用于连接。在中央循环内,首先设置了lineCap和lineJoin属性的当前值,然后用fillText方法在画布上显示出来。
使用这些设置,代码将绘制九个形状,每个形状都有一个 20 像素宽的线,具有不同的线帽和连接设置,如 图 26-11 所示。
正如你所看到的,平截头线帽很短,方形的较长,而圆形的则介于两者之间。圆角连接是曲线的,斜角连接是切割过角的,而尖角连接具有尖锐的角。连接同样适用于不是 90 度的角的连接。
图 26-11. 所有线帽和连接的组合
miterLimit 属性
如果你发现你的尖角斜接被截断得太短,你可以使用 miterLimit 属性来延长它们,如下所示:
context.miterLimit = 15
默认值为 10,所以你也可以减少斜接限制。如果 miterLimit 没有设置为足够大的值以适应斜接,那么锐角斜接将会简单地变成斜接。因此,如果你遇到尖锐斜接的问题,只需增加你为 miterLimit 提供的值,直到斜接正确显示。
使用路径
前面的示例使用了两种方法来设置路径,供线条绘制方法使用。beginPath 方法设置路径的开始,而 closePath 设置路径的结束。在每个路径内部,你可以使用各种方法来移动绘图位置以及创建线条、曲线和其他形状。让我们简要看一下来自 示例 26-12 的相关部分,简化为仅创建模式的单个实例:
context.beginPath()
context.moveTo(20, 100)
context.lineTo(20, 20)
context.lineTo(155, 20)
context.lineTo(155, 100)
context.stroke()
context.closePath()
在这段代码片段中,第一行开始了一个路径,然后通过调用 moveTo 方法将绘图位置移动到离画布左上角 20 像素横向和 100 像素纵向的位置。
然后紧接着是对 lineTo 的三次调用,分别画出三条线,首先向上到位置 (20, 20),然后向右到 (155, 20),最后再次向下到 (155, 100)。一旦路径设置好了,调用 stroke 方法进行绘制,最后关闭路径因为不再需要。
注意
在完成路径后立即关闭路径是非常重要的;否则,在使用多条路径时可能会得到一些非常意想不到的结果。
moveTo 和 lineTo 方法
moveTo 方法和 lineTo 方法都接受简单的 x 和 y 坐标作为它们的参数,它们的区别在于 moveTo 从当前位置拿起一个虚拟的笔然后移动到一个新位置,而 lineTo 在当前虚拟笔的位置到指定的新位置之间画一条线。或者,如果调用了 stroke 方法但没有其他方法的话,将会画出一条线。因此,我们只能说 lineTo 创建了一个潜在的画线,但它同样可以是填充区域的轮廓的一部分,例如。
stroke 方法
stroke方法的作用是将到目前为止在路径中创建的所有线条实际绘制到画布上。如果在未关闭路径的情况下发出该命令,则会立即绘制到最近的虚拟笔位置。
然而,如果关闭路径,然后发出一个stroke调用,它的效果也会从当前位置回到起始位置连接路径,在这个例子中会将形状变成矩形(这不是我们想要的,因为我们需要看到线帽以及连接点)。
注意
这种路径闭合后的连接效果是必需的(稍后您将看到),以便准备好任何您希望对其使用的fill方法;否则,用于填充的图形可能会超出路径的边界。
rect 方法
如果需要创建四边形而不是前面示例中的三边形(并且您不希望关闭路径),可以发出另一个lineTo调用以连接所有内容,就像这样(以粗体显示):
context.beginPath()
context.moveTo(20, 100)
context.lineTo(20, 20)
context.lineTo(155, 20)
context.lineTo(155, 100)
`context``.``lineTo``(``20``,` `100``)`
context.closePath()
但是,有一种更简单的方法可以绘制带有轮廓的矩形,即使用rect方法,就像这样:
rect(20, 20, 155, 100)
在单个调用中,此命令接受两对x和y坐标并绘制一个矩形,其左上角位于(20, 20)位置,右下角位于(155, 100)位置。
填充区域
使用路径,您可以创建复杂的区域,还可以填充实色、渐变色或图案填充。在示例 26-13 中,使用了一些基本的三角函数来创建复杂的星形图案。我不会详细说明数学如何工作,因为这对示例不重要(尽管如果您想玩转代码,请尝试更改分配给points、scale1和scale2变量的值,以获得不同的效果)。
示例 26-13. 填充复杂路径
<!DOCTYPE html>
<html>
<head>
<title>Filling a Path</title>
<script src='OSC.js'></script>
</head>
<body>
<canvas id='mycanvas' width='320' height='320'></canvas>
<script>
canvas = O('mycanvas')
context = canvas.getContext('2d')
S(canvas).background = 'lightblue'
`context``.``strokeStyle` `=` `'orange'`
`context``.``fillStyle` `=` `'yellow'`
orig = 160
points = 21
dist = Math.PI / points * 2
scale1 = 150
scale2 = 80
`context``.``beginPath``(``)`
for (j = 0 ; j < points ; ++j)
{
x = Math.sin(j * dist)
y = Math.cos(j * dist)
`context``.``lineTo``(``orig` `+` `x` `*` `scale1``,` `orig` `+` `y` `*` `scale1``)`
`context``.``lineTo``(``orig` `+` `x` `*` `scale2``,` `orig` `+` `y` `*` `scale2``)`
}
`context``.``closePath``(``)`
`context``.``stroke``(``)`
`context``.``fill``(``)`
</script>
</body>
</html>
您真正需要查看的只是以粗体显示的那些行,其中开始了一个路径,一对lineTo调用定义了形状,路径被关闭,然后使用stroke和fill方法绘制橙色的形状轮廓并填充黄色(如图 26-12 所示)。
注意
使用路径,您可以创建任意复杂的对象,可以使用公式或循环(如本例中)或仅使用一长串的moveTo和/或lineTo调用。
图 26-12. 绘制和填充复杂路径
clip 方法
有时,在构建路径时,您可能希望忽略画布的某些部分(也许是因为您正在部分“后面”绘制另一个对象,只想显示可见部分)。您可以使用clip方法来实现这一点,该方法创建一个边界,超出该边界的stroke、fill或其他方法将不会产生任何效果。
为了说明这一点,示例 26-14 通过将虚拟笔指针移动到左边缘,然后画一条lineTo到右边缘,再向下移动 30 像素,然后再返回到左边缘,依此类推,创建了类似窗帘的效果。这在画布上绘制了一系列深度为 30 像素的水平条,如图 26-13 所示。
示例 26-14. 创建剪辑区域
context.beginPath()
for (j = 0 ; j < 10 ; ++j)
{
context.moveTo(20, j * 48)
context.lineTo(620, j * 48)
context.lineTo(620, j * 48 + 30)
context.lineTo(20, j * 48 + 30)
}
`context``.``stroke``(``)`
context.closePath()
图 26-13. 水平条形图的路径
要将此示例转换为画布上的剪辑区域,您只需将示例中用粗体突出显示的stroke调用替换为clip调用,如下所示:
context.clip()
现在不会看到条形图的轮廓,但是一个由所有单独的条形图组成的剪辑区域将会存在。为了说明这一点,示例 26-15 使用这种方法替代,并在之前的示例基础上添加了在画布上绘制简单的绿草图像的修改,位于一个包含闪耀太阳的蓝天下(修改自示例 26-12),用粗体突出显示的更改,并在图 26-14 中显示结果。
示例 26-15. 在剪辑区域的边界内绘制
`context``.``fillStyle` `=` `'white'`
`context``.``strokeRect``(``20``,` `20``,` `600``,` `440``)` // Black border `context``.``fillRect``(` `20``,` `20``,` `600``,` `440``)` // White background
context.beginPath()
for (j = 0 ; j < 10 ; ++j)
{
context.moveTo(20, j * 48)
context.lineTo(620, j * 48)
context.lineTo(620, j * 48 + 30)
context.lineTo(20, j * 48 + 30)
}
`context``.``clip``(``)`
context.closePath()
`context``.``fillStyle` `=` `'blue'` // Blue sky `context``.``fillRect``(``20``,` `20``,` `600``,` `320``)`
`context``.``fillStyle` `=` `'green'` // Green grass `context``.``fillRect``(``20``,` `320``,` `600``,` `140``)`
`context``.``strokeStyle` `=` `'orange'`
`context``.``fillStyle` `=` `'yellow'`
`orig` `=` `170`
`points` `=` `21`
`dist` `=` `Math``.``PI` `/` `points` `*` `2`
`scale1` `=` `130`
`scale2` `=` `80`
`context``.``beginPath``(``)`
`for` `(``j` `=` `0` `;` `j` `<` `points` `;` `++``j``)`
`{`
`x` `=` `Math``.``sin``(``j` `*` `dist``)`
`y` `=` `Math``.``cos``(``j` `*` `dist``)`
`context``.``lineTo``(``orig` `+` `x` `*` `scale1``,` `orig` `+` `y` `*` `scale1``)`
`context``.``lineTo``(``orig` `+` `x` `*` `scale2``,` `orig` `+` `y` `*` `scale2``)`
`}`
`context``.``closePath``(``)`
`context``.``stroke``(``)` // Sun outline `context``.``fill``(``)` // Sun fill
图 26-14. 仅在允许的剪辑区域内绘制
好了,我们不会在这里赢得任何比赛,但您可以看到在有效使用时剪辑可以有多强大。
isPointInPath 方法
有时候您需要知道特定点是否位于您构建的路径中。但是,您可能只有在非常熟练于 JavaScript 并且能够编写相当复杂的程序时才会想要使用此功能,并且通常会将其作为条件if语句的一部分调用,如下所示:
if (context.isPointInPath(23, 87))
{
// Do something here
}
调用的第一个参数是位置的x坐标,第二个参数是位置的y坐标。如果指定的位置位于路径中的任何点上,则该方法返回值为true,因此执行if语句的内容。否则,返回值为false,则不执行if语句的内容。
注意
使用isPointInPath方法的完美场景是创建使用画布的游戏,例如希望检查导弹击中目标、球击中墙壁或球拍等边界条件。
使用曲线
除了直线路径,您还可以使用各种不同的方法创建几乎无限种类的曲线路径,从简单的弧线和圆到复杂的二次和贝塞尔曲线。
实际上,您不需要使用路径来创建许多线条、矩形和曲线,因为您可以通过直接调用它们的方法来直接绘制它们。但是使用路径可以给您提供更精确的控制,因此我倾向于在定义的路径内绘制画布上的内容,就像以下示例一样。
弧方法
arc方法要求您传递弧的中心的x和y位置,以及像素的半径。除了这些值,您还需要传递一对弧度偏移量,并且可以选择包括方向,如下所示:
context.arc(55, 85, 45, 0, Math.PI / 2, false)
由于默认方向是顺时针的(一个值为false),这可以省略,或者您可以将其更改为true以逆时针方向绘制弧。
示例 26-16 创建了三组四个弧,前两组顺时针方向,第三组逆时针方向。此外,前两组弧在调用stroke方法之前路径已关闭,因此起点和终点相连,而后面的两组弧在路径关闭之前绘制,因此不相连。
示例 26-16. 绘制各种弧
context.strokeStyle = 'blue'
arcs =
[
Math.PI,
Math.PI * 2,
Math.PI / 2,
Math.PI / 180 * 59
]
for (j = 0 ; j < 4 ; ++j)
{
context.beginPath()
context.arc(80 + j * 160, 80, 70, 0, arcs[j])
context.closePath()
context.stroke()
}
context.strokeStyle = 'red'
for (j = 0 ; j < 4 ; ++j)
{
context.beginPath()
context.arc(80 + j * 160, 240, 70, 0, arcs[j])
context.stroke()
context.closePath()
}
context.strokeStyle = 'green'
for (j = 0 ; j < 4 ; ++j)
{
context.beginPath()
context.arc(80 + j * 160, 400, 70, 0, arcs[j], true)
context.stroke()
context.closePath()
}
为了创建更短的代码,我使用循环绘制了所有的弧,以便将每个弧的长度存储在数组arcs中。这些值以弧度表示,一个弧度相当于 180 ÷ π(π是圆周与直径的比值,约为 3.1415927),它们的计算如下:
Math.PI
等同于 180 度
Math.PI * 2
等同于 360 度
Math.PI / 2
等同于 90 度
Math.PI / 180 * 59
等同于 59 度
图 26-15 展示了三行弧,并说明了在最后一组中使用方向参数true以及根据您是否希望绘制连接起始点和终点的线条而仔细选择关闭路径的重要性。
图 26-15. 各种弧类型
注意
如果您更喜欢使用度而不是弧度,可以创建一个新的Math库函数,如下所示:
Math.degreesToRadians = function(degrees)
{
return degrees * Math.PI / 180
}
然后用以下内容替换从示例 26-16 的第二行开始的创建数组的代码:
arcs =
[
Math.degreesToRadians(180),
Math.degreesToRadians(360),
Math.degreesToRadians(90),
Math.degreesToRadians(59)
]
arcTo 方法
而不是一次创建整个弧,您可以选择从路径中的当前位置弧到另一个位置,就像在以下arcTo调用中(它只需要两对x和y坐标和一个半径):
context.arcTo(100, 100, 200, 200, 100)
您传递给该方法的位置代表想象的切线触及弧的圆周的起点和终点处。切线是一条直线与圆周的接触点,使得接触点两侧的角度相等。
为了说明其工作原理,例子 26-17 绘制了八个不同半径从 0 到 280 像素的弧线。每次循环时,都会在位置 (20, 20) 处创建一个新路径的起始点。然后使用虚拟切线从该位置绘制弧线到位置 (240, 240),然后再到位置 (460, 20)。在这种情况下,它定义了一个互相成直角的 V 形的切线对。
例子 26-17. 绘制八个不同半径的弧线
for (j = 0 ; j <= 280 ; j += 40)
{
context.beginPath()
context.moveTo(20, 20)
context.arcTo(240, 240, 460, 20, j)
context.lineTo(460, 20)
context.stroke()
context.closePath()
}
arcTo 方法仅绘制弧线触及第二个虚拟切线的点。因此,在每次调用 arcTo 后,lineTo 方法创建从 arcTo 结束位置到位置 (460,20) 的剩余线段。然后使用 stroke 方法将结果绘制到画布上,并关闭路径。
正如您在 图 26-16 中看到的,当 arcTo 使用半径值 0 调用时,它会创建一个尖锐的连接点。在这种情况下,它是一个直角(但如果两条虚拟切线相互形成其他角度,连接点将位于那个角度)。随着半径的增大,您可以看到弧线变得越来越大。
图 26-16. 绘制不同半径的弧线
从根本上说,您可以最好地使用 arcTo 来从绘制的一个部分弯曲到另一个部分,根据前后位置的弧度切线,仿佛它们是弧线的切线。如果这听起来很复杂,别担心:您很快就会掌握它,并发现这实际上是一种方便和逻辑的绘制弧线的方式。
二次曲线绘制方法
尽管弧线很有用,但它们只是一种曲线类型,对于更复杂的设计可能有所限制。但不用担心:还有更多方法来绘制曲线,例如 quadraticCurveTo 方法。使用这种方法,您可以在曲线附近放置一个虚拟吸引子,将其朝该方向拉动,就像空间中的物体路径被它经过的行星和恒星的引力所拉动一样。不过,与重力不同的是,吸引子距离越远,它的吸引力就越大!
例子 26-18 包含对此方法的六次调用,创建了一朵蓬松云朵的路径,然后用白色填充。 图 26-17 说明了云朵外部虚线的角度代表应用于每条曲线的吸引点。
例子 26-18. 使用二次曲线绘制云朵
context.beginPath()
context.moveTo(180, 60)
context.quadraticCurveTo(240, 0, 300, 60)
context.quadraticCurveTo(460, 30, 420, 100)
context.quadraticCurveTo(480, 210, 340, 170)
context.quadraticCurveTo(240, 240, 200, 170)
context.quadraticCurveTo(100, 200, 140, 130)
context.quadraticCurveTo( 40, 40, 180, 60)
context.fillStyle = 'white'
context.fill()
context.closePath()
图 26-17. 使用二次曲线绘制
注意
顺便说一下,为了在此图像中实现云的虚线周围的虚线,我使用了stroke方法与setLineDash方法结合使用,后者接受表示短划线和空格长度的列表。在这种情况下,我使用了setLineDash([2, 3]),但您可以创建复杂到您喜欢的虚线,比如setLineDash([1, 2, 1, 3, 5, 1, 2, 4])。
bezierCurveTo 方法
如果您仍然觉得二次曲线对您的需求不够灵活,那么对于每条曲线使用两个吸引子如何?使用bezierCurveTo方法,您可以做到这一点,就像在示例 26-19 中那样,其中创建了一个曲线,连接位置(24, 20)和(240, 220),但是吸引子在画布外部不可见(在此示例中位于位置(720, 480)和(-240, -240))。图 26-18 展示了这条曲线的形变过程。
示例 26-19. 创建具有两个吸引子的贝塞尔曲线
context.beginPath()
context.moveTo(240, 20)
context.bezierCurveTo(720, 480, -240, -240, 240, 220)
context.stroke()
context.closePath()
图 26-18. 具有两个吸引子的贝塞尔曲线
吸引子不需要位于画布的相对两侧;您可以将它们放置在任何位置,当它们彼此靠近时,它们将施加联合的吸引力(而不是像前面的例子中那样的对立吸引力)。使用这些不同类型的曲线方法,您可以绘制您所需的每种类型的曲线。
图像操作
您不仅可以使用图形方法在画布上绘制和书写,还可以在画布上放置图像或从中提取它们。而且,您不限于简单的复制粘贴命令,因为在读取或写入图像时,您可以拉伸和扭曲图像,并完全控制混合和阴影效果。
drawImage 方法
使用drawImage方法,您可以获取从网站加载的图像对象、上传到服务器的图像,甚至从画布中提取的图像,并将其绘制到画布上。该方法支持多种参数,其中许多是可选的,但在其最简单的形式下,您可以像以下这样调用drawImage,仅传递图像及一对x和y坐标:
context.drawImage(myimage, 20, 20)
此命令将myimage对象中包含的图像绘制到具有context上下文的画布上,其左上角位于位置(20, 20)。
警告
在使用图像之前确保其已加载的最佳实践是将您的图像处理代码封装在一个仅在图像加载时触发的函数中,如下所示:
myimage = new Image()
myimage.src = 'image.gif'
myimage.onload = function()
{
context.drawImage(myimage, 20, 20)
}
调整图像大小
如果您需要在放置在画布上的图像上调整大小,请在调用中添加第二对表示所需宽度和高度的参数,如下(用粗体标记):
context.drawImage(myimage, 140, 20, `220`, `220`)
context.drawImage(myimage, 380, 20, `80`, `220`)
此处图像放置在两个位置:第一个位置为(140, 20),图像被放大(从 100 像素正方形到 220 像素正方形),而第二个位置为(380, 20),图像在水平方向被挤压并在垂直方向上扩展,宽度和高度为 80 × 220 像素。
选择图像区域
在使用 drawImage 时,并不局限于使用整个图像;还可以选择图像内的区域。例如,如果希望将所有打算使用的图形图像放在一个单独的图像文件中,然后只需抓取所需的图像部分,这会非常方便。开发人员经常使用这种技巧来加快页面加载速度并减少服务器请求。
不过,这样做稍微复杂一些,因为在这种方法中,与在参数列表末尾添加更多参数不同的是,在提取图像的部分时,必须首先放置这些参数。
因此,例如,要在位置 (20, 140) 处放置一个图像,可以发出以下命令:
context.drawImage(myimage, 20, 140)
要给抓取的部分设置宽度和高度为 100 × 100 像素,可以像这样修改调用(用粗体标出):
context.drawImage(myimage, 20, 140, `100`, `100`)
但是,如果想要抓取(或裁剪)一个只有 40 × 40 像素的子区域(例如),其位于图像的 (30, 30) 处,你可以像这样调用该方法(新参数用粗体表示):
context.drawImage(myimage, `30``,` `30``,` `40``,` `40``,` 20, 140)
要将抓取的部分调整为 100 像素正方形,可以使用以下方式:
context.drawImage(myimage, `30``,` `30``,` `40``,` `40`, 20, 140, 100, 100)
警告
我觉得这非常令人困惑,无法想到这种方法为什么会这样工作的合乎逻辑的原因。但既然它确实如此,我恐怕除了强迫自己记住在哪些条件下哪些参数放在哪里之外,别无他法。
示例 26-20 使用多种调用 drawImage 方法的方式来获得 图 26-19 所显示的结果。为了更清楚,我已经分开参数,使得每列的值提供相同的信息。
示例 26-20. 画布上绘制图像的各种方式
myimage = new Image()
myimage.src = 'image.png'
myimage.onload = function()
{
context.drawImage(myimage, 20, 20 )
context.drawImage(myimage, 140, 20, 220, 220)
context.drawImage(myimage, 380, 20, 80, 220)
context.drawImage(myimage, 30, 30, 40, 40, 20, 140, 100, 100)
}
图 26-19. 调整大小和裁剪图像到画布上的图像
从画布复制
你还可以将一个画布用作绘制到同一个(或另一个)画布的源图像。只需在图像对象的位置提供画布对象的名称,并以与图像相同的方式使用所有其余的参数。
添加阴影
在画布上绘制图像(或图像部分)或者任何其他内容时,还可以通过设置以下一个或多个属性来放置阴影:
shadowOffsetX
阴影向右移动的水平偏移量(如果值为负,则向左移动)。
shadowOffsetY
阴影向下移动的垂直偏移量(如果值为负,则向上移动)。
shadowBlur
模糊阴影轮廓的像素数量。
shadowColor
用于阴影的基础颜色。如果使用了模糊,这种颜色将与模糊区域的背景混合。
这些属性不仅适用于文本和线条,还适用于实心图像,如示例 26-21 中所示,其中一些文本、图像和使用路径创建的对象都添加了阴影。在图 26-20 中,您可以看到阴影智能地围绕图像的可见部分流动,而不仅仅是其矩形边界。
示例 26-21. 在画布上绘制时应用阴影
myimage = new Image()
myimage.src = 'apple.png'
orig = 95
points = 21
dist = Math.PI / points * 2
scale1 = 75
scale2 = 50
myimage.onload = function()
{
context.beginPath()
for (j = 0 ; j < points ; ++j)
{
x = Math.sin(j * dist)
y = Math.cos(j * dist)
context.lineTo(orig + x * scale1, orig + y * scale1)
context.lineTo(orig + x * scale2, orig + y * scale2)
}
context.closePath()
context.shadowOffsetX = 5
context.shadowOffsetY = 5
context.shadowBlur = 6
context.shadowColor = '#444'
context.fillStyle = 'red'
context.stroke()
context.fill()
context.shadowOffsetX = 2
context.shadowOffsetY = 2
context.shadowBlur = 3
context.shadowColor = 'yellow'
context.font = 'bold 36pt Times'
context.textBaseline = 'top'
context.fillStyle = 'green'
context.fillText('Sale now on!', 200, 5)
context.shadowOffsetX = 3
context.shadowOffsetY = 3
context.shadowBlur = 5
context.shadowColor = 'black'
context.drawImage(myimage, 245, 45)
}
图 26-20. 不同类型绘图对象下的阴影
在像素级别编辑
HTML5 画布不仅为您提供了强大的绘图方法范围,还允许您在像素级别直接在引擎盖下进行操作,其中包括三种强大的方法。
getImageData 方法
使用getImageData方法,您可以抓取画布的一部分(或全部),以便您可以按任何方式修改检索到的数据,然后将其保存回画布的其他位置(或另一画布)。
为了说明这是如何工作的,示例 26-22 首先加载一个现成的图像并将其绘制到画布上。然后,画布数据被读取到一个名为idata的对象中,其中所有颜色被平均在一起以将每个像素更改为灰度,然后稍微调整以将每种颜色向深褐色移动,如图 26-21 所示。下一部分解释了像素的data数组及当值50被添加到或从数组元素中减去时会发生什么。
示例 26-22. 操纵图像数据
myimage = new Image()
myimage.src = 'photo.jpg'
myimage.crossOrigin = ''
myimage.onload = function()
{
context.drawImage(myimage, 0, 0)
idata = context.getImageData(0, 0, myimage.width, myimage.height)
for (y = 0 ; y < myimage.height ; ++y)
{
pos = y * myimage.width * 4
for (x = 0 ; x < myimage.width ; ++x)
{
average =
(
idata.data[pos] +
idata.data[pos + 1] +
idata.data[pos + 2]
) / 3
idata.data[pos] = average + 50
idata.data[pos + 1] = average
idata.data[pos + 2] = average - 50
pos += 4;
}
}
context.putImageData(idata, 320, 0)
}
图 26-21. 将图像转换为深褐色(在灰度模式下查看此图像时,只会看到细微差异)
data 数组
这种图像操作依赖于data数组,它是通过调用getImageData返回的idata对象的属性。此方法返回一个包含所选区域的所有像素数据的数组,其组成部分包括红色、绿色、蓝色和 alpha 透明度。因此,每个彩色像素使用四个数据项来存储。
许多最近的浏览器已采纳严格的安全措施以防止跨源攻击,这就是为什么在这个示例中我们必须为myimage对象添加crossOrigin属性,并将其值设置为空字符串(代表默认值为'anonymous'),以明确允许读取图像数据。出于同样的安全原因,该示例只有在从 Web 服务器(如在线服务器或第二章中的 AMPPS 安装)加载时才能正确工作;在仅从本地文件系统加载时将无法正确工作。
所有数据按顺序存储在data数组中,因此红色值的后面是蓝色值,然后是绿色值,最后是透明度值;然后,数组中的下一个项是下一个像素的红色值,依此类推。因此,对于位置(0, 0)的像素,你将会有以下数值:
idata.data[0] // Red level
idata.data[1] // Green level
idata.data[2] // Blue level
idata.data[3] // Alpha level
然后是位置(1, 0),如下所示:
idata.data[4] // Red level
idata.data[5] // Green level
idata.data[6] // Blue level
idata.data[7] // Alpha level
在这个图像中,一切都继续以同样的方式进行,直到第一行中最右边的像素——即第 0 行中的第 320 像素,位于位置(319, 0)——被达到。此时,将 319 乘以 4(每个像素数据项的数量)以获得以下数组元素,它们包含这个像素的数据:
idata.data[1276] // Red level
idata.data[1277] // Green level
idata.data[1278] // Blue level
idata.data[1279] // Alpha level
这会导致数据指针移动回到图像的第一列,但这次是第 1 行的第一列,即位置(0, 1),其偏移量为(0 × 4) + (1 × 320 × 4),即 1,280:
idata.data[1280] // Red level
idata.data[1281] // Green level
idata.data[1282] // Blue level
idata.data[1283] // Alpha level
因此,如果图像数据存储在idata中,图像宽度为w,要访问的像素位置为x和y,则在直接访问图像数据时使用的关键公式如下:
red = idata.data[x * 4 + y * w * 4 ]
green = idata.data[x * 4 + y * w * 4 + 1]
blue = idata.data[x * 4 + y * w * 4 + 2]
alpha = idata.data[x * 4 + y * w * 4 + 3]
使用这些知识,我们通过仅获取每个像素的红色、蓝色和绿色分量并求平均值,来创建图 26-12 中的棕色效果(其中pos是当前像素数组位置的变量指针):
average =
(
idata.data[pos] +
idata.data[pos + 1] +
idata.data[pos + 2]
) / 3
现在average包含了平均颜色值(通过将所有像素值相加并除以 3 获得),这个值被写回到像素的所有颜色中,但红色增加了值50,蓝色减少了相同数量:
idata.data[pos] = average + 50
idata.data[pos + 1] = average
idata.data[pos + 2] = average - 50
结果是增加每个像素的红色值并减少蓝色值(否则这些颜色只需写回平均值将成为单色图像),从而赋予它棕色色调。
放置图像数据方法
当你修改了图像数据数组以满足你的要求后,只需像前面的示例中显示的那样调用putImageData方法,传递idata对象和应该出现的左上角的坐标即可将其写入画布。之前显示的调用将修改后的图像副本放置在原始图像的右侧:
context.putImageData(idata, 320, 0)
注意
如果你只想修改画布的一部分,你无需获取整个画布;只需获取包含你感兴趣区域的部分。你也不需要将图像数据写回到原来获取它的位置;图像数据可以写入画布的任何部分。
创建图像数据方法
你不必直接从画布创建对象;你也可以通过调用createImageData方法创建一个新的对象,并使用空白数据。以下示例创建一个宽度为 320 像素、高度为 240 像素的对象:
idata = createImageData(320, 240)
或者,你可以像这样从现有对象创建一个新对象:
newimagedataobject = createImageData(imagedata)
然后由您决定如何向这些对象添加像素数据或以其他方式修改它们,如何将它们粘贴到画布上或从它们创建其他对象,等等。
高级图形效果
在 HTML5 画布上的更高级功能之一是能够分配各种合成和透明效果,以及应用强大的转换,如缩放、拉伸和旋转。
全局合成操作属性
有 12 种不同的方法可用于精细调整如何在画布上放置对象,考虑到现有和未来的对象。这些称为合成选项,并且它们的应用方式如下:
context.globalCompositeOperationProperty = 'source-over'
合成类型如下:
source-over
默认。源图像被复制到目标图像上。
source-in
只显示出现在目标内的源图像部分,并移除目标图像。源图像的任何 alpha 透明度会导致其下的目标被移除。
source-out
只显示未出现在目标内的源图像部分,并移除目标图像。源图像的任何 alpha 透明度会导致其下的目标被移除。
source-atop
源图像在覆盖目标的地方显示。目标图像在目标不透明且源图像透明时显示。其他区域为透明。
destination-over
源图像在目标图像下绘制。
destination-in
在源和目标图像重叠但源图像透明部分不在的区域显示目标图像。源图像不显示。
destination-out
只显示目标中在源图像非透明部分之外的部分。源图像不显示。
destination-atop
源图像显示在目标未显示的位置。目标和源重叠的地方,显示目标图像。源图像的任何透明部分防止目标图像的该区域显示。
lighter
应用源和目标的总和,以便它们不重叠的地方正常显示;它们重叠的地方,显示两者的总和但变亮。
darker
应用源和目标的总和,以便它们不重叠的地方正常显示;它们重叠的地方,显示两者的总和但变暗。
copy
源图像复制到目标上。源的任何透明区域会导致它所覆盖的目标不显示。
xor
当源和目标图像不重叠时,它们显示为正常。它们重叠的区域,它们的颜色值进行异或运算。
示例 26-23 通过创建 12 个不同的画布来展示所有这些合成类型的效果,每个画布上都有两个对象(一个填充的圆和阴阳图像),它们彼此偏移但有重叠。
示例 26-23. 使用所有 12 种合成效果
image = new Image()
image.src = 'image.png'
image.onload = function()
{
types =
[
'source-over', 'source-in', 'source-out',
'source-atop', 'destination-over', 'destination-in',
'destination-out', 'destination-atop', 'lighter',
'darker', 'copy', 'xor'
]
for (j = 0 ; j < 12 ; ++j)
{
canvas = O('c' + (j + 1))
context = canvas.getContext('2d')
S(canvas).background = 'lightblue'
context.fillStyle = 'red'
context.arc(50, 50, 50, 0, Math.PI * 2, false)
context.fill()
context.globalCompositeOperation = types[j]
context.drawImage(image, 20, 20, 100, 100)
}
}
注意
与本章的其他一些示例类似,此示例(可从配套网站下载)包含一些 HTML 和/或 CSS 来增强显示效果,这里未显示因为它对程序的运行不是必需的。
此程序使用 for 循环来迭代存储在数组 types 中的每种合成类型。每次循环时,在先前某些 HTML(未显示)中已经创建的 12 个画布元素上创建一个新的上下文,它们的 ID 分别为 c1 到 c12。
在每个画布上,首先放置一个直径为 100 像素的红色圆在左上角,然后选择合成类型并将阴阳图像放置在圆上,但向右和向下偏移 20 像素。图 26-22 显示了每种类型的效果。正如你所见,可以实现多种效果。
图 26-22. 12 种合成效果示例
globalAlpha 属性
在画布上绘图时,可以使用 globalAlpha 属性指定要应用的透明度,支持从 0(完全透明)到 1(完全不透明)的值。以下命令将 alpha 设置为 0.9,使得未来的绘图操作将是 90% 不透明(或 10% 透明):
context.globalAlpha = 0.9
此属性可与所有其他属性一起使用,包括合成选项。
变换
在将元素绘制到 HTML5 画布时,画布支持四个函数来应用变换:scale、rotate、translate 和 transform。它们可以单独或一起使用,产生更加有趣的效果。
缩放方法
你可以通过首先调用 scale 方法来缩放未来的绘图操作。该方法接受水平和垂直缩放因子,可以为负、零或正。
在示例 26-24 中,阴阳图像以其原始尺寸 100 × 100 像素绘制到画布上。然后水平放大三倍和垂直放大两倍,再次调用drawImage函数将伸展的图像放置在原始图像旁边。最后,使用值为0.33和0.5再次应用缩放来恢复一切正常,并且再次绘制图像,这次是在原始图像下方。图 26-23 显示了结果。
示例 26-24. 放大和缩小尺寸
context.drawImage(myimage, 0, 0)
context.scale(3, 2)
context.drawImage(myimage, 40, 0)
context.scale(.33, .5)
context.drawImage(myimage, 0, 100)
图 26-23. 将图像放大然后再次缩小
仔细观察,你会发现原图下方的复制图像由于放大和再次缩小而变得稍微模糊了一点。
通过使用一个或多个缩放参数的负值,你可以在水平或垂直方向(或两者)中反转一个元素,同时(或代替)缩放。例如,以下代码翻转上下文以创建镜像图像:
context.scale(-1, 1)
save和restore方法
如果你需要对不同绘图元素进行多次缩放操作,不仅可能会使结果变得模糊,而且计算起来也会非常耗时,例如,三倍放大后再缩小需要0.33的值来还原(而两倍放大则需要0.5的值来逆转)。
因此,你可以在发出scale调用之前调用save保存当前上下文,并在稍后通过restore调用将缩放恢复到正常。查看下面的示例,可以替换示例 26-24 中的代码:
context.drawImage(myimage, 0, 0)
`context``.``save``(``)`
context.scale(3, 2)
context.drawImage(myimage, 40, 0)
`context``.``restore``(``)`
context.drawImage(myimage, 0, 100)
save和restore方法非常强大,因为它们不仅适用于图像缩放,实际上它们适用于以下所有属性,并且因此可以随时用于保存当前属性,然后稍后恢复它们:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign和textBaseline。所有四个转换方法的属性也由save和restore管理:scale、rotate、translate和transform。
旋转方法
使用rotate方法,你可以选择应用对象(或任何绘图方法)到画布的角度。角度用弧度指定,与 180 / π相同,约为 57 度。
旋转发生在画布原点周围,默认情况下是左上角(但很快你会看到,这可以更改)。示例 26-25 展示了四次显示阴阳图像,每个连续的图像旋转Math.PI / 25弧度。
示例 26-25. 旋转图像
for (j = 0 ; j < 4 ; ++j)
{
context.drawImage(myimage, 20 + j * 120 , 20)
context.rotate(Math.PI / 25)
}
如你所见,在图 26-24 中,结果可能并非你所期望的,因为图像并未围绕自身旋转。相反,旋转发生在画布原点(位置为(0, 0))。更重要的是,每次新的旋转都累加了前一个旋转。但是,为了纠正这些问题,你可以始终使用translate方法与save和restore方法结合使用。
图 26-24. 四种不同旋转角度的图像
注意
弧度是一个合理的度量单位,因为一个完整圆有 π × 2 弧度。因此,π 弧度是半圆,π ÷ 2 弧度是一个四分之一圆,π ÷ 2 × 3(或 π × 1.5)弧度是三分之三圆,等等。为了避免记住 π 的值,你可以随时参考Math.PI中的值。
translate 方法
要改变旋转的原点,你可以调用translate方法将其移动到其他位置。目标位置可以在画布内(或外)的任何地方。通常,你会指定对象目标位置内的某个点(通常是其中心)。
示例 26-26 在每次调用rotate之前执行此转换,现在产生了可能由前面示例意图的效果。此外,在每次操作之前和之后调用save和restore方法,确保每次旋转都是独立应用的,而不是在上一个旋转的基础上累加的。
示例 26-26. 在原地旋转对象
w = myimage.width
h = myimage.height
for (j = 0 ; j < 4 ; ++j)
{
context.save()
context.translate(20 + w / 2 + j * (w + 20), 20 + h / 2)
context.rotate(Math.PI / 5 * j)
context.drawImage(myimage, -(w / 2), -(h / 2))
context.restore()
}
在这个例子中,在每次旋转之前,上下文被保存,并且原点被转换到每个图像将被绘制的确切中心点。然后我们发出旋转指令,并通过提供负值将图像绘制到新原点的左上方,使其中心与原点匹配。其结果显示在图 26-25 中。
图 26-25. 在原地旋转图像
总结一下:当你希望在原地旋转或变换(接下来描述)一个对象时,你应该执行以下操作:
-
保存上下文。
-
将画布原点转换为要放置对象的中心。
-
发出旋转或变换指令。
-
使用任何支持的绘图方法绘制对象,使用负的目标位置点宽度的一半向左和高度的一半向上绘制。
-
恢复上下文以恢复原点。
transform 方法
当你已经尝试过所有其他画布功能,但仍无法以你需要的方式操纵对象时,就该使用transform方法了。使用它,你可以在画布上绘制的对象应用变换矩阵,给你提供多种可能性和强大的功能,可以在单个指令中结合缩放和旋转。
该方法使用的转换矩阵是一个 3 × 3 矩阵,具有 9 个值,但只有 6 个是外部提供给transform方法的。因此,与其解释这个矩阵乘法是如何工作的,我只需要解释其六个参数的效果,依次是(顺序可能有点反直觉):
-
水平缩放
-
水平斜切
-
垂直斜切
-
垂直缩放
-
水平平移
-
垂直平移
你可以通过多种方式应用这些值,例如,通过模仿示例 26-24 中的scale方法来替换这个调用:
context.scale(3, 2)
以下是:
context.transform(`3`, 0, 0, `2`, 0, 0)
同样,你可以用示例 26-26 中的这个调用替换它:
context.translate(20 + w / 2 + j * (w + 20), 20 + h / 2)
以下是:
context.transform(1, 0, 0, 1, `20` `+` `w` `/` `2` `+` `j` `*` `(``w` `+` `20``)`, `20` `+` `h` `/` `2`)
注意
请注意,水平和垂直缩放参数被赋予值1,以确保 1:1 的结果,而斜切值为0,以防止结果被斜切。
你甚至可以结合前两行代码一起进行翻译和同时缩放,就像这样:
context.transform(`3`, 0, 0, `2`, `20` `+` `w` `/` `2` `+` `j` `*` `(``w` `+` `20``)`, `20` `+` `h` `/` `2`)
正如你所预料的,斜切参数会将元素按指定方向倾斜,例如,从正方形创建一个菱形。
作为斜切的另一个例子,示例 26-27 在画布上绘制阴阳图像,然后用transform方法创建一个斜切副本。斜切值可以是任何负数、零或正数,但我选择了水平值为1,这使得图像底部向右斜切了一个图像宽度,并按比例拉动了其他部分(见图 26-26)。
示例 26-27. 创建一个原始和斜切的图像
context.drawImage(myimage, 20, 20)
context.transform(1, 0, `1`, 1, 0, 0)
context.drawImage(myimage, 140, 20)
图 26-26. 将对象水平向右倾斜
注意
你甚至可以通过提供一个负值和一个相反的正斜切值来用transform旋转一个对象。但要注意:这样做会修改元素的大小,因此你还需要同时调整比例参数。另外,你需要记住平移原点。因此,我建议在完全熟练使用transform之前,坚持使用rotate方法。
setTransform 方法
作为使用save和restore方法的替代方案,你可以设置绝对变换,这将重置变换矩阵,然后应用提供的值。像这样使用setTransform方法(它应用了一个水平正斜切值为1的例子):
context.setTransform(1, 0, `1`, 1, 0, 0)
注意
要了解更多关于变换矩阵的信息,请参阅Wikipedia 文章。
HTML5 画布是网页开发者制作更大、更好、更专业和更引人入胜网站的重要资产。在接下来的章节中,我们将看看另外两个伟大的 HTML5 功能:无需插件的浏览器内音频和视频。
问题
-
如何在 HTML 中创建一个画布元素?
-
如何让 JavaScript 访问画布元素?
-
如何开始和结束创建画布路径?
-
你可以使用什么方法将画布中的数据提取成图像?
-
如何创建包含超过两种颜色的渐变填充?
-
在绘图时如何调整线条的宽度?
-
你会使用哪种方法来指定画布的一个部分,以便未来的绘图只发生在那个区域内?
-
如何用两个虚拟吸引子绘制复杂曲线?
-
getImageData方法每像素返回多少个数据项? -
transform方法的哪两个参数用于缩放操作?
查看“第二十六章答案”,在附录 A 中找到这些问题的答案。