JavaScript 客户端 Web API
客户端Web API
- 是什么?
- 它们如何工作的?
- 如何在代码中使用它们?
- 它们是如何组织的?
一、Web API简介
1、什么是 API?
- 应用程序接口(API)是基于编程语言构建的结构,使开发人员更容易地创建复杂的功能。它们抽象了复杂的代码,并提供一些简单的接口规则直接使用。
- 例子:想想您的房子、公寓或其他住宅的电力供应,如果您想在您的房子里使用一个设备,只要把电源插头插入插座即可,而不会直接把它连接到进户的电线上 - 这样做非常低效,而且如果您不是电工,这么做将是困难和危险的。
- 同样,比如说,编程来显示一些3D图形,使用以更高级语言编写的API(例如JavaScript或Python)将会比直接编写直接控制计算机的GPU或其他图形功能的低级代码(比如C或C++)来执行操作要容易得多。
客户端 JavaScript 中的 API
客户端JavaScript中有很多可用的API — 他们本身并不是JavaScript语言的一部分,却建立在JavaScript语言核心的顶部,为使用JavaScript代码提供额外的超强能力。他们通常分为两类:
1、浏览器API 内置于Web浏览器中,能从浏览器和电脑周边环境中提取数据,并用来做有用的复杂的事情 。例如Geolocation API提供了一些简单的JavaScript结构以获得位置数据,因此您可以在Google地图上标示您的位置。在后台,浏览器确实使用一些复杂的低级代码(例如C++)与设备的GPS硬件(或可以决定位置数据的任何设施)通信来获取位置数据并把这些数据返回给您的代码中使用浏览器环境;但是,这种复杂性通过API抽象出来,因而与您无关。
2、第三方API 缺省情况下不会内置于浏览器中,通常必须在Web中的某个地方获取代码和信息。例如Twitter API 使您能做一些显示最新推文这样的事情,它提供一系列特殊的结构,可以用来请求Twitter服务并返回特殊的信息。
JavaScript,API和其他JavaScript工具之间的关系
- JavaScript:一种内置于浏览器的高级脚本语言,您可以用来实现Web页面/应用中的功能。注意JavaScript也可用于其他象Node这样的的编程环境。但现在您不必考虑这些。
- 客户端 API:内置于浏览器的结构程序,位于JavaScript语言顶部,使您可以更容易的实现功能。
- 第三方 API : 置于第三方普通的结构程序(例如Twitter,Facebook),使您可以在自己的Web页面中使用那些平台的某些功能(例如在您的Web页面显示最新的Tweets)。
- JavaScript 库:常是包含具有特定功能的一个或多个JavaScript文件,把这些文件关联到您的Web页以快速或授权编写常见的功能。例如包含
jQuery
和Mootools
- JavaScript 框架: 从库开始的下一步,JavaScript 框架视图把 HTML、CSS、JavaScript 和其他安装的技术打包在一起,然后用来从头编写一个完整的 Web 应用。
2、API 可以做什么?
在主流浏览器中有大量的可用API,您可以在代码中做许多的事情,对此可以查看 MDN API index page。
a、常见浏览器API
- (1)操作文档的API :内置于浏览器中。最明显的例子是DOM(文档对象模型)API,它允许您操作HTML和CSS — 创建、移除以及修改HTML,动态地将新样式应用到您的页面,等等。每当您看到一个弹出窗口出现在一个页面上,或者显示一些新的内容时,这都是DOM的行为。 您可以在在Manipulating documents中找到关于这些类型的API的更多信息。
- (2)从服务器获取数据的API : 用于更新网页的一小部分是相当好用的。这个看似很小的细节能对网站的性能和行为产生巨大的影响 — 如果您只是更新一个股票列表或者一些可用的新故事而不需要从服务器重新加载整个页面将使网站或应用程序感觉更加敏感和“活泼”。使这成为可能的API包括XMLHttpRequest和Fetch API。您也可能会遇到描述这种技术的术语Ajax。您可以在Fetching data from the server找到关于类似的API的更多信息。
- (3)用于绘制和操作图形的API : 目前已被浏览器广泛支持 — 最流行的是允许您以编程方式更新包含在HTML 元素中的像素数据以创建2D和3D场景的Canvas和WebGL。例如,您可以绘制矩形或圆形等形状,将图像导入到画布上,然后使用Canvas API对其应用滤镜(如棕褐色滤镜或灰度滤镜),或使用WebGL创建具有光照和纹理的复杂3D场景。这些API经常与用于创建动画循环的API(例如window.requestAnimationFrame())和其他API一起不断更新诸如动画和游戏之类的场景。
- (4) 音频和视频API : 例如HTMLMediaElement,Web Audio API和WebRTC允许您使用多媒体来做一些非常有趣的事情,比如创建用于播放音频和视频的自定义UI控件,显示字幕字幕和您的视频,从网络摄像机抓取视频,通过画布操纵(见上),或在网络会议中显示在别人的电脑上,或者添加效果到音轨(如增益,失真,平移等) 。
- (5) 设备API : 基本上是以对网络应用程序有用的方式操作和检索现代设备硬件中的数据的API。我们已经讨论过访问设备位置数据的地理定位API,因此您可以在地图上标注您的位置。其他示例还包括通过系统通知(参见Notifications API)或振动硬件(参见Vibration API)告诉用户Web应用程序有用的更新可用。
- (6) 客户端存储API : 在Web浏览器中的使用变得越来越普遍 - 如果您想创建一个应用程序来保存页面加载之间的状态,甚至让设备在处于脱机状态时可用,那么在客户端存储数据将会是非常有用的。例如使用Web Storage API的简单的键 - 值存储以及使用IndexedDB API的更复杂的表格数据存储。
b、常见第三方API
- 第三方API种类繁多; 下列是一些比较流行的你可能迟早会用到的第三方API:
- The Twitter API, 允许您在您的网站上展示您最近的推文等。
- The Google Maps APIGoogle Maps API 允许你在网页上对地图进行很多操作(这很有趣,它也是Google地图的驱动器)。现在它是一整套完整的,能够胜任广泛任务的API。其能力已经被Google Maps API Picker见证。
- The Facebook suite of API 允许你将很多Facebook生态系统中的功能应用到你的app,使之受益,比如说它提供了通过Facebook账户登录、接受应用内支付、推送有针对性的广告活动等功能。
- The YouTube API, 允许你将Youtube上的视频嵌入到网站中去,同时提供搜索Youtube,创建播放列表等众多功能。
- The Twilio API, 其为您的app提供了针对语音通话和视频聊天的框架,以及从您的app发送短信息或多媒体信息等诸多功能。
- 注: 你可以在 Programmable Web API directory.上发现更多关于第三方API的信息。
3、API如何工作?
不同的JavaScript API以稍微不同的方式工作,但通常它们具有共同的特征和相似的主题。
a、它们是基于对象的
- API使用一个或多个 JavaScript objects 在您的代码中进行交互,这些对象用作API使用的数据(包含在对象属性中)的容器以及API提供的功能(包含在对象方法中)。
- 让我们回到Geolocation API的例子 - 这是一个非常简单的API,由几个简单的对象组成:
- Geolocation, 其中包含三种控制地理数据检索的方法
- Position, 表示在给定的时间的相关设备的位置。 — 它包含一个当前位置的 Coordinates 对象。还包含了一个时间戳,这个时间戳表示获取到位置的时间。
- Coordinates, 其中包含有关设备位置的大量有用数据,包括经纬度,高度,运动速度和运动方向等。
- 那么这些对象如何相互作用?如果您查看我们的 maps-example.html 示例 (也可以参考:see it live also), 您将看到以下代码:
navigator.geolocation.getCurrentPosition(function(position) { var latlng = new google.maps.LatLng(position.coords.latitude,position.coords.longitude); var myOptions = { `zoom: 8,` `center: latlng,` `mapTypeId: google.maps.MapTypeId.TERRAIN,` `disableDefaultUI: true` } var map = new google.maps.Map(document.querySelector("#map_canvas"), myOptions); });
- Note: 当您第一次加载上述实例,应当存在一个对话框询问您是否乐意对此应用共享位置信息(参见 They have additional security mechanisms where appropriate 这一稍后将会提到的部分)。 您需要同意这项询问以将您的位置于地图上绘制。如果您始终无法看见地图,您可能需要手动修改许可项。修改许可项的方法取决于您使用何种浏览器,对于Firefox浏览器来说,在页面信息 > 权限 中修改位置权限,在Chrome浏览器中则进入 设置 > 隐私 > 显示高级设置 > 内容设置,其后修改位置设定。
- 我们首先要使用 Geolocation.getCurrentPosition() 方法返回设备的当前位置。浏览器的 Geolocation 对象通过调用 Navigator.geolocation 属性来访问.
navigator.geolocation.getCurrentPosition(function(position) { ... });
- 这相当于做同样的事情
var myGeo = navigator.geolocation;
myGeo.getCurrentPosition(function(position) { ... });
- 但是我们可以使用 "点运算符" 将我们的属性和方法的访问链接在一起,减少了我们必须写的行数。
Geolocation.getCurrentPosition()
方法只有一个必须的参数,这个参数是一个匿名函数,当设备的当前位置被成功取到时,这个函数会运行。 这个函数本身有一个参数,它包含一个表示当前位置数据的 Position 对象。- 注意:由另一个函数作为参数的函数称为 (callback function "回调函数").
- 仅在操作完成时调用函数的模式在JavaScript API中非常常见 - 确保一个操作已经完成,然后在另一个操作中尝试使用该操作返回的数据。这些被称为 asynchronous “异步”操作。由于获取设备的当前位置依赖于外部组件(设备的GPS或其他地理定位硬件), 我们不能保证会立即使用返回的数据。 因此,这样子是行不通的:
var position = navigator.geolocation.getCurrentPosition();
var myLatitude = position.coords.latitude;
- 如果第一行还没有返回结果,则第二行将会出现错误,因为位置数据还不可用。 出于这个原因,涉及同步操作的API被设计为使用 callback functions “回调函数”,或更现代的 Promises 系统,这些系统在ECMAScript 6中可用,并被广泛用于较新的API。
- 我们将Geolocation API与第三方API(Google Maps API)相结合, — 我们正在使用它来绘制Google地图上由
getCurrentPosition()
返回的位置。 我们通过链接到页面上使这个API可用
<script type="text/javascript"
src="https://maps.google.com/maps/API/js?key=AIzaSyDDuGt0E5IEGkcE6ZfrKfUtE9Ko_de66pA"></script>
- 要使用该API, 我们首先使用
google.maps.LatLng()
构造函数创建一个LatLng对象实例, 该构造函数需要我们的地理定位 Coordinates.latitude 和 Coordinates.longitude值作为参数:
var latlng = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
- 该对象实例被设置为
myOptions
对象的center
属性的值。然后我们通过调用google.maps.Map()
构造函数创建一个对象实例来表示我们的地图, 并传递它两个参数 — 一个参数是我们要渲染地图的<div>
元素的引用 (ID为 map_canvas), 以及另一个参数是我们在上面定义的myOptions对象
var myOptions = {
zoom: 8,
center: latlng,
mapTypeId: google.maps.MapTypeId.TERRAIN,
disableDefaultUI: true
}
var map = new google.maps.Map(document.querySelector("#map_canvas"), myOptions);
- 最后一块代码突出显示了您将在许多API中看到的两种常见模式。 首先,API对象通常包含构造函数,可以调用这些构造函数来创建用于编写程序的对象的实例。 其次,API对象通常有几个可用的options(如上面的myOptions对象),可以调整以获得您的程序所需的确切环境(根据不同的环境,编写不同的Options对象)。 API构造函数通常接受options对象作为参数,这是您设置这些options的地方。
b、它们有可识别的入口点
- 使用API时,应确保知道API入口点的位置。 在Geolocation API中,这非常简单 - 它是
Navigator.geolocation
属性, 它返回浏览器的Geolocation
对象,所有有用的地理定位方法都可用。 - 文档对象模型 (DOM) API有一个更简单的入口点 —它的功能往往被发现挂在 Document 对象, 或任何你想影响的HTML元素的实例,例如
var em = document.createElement('em'); // create a new em element
var para = document.querySelector('p'); // reference an existing p element
em.textContent = 'Hello there!'; // give em some text content
para.appendChild(em); // embed em inside para
- 其他API具有稍微复杂的入口点,通常涉及为要编写的API代码创建特定的上下文。例如,Canvas API的上下文对象是通过获取要绘制的 元素的引用来创建的,然后调用它的HTMLCanvasElement.getContext()方法:
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
- 然后,我们想通过调用内容对象 (它是CanvasRenderingContext2D的一个实例)的属性和方法来实现我们想要对画布进行的任何操作, 例如:
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
};
c、它们使用事件来处理状态的变化
- 我们之前已经在课程中讨论了事件,在我们的事件介绍文章中 - 详细介绍了客户端Web事件是什么以及它们在代码中的用法。
- 一些Web API不包含事件,但有些包含一些事件。
- 当事件触发时,允许我们运行函数的处理程序属性通常在单独的 “Event handlers”(事件处理程序) 部分的参考资料中列出。
- 为一个简单的例子,XMLHttpRequest 对象的实例 (每一个实例都代表一个到服务器的HTTP请求,来取得某种新的资源)都有很多事件可用,例如 onload 事件在成功返回时就触发包含请求的资源,并且现在就可用。
- 下面的代码提供了一个简单的例子来说明如何使用它:
var requestURL = 'https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json';
var request = new XMLHttpRequest();
request.open('GET', requestURL);
request.responseType = 'json';
request.send();
request.onload = function() {
var superHeroes = request.response;
populateHeader(superHeroes);
showHeroes(superHeroes);
}
- 注意:您可以在我们的ajax.html示例中看到此代码 (或者 在线运行版本 see it live also).
- 前五行指定了我们要获取的资源的位置,使用XMLHttpRequest() 构造函数创建请求对象的新实例 ,打开HTTP 的 GET 请求以取得指定资源,指定响应以JSON格式发送,然后发送请求。
- 然后 onload 处理函数指定我们如何处理响应。 我们知道请求会成功返回,并在需要加载事件(如onload 事件)之后可用(除非发生错误),所以我们将包含返回的JSON的响应保存在superHeroes变量中,然后将其传递给两个不同的函数以供进一步处理。
d、它们在适当的地方有额外的安全机制
- WebAPI功能受到与JavaScript和其他Web技术(例如同源政策)相同的安全考虑 但是他们有时会有额外的安全机制。例如,一些更现代的WebAPI将只能在通过HTTPS提供的页面上工作,因为它们正在传输潜在的敏感数据(例如 服务工作者 和 推送)。 -另外,一旦调用WebAPI请求,用户就可以在您的代码中启用一些WebAPI请求权限。
- 这些许可提示会被提供给用户以确保安全 - 如果这些提示不在适当位置,那么网站可能会在您不知情的情况下开始秘密跟踪您的位置,或者通过大量恼人的通知向您发送垃圾邮件。
二、操作文档
1、web浏览器的重要部分
- window是载入浏览器的标签,在JavaScript中用Window对象来表示,使用这个对象的可用方法,你可以返回窗口的大小(参见Window.innerWidth和Window.innerHeight),操作载入窗口的文档,存储客户端上文档的特殊数据(例如使用本地数据库或其他存储设备),为当前窗口绑定event handler,等等。
- navigator表示浏览器存在于web上的状态和标识(即用户代理)。在JavaScript中,用Navigator来表示。你可以用这个对象获取一些信息,比如来自用户摄像头的地理信息、用户偏爱的语言、多媒体流等等。
- document(在浏览器中用DOM表示)是载入窗口的实际页面,在JavaScript中用Document 对象表示,你可以用这个对象来返回和操作文档中HTML和CSS上的信息。例如获取DOM中一个元素的引用,修改其文本内容,并应用新的样式,创建新的元素并添加为当前元素的子元素,甚至把他们一起删除。
2、文档对象模型
- 在浏览器标签中当前载入的文档用文档对象模型来表示。这是一个由浏览器生成的“树结构”,使编程语言可以很容易的访问HTML结构 — 例如浏览器自己在呈现页面时,使用它将样式和其他信息应用于正确的元素,而页面呈现完成以后,开发人员可以用JavaScript操作DOM。
<!DOCTYPE html>
<html>
<head>
`<meta charset="utf-8">`
`<title>Simple DOM example</title>`
</head>
<body>
`<section>`
`<img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth.">`
`<p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p>`
`</section>`
</body>
</html>
- 文档中每个元素和文本在树中都有它们自己的入口 — 称之为节点。
- 元素节点: 一个元素,存在于DOM中。
- 根节点: 树中顶层节点,在HTML的情况下,总是一个HTML节点(其他标记词汇,如SVG和定制XML将有不同的根元素)。
- 子节点: 直接位于另一个节点内的节点。例如上面例子中,IMG是SECTION的子节点。
- 后代节点: 位于另一个节点内任意位置的节点。例如 上面例子中,IMG是SECTION的子节点,也是一个后代节点。IMG不是BODY的子节点,因为它在树中低了BODY两级,但它是BODY的后代之一。
- 父节点: 里面有另一个节点的节点。例如上面的例子中BODY是SECTION的父节点。
- 兄弟节点: DOM树中位于同一等级的节点。例如上面例子中,IMG和P是兄弟。
- 文本节点: 包含文字串的节点
3、基本的DOM操作
- 选择dom元素,并储存在一个变量中
var link = document.querySelector('a');
- 使用它的可用属性和方法来操作它
link.textContent = 'Mozilla Developer Network';
link.href = 'https://developer.mozilla.org';
- 注意,和JavaScript中的许多事情一样,有很多方法可以选择一个元素,并在一个变量中存储一个引用。Document.querySelector()是推荐的主流方法,它允许你使用CSS选择器选择元素,使用很方便。上面的querySelector()调用会匹配它在文档中遇到的第一个元素。如果想对多个元素进行匹配和操作,你可以使用Document.querySelectorAll(),这个方法匹配文档中每个匹配选择器的元素,并把它们的引用存储在一个array中。
- Document.getElementById(),选择一个id属性值已知的元素,例如
<p id="myId">My paragraph</p>
。ID作为参数传递给函数,即var elementRef = document.getElementById('myId')
。 - Document.getElementsByTagName(),返回页面中包含的所有已知类型元素的数组。如s, 。元素类型作为参数传递给函数,即
var elementRefArray = document.getElementsByTagName('p')
.
4、创建并放置新的节点
var para = document.createElement('p');
para.textContent = 'We hope you enjoyed the ride.';
- 使用Document.createTextNode()创建一个文本节点:
var text = document.createTextNode(' — the premier source for web development knowledge.');
linkPara.appendChild(text);
5、移动和删除元素
- 如果你想把具有内部链接的段落移到sectioin的底部,简单的做法是:
sect.appendChild(linkPara);
- 这样可以把段落下移到section的底部。你可能想过要做第二个副本,但是情况并非如此 — linkPara是指向该段落唯一副本的引用。如果你想做一个副本并也把它添加进去,只能用Node.cloneNode() 方法来替代。
- 拥有要删除的节点和其父节点的引用:
sect.removeChild(linkPara);
- 要删除一个仅基于自身引用的节点可能稍微有点复杂,这也是很常见的。没有方法会告诉节点删除自己,所以你必须像下面这样操作。
linkPara.parentNode.removeChild(linkPara);
6、操作样式
- 方式一:是直接在想要动态设置样式的元素内部添加内联样式。这是用HTMLElement.style属性来实现。这个属性包含了文档中每个元素的内联样式信息。你可以设置这个对象的属性直接修改元素样式。
para.style.color = 'white';
para.style.backgroundColor = 'black';
para.style.padding = '10px';
para.style.width = '250px';
para.style.textAlign = 'center';
- 结果:注意: CSS样式的JavaSript属性版本以小驼峰式命名法书写,而CSS版本带连接符号(
backgroundColor
对background-color
)。
<p style="color: white; background-color: black; padding:
10px; width: 250px; text-align: center;">We hope you enjoyed the
ride.</p>
- 方式二:在HTML的中添加下列代码 :
<style>
.highlight {
color: white;
background-color: black;
padding: 10px;
width: 250px;
text-align: center;
}
</style>
- 方式三:HTML操作的常用方法 — Element.setAttribute() — 这里有两个参数,你想在元素上设置的属性,你要为它设置的值。
para.setAttribute('class', 'highlight');
- 总结:
- 第一种方式无需安装,适合简单应用,第二种方式更加正统(没有CSS和JavaScript的混合,没有内联样式,而这些被认为是不好的体验)。当你开始构建更大更具吸引力的应用时,你可能会更多地使用第二种方法,但这完全取决于你自己。
- 使用JavaScript创建静态内容是毫无意义的 — 最好将其写入HTML,而不使用JavaScript。
7、从Window对象中获取有用的信息
<!DOCTYPE html>
<html>
<head>
`<meta charset="utf-8">`
`<title>Window resize example</title>`
`<style>`
`body {`
`margin: 0;`
`}`
`div {`
`box-sizing: border-box;`
`width: 100px;`
`height: 100px;`
`background-image: url(bgtile.png);`
`border: 10px solid white;`
`}`
`</style>`
</head>
<body>
`<div></div>`
`<script>`
`// 获取这个div的引用,然后获取视窗(显示文档的内部窗口)的宽度和高度`
`var div = document.querySelector('div');`
`var WIDTH = window.innerWidth;`
`var HEIGHT = window.innerHeight;`
`// 动态地改变div的宽度和高度,使其等于视窗的宽度和高度。`
`div.style.width = WIDTH + 'px';`
`div.style.height = HEIGHT + 'px';`
`// 在我们调整窗口时,我们怎样用事件来调整div的大小?`
`window.onresize = function(){`
`WIDTH = window.innerWidth;`
`HEIGHT = window.innerHeight;`
`div.style.width = WIDTH + 'px';`
`div.style.height = HEIGHT = 'px';`
`}`
`</script>`
</body>
</html>
- 视窗(显示文档的内部窗口)的宽度和高度, 并存入变量中 — 这两个值包含在Window.innerWidth 和 Window.innerHeight属性中。
- 在我们调整窗口时,我们怎样用事件来调整div的大小? Window对象有一个称为resize的可用事件。每次窗口调整大小时都会触发该事件 — 我们可以通过Window.onresize 事件处理程序来访问它,并返回每次改变大小的代码。
8、一个动态的购物单
<!DOCTYPE html>
<html>
<head>
`<meta charset="utf-8">`
`<title>Shopping list example</title>`
`<style>`
`li {`
`margin-bottom: 10px;`
`}`
`li button {`
`font-size: 8px;`
`margin-left: 20px;`
`color: #666;`
`}`
`</style>`
</head>
<body>
`<h1>My shopping list</h1>`
`<div>`
`<label for="item">Enter a new item:</label>`
`<input type="text" name="item" id="item">`
`<button>Add item</button>`
`</div>`
`<ul>`
`</ul>`
`<script>`
`// 1、创建三个变量来保存list(<ul>)、<input>和<button>元素的引用`
`var myUl = document.querySelector('ul');`
`var ipt = document.getElementById('item');`
`var btn = document.querySelector('button');`
`// 2、创建一个函数响应点击按钮。`
`btn.onclick = function(){`
`//alert('message');`
`// 3、在函数体内,开始要在一个变量中存储输入框的当前值。`
`var iptVal = ipt.value;`
`console.log(iptVal);`
`// 4、然后,为输入框元素设置空字符 - ''使其为空`
`iptVal = '';`
`// 5、创建三个新元素 — 一个list项(<li>),<span>和 <button>,并把它们存入变量之中。`
`var lis = document.createElement('li');`
`var span = document.createElement('span');`
`var delBtn = document.createElement('button');`
`// 6、把span和button作为list项的子节点。`
`lis.appendChild(span);`
`lis.appendChild(delBtn);`
`// 7、把之前保存的输入框元素的值设置为span的文本内容,按钮的文本内容设置为'Delete'`
`span.textContent = iptVal;`
`delBtn.textContent = 'Delete';`
`// 8、把list项设置为list的子节点。`
`myUl.appendChild(lis);`
`// 9、为删除按钮绑定事件处理程序。当点击按钮时,删除它所在的整个list项。`
`delBtn.onclick = function(){`
`myUl.removeChild(lis);`
`// 10、最后,使用focus()方法聚焦输入框准备输入下一个购物项。`
`ipt.focus();`
`}`
`}`
`</script>`
</body>
</html>
9、另见
- 你还可以使用更多的特性来操作文档,检查一些参考,看看你能发现些什么?
- (MDN上有完整的Web API 列表,参见Web API index !)
三、从服务器获取数据
1、这里有什么问题?
- 想更新网页的任何部分,例如显示一套新的产品或者加载一个新的页面,你需要再一次加载整个页面。这是非常浪费的并且导致了差的用户体验尤其是现在的页面越来越大且越来越复杂。 ###(1)Ajax开始
- 这导致了创建允许网页请求小块数据(例如 HTML, XML, JSON, 或纯文本) 和 仅在需要时显示它们的技术,从而帮助解决上述问题。
- 通过使用诸如 XMLHttpRequest 之类的API或者 — 最近以来的 Fetch API 来实现. 这些技术允许网页直接处理对服务器上可用的特定资源的 HTTP 请求,并在显示之前根据需要对结果数据进行格式化。
- 注意:在早期,这种通用技术被称为Asynchronous JavaScript and XML(Ajax), 因为它倾向于使用
XMLHttpRequest
来请求XML数据。 但通常不是这种情况 (你更有可能使用 XMLHttpRequest 或 Fetch 来请求JSON), 但结果仍然是一样的,术语“Ajax”仍然常用于描述这种技术。 - Ajax模型包括使用Web API作为代理来更智能地请求数据,而不仅仅是让浏览器重新加载整个页面。
- 意义:
- 页面更新速度更快,您不必等待页面刷新,这意味着该网站体验感觉更快,响应更快。
- 每次更新都会下载更少的数据,这意味着更少地浪费带宽。在宽带连接的桌面上这可能不是一个大问题,但是在移动设备和发展中国家没有无处不在的快速互联网服务是一个大问题。
- 为了进一步提高速度,有些网站还会在首次请求时将资产和数据存储在用户的计算机上,这意味着在后续访问中,他们将使用本地版本,而不是在首次加载页面时下载新副本。 内容仅在更新后从服务器重新加载。
2、基本的Ajax请求
(1)XMLHttpRequest
- XMLHttpRequest (通常缩写为XHR)现在是一个相当古老的技术 - 它是在20世纪90年代后期由微软发明的,并且已经在相当长的时间内跨浏览器进行了标准化。
- 1、为例子做些准备, 将 ajax-start.html 和四个文本文件 — verse1.txt, verse2.txt, verse3.txt, verse4.txt
- 2、在
<script>
的内部, 添加下面的代码. 将<select>
和<pre>
元素的引用存储到变量中, 并定义一个 onchange 事件处理函数,可以在select的值改变时, 将其值传递给updateDisplay()
函数作为参数。
var verseChoose = document.querySelector('select');
var poemDisplay = document.querySelector('pre');
verseChoose.onchange = function() {
var verse = verseChoose.value;
updateDisplay(verse);
};
- 3、定义
updateDisplay()
函数
function updateDisplay(verse) {
};
- 4、我们将通过构造一个 指向我们要加载的文本文件的相对URL 来启动我们的函数, 因为我们稍后需要它. 任何时候
<select>
元素的值都与所选的<option>
内的文本相同 (除非在值属性中指定了不同的值) — 例如 "Verse 1". 相应的诗歌文本文件是 "verse1.txt", 并与HTML文件位于同一目录中, 因此只需要文件名即可。 但是,Web服务器往往是区分大小写的,文件名没有空格。 要将“Verse 1”转换为“verse1.txt”,我们需要将V转换为小写,删除空格,并在末尾添加.txt。 这可以通过replace()
,toLowerCase()
, 和 简单的 string concatenation 来完成. 在updateDisplay()
函数中添加以下代码:
verse = verse.replace(" ", "");
verse = verse.toLowerCase();
var url = verse + '.txt';
- 5、要开始创建XHR请求,您需要使用
XMLHttpRequest()
的构造函数创建一个新的请求对象。 你可以把这个对象叫做你喜欢的任何东西, 但是我们会把它叫做 request 来保持简单. 在之前的代码中添加以下内容:var request = new XMLHttpRequest();
- 6、接下来,您需要使用open()方法来指定用于从网络请求资源的 HTTP request method , 以及它的URL是什么。我们将在这里使用 GET 方法, 并将URL设置为我们的 url 变量. 在你上面的代码中添加以下代码:
request.open('GET', url);
- 7、接下来,我们将设置我们期待的响应类型 — 这是由请求的 responseType 属性定义的 — 作为 text. 这并不是绝对必要的 — XHR默认返回文本 —但如果你想在以后获取其他类型的数据,养成这样的习惯是一个好习惯. 接下来添加:
request.responseType = 'text';
- 8、从网络获取资源是一个 asynchronous "异步" 操作, 这意味着您必须等待该操作完成(例如,资源从网络返回),然后才能对该响应执行任何操作,否则会出错,将被抛出错误。 XHR允许你使用它的 onload 事件处理器来处理这个事件 — 当onload 事件触发时(当响应已经返回时)这个事件会被运行。 发生这种情况时, response 数据将在XHR请求对象的响应属性中可用。
- 9、在后面添加以下内容. 你会看到,在 onload 事件处理程序中,我们将 poemDisplay (
元素 ) 的 textContent 设置为 request.response 属性的值。
request.onload = function() {
poemDisplay.textContent = request.response;
};
- 10、以上都是XHR请求的设置 — 在我们告诉它之前,它不会真正运行,这是通过 send() 完成的.
- 11、这个例子中的一个问题就是它首次加载时不会显示任何诗。 为了解决这个问题,在代码的底部添加以下两行 (正好在关闭的 </script> 标签之上) 默认加载第1节,并确保 元素始终显示正确的值:
updateDisplay('Verse 1');
verseChoose.value = 'Verse 1';
(2)Fetch
- Fetch API基本上是XHR的一个现代替代品——它是最近在浏览器中引入的,它使异步HTTP请求在JavaScript中更容易实现,对于开发人员和在Fetch之上构建的其他API来说都是如此。
- 将
updateDisplay()
里的XHR代码:
var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'text';
request.onload = function() {
poemDisplay.textContent = request.response;
};
request.send();
- 替换成Fetch
fetch(url).then(function(response) {
response.text().then(function(text) {
`poemDisplay.textContent = text;`
});
});
- 那么Fetch代码中发生了什么呢?
- 首先,我们调用了
fetch()
方法,将我们要获取的资源的URL传递给它。这相当于现代版的XHR中的request.open()
,另外,您不需要任何等效的send()
方法。 - 然后,你可以看到
.then()
方法连接到了fetch()
末尾-这个方法是Promises
的一部分,是一个用于执行异步操作的现代JavaScript特性。fetch()
返回一个promise,它将解析从服务器发回的响应。我们使用then()
来运行一些后续代码,这是我们在其内部定义的函数。这相当于XHR版本中的onload
事件处理程序。 - 当
fetch()
promise 解析时,这个函数会自动将响应从服务器传递给参数。在函数内部,我们获取响应并运行其text()
方法。这基本上将响应作为原始文本返回,这相当于在XHR版本中的responseType = 'text'
。 - 你会看到
text()
也返回了一个 promise, 所以我们连接另外一个.then()
到它上面, 在其中我们定义了一个函数来接收text()
promise解析的生文本。 - 在promise的函数内部,我们做的和在XHR版本中差不多— 设置
<pre>
元素的文本内容为text的值。
- 首先,我们调用了
- 完整例子(在server端运行才能运行XHR请求)
<html> <head> `<meta charset="utf-8">` `<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">` `<meta name="viewport" content="width=device-width">` `<title>Ajax starting point</title>` `<style>` `html, pre {` `font-family: sans-serif;` `}` `body {` `width: 500px;` `margin: 0 auto;` `background-color: #ccc;` `}` `pre {` `line-height: 1.5;` `letter-spacing: 0.05rem;` `padding: 1rem;` `background-color: white;` `}` `label {` `width: 200px;` `margin-right: 33px;` `}` `select {` `width: 350px;` `padding: 5px;` `}` `</style>` `<!--[if lt IE 9]>` `<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"> </script>` `<![endif]-->` </head> <body> `<h1>Ajax starting point</h1>` `<form>` `<label for="verse-choose">Choose a verse</label>` `<select id="verse-choose" name="verse-choose">` `<option>Verse 1</option>` `<option>Verse 2</option>` `<option>Verse 3</option>` `<option>Verse 4</option>` `</select>` `</form>` `<h2>The Conqueror Worm, <em>Edgar Allen Poe, 1843</em></h2>` `<pre></pre>` `<script>` `var verseChoose = document.querySelector('select');` `var poemDisplay = document.querySelector('pre');` `verseChoose.onchange = function(){` `var verse = verseChoose.value;` `updateDisplay(verse);` `}` `function updateDisplay(verse){` `verse = verse.replace(' ','');` `verse = verse.toLowerCase();` `var url = verse + '.txt';` `}` `// XHR ++#EW45to\*/` `// var request = new XMLHttpRequest();` `// request.open('GET', url);` `// request.responseType = 'text';` `// request.onload = function(){` `// poemDisplay.textContent = request.response;` `// }` `// request.send();` `// Fetch` `fetch(url).then(function(response){` `response.text().then(function(text){` `poemDisplay.textContent = text;` `})` `})` `updateDisplay('Verse 1');` `verseChoose.value = 'Verse 1';` `</script>` </body> </html>
(3)关于promises
大多数现代的JavaScript api都是基于promises的。
fetch(url).then(function(response) { response.text().then(function(text) { `poemDisplay.textContent = text;` }); });
第一行是说‘’获取位于url里的资源(fetch(url))
‘’和“然后当promise解析后运行指定的函数(.then(function() { ... }))
”。"解析"的意思是"在将来某一时刻完成指定的操作"。在本例中,指定的操作是从指定的URL(使用HTTP请求)获取资源,并返回对我们执行某些操作的响应。
实际上,传递给 then()
是一段不会立即执行的代码 — 而是当返回响应时代码会被运行。注意,你还可以选择把你的 promise 保存到一个变量里, 链接 .then()
在相同的位置。下面的代码会做相同的事情。
var myFetch = fetch(url);
myFetch.then(function(response) {
response.text().then(function(text) {
`poemDisplay.textContent = text;`
});
});
- 因为方法 fetch() 返回一个解析HTTP响应的promise, 你在 .then() 中定义的任何函数会被自动给与一个响应作为一个参数。你可以给这个参数取任何名字,以下的例子依然可以实现:(例子里把response参数叫做狗饼干---'dogBiscuits'=狗饼干)但是把参数叫做描述其内容的名字更有意义。
fetch(url).then(function(dogBiscuits) { dogBiscuits.text().then(function(text) { `poemDisplay.textContent = text;` }); }); function(response) {response.text().then(function(text) {poemDisplay.textContent = text;}); }
- response 对象有个
text()
方法, 获取响应主体中的原始数据a并把它转换成纯文本, 那时我们想要的格式。它也返回一个promise (解析结果文本字符串), 所以这里我们再使用.then()
, 在里面我们再定义一个操作文本字符串的函数。我们设置诗歌的<pre>
元素的 textContent 属性和这个文本字符串相同, 这样就非常简单地解决了。 - 值得注意的是你可以直接将promise块 (
.then()
块, 但也有其他类型) 链接到另一个的尾部, 顺着链条将每个块的结果传到下一个块。 这使得promises非常强大。 - 下面的代码块和我们原始的例子做的是相同的事, 但它是不同的写法:
fetch(url).then(function(response) {
return response.text()
}).then(function(text) {
poemDisplay.textContent = text;
});
- 很多开发者更喜欢这种样式, 因为它更扁平并且按理说对于更长的promise链它更容易读 — 每一个promise(承诺)接续上一个promise,而不是在上一个promise的里面(会使得整个代码笨重起来,难以理解)。以上两种写法还有一个不同的地方是我们在response.text() 语句之前得包含一个 return 语句, 用来把这一部分的结果传向promise链的下一段。
(4)你应该用哪种方法呢?
- 这完全取决于你正在干的项目是啥样。XHR已经面世非常之久,现在已经有了相当棒的跨浏览器支持。然而对于网页平台来说,Fetch和Promise是新近的产物,除了IE和Safari浏览器不支持,别的浏览器大多提供了支持。(现在Safari也即将为fetch和promise提供支持)。
- 如果你的项目需要支持年代久远的浏览器,那么使用XHR可能会更爽一些。如果你的项目比较激进而且你根本不管老版的浏览器吃不吃这套,那就选择Fetch吧老铁。
- 话说回来,咱倒真应该两者都学学——因为使用IE浏览器的人们在变少,Fetch会变得越来越流行(事实上IE已经没人管了,因为微软Edge浏览器的受宠),但在所有浏览器彻底支持Fetch之前,你可能还得和XHR纠缠一阵子。
四、第三方API
- 学习了解第三方API的运作方式
- 如何运用它们来提高网站性能
一、什么是第三方API?
- 第三方API是由第三方(通常是Facebook,Twitter或Google等公司)提供的API,允许您通过JavaScript访问其功能,并在您自己的站点上使用它
1、它们植根于第三方服务器
- 浏览器API在浏览器构建之初就存在 — 用JavaScript就可以立即访问它们。第三方API,从某种角度讲,是植根于第三方服务器上的。要通过 JavaScript获取它们,您首先需要链接到其功能接口上并使其在您的页面上生效。通常来说,这首先需要您通过一个 <script> 元素连接到第三方服务器所开放的JavaScript库。
<script type="text/javascript"
src="https://maps.google.com/maps/api/js?key=AIzaSyDDuGt0E5IEGkcE6ZfrKfUtE9Ko_de66pA"></script>
- 然后您便可使用该库中可用的对象了,如:
var latlng = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
var myOptions = {
zoom: 8,
center: latlng,
mapTypeId: google.maps.MapTypeId.TERRAIN,
disableDefaultUI: true
}
var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
- 代码中我们用 google.maps.LatLng() 构造器创建了一个新的 LatLng 对象,它包含了我们想展示的地址的纬度和经度,作为一个Geolocation API返回。然后,我们创建了包含这个对象,和其他有关地图显示信息的选项对象(myOptions) 。最后,用 google.maps.Map() 构造器创建了map对象,它接受网页元素(地图展示处)和选项对象两个参数。
- 以上就是用 Google Maps API 建立一个简单地图所需要的所有信息。所有复杂的工作都全由你所连接的第三方服务器处理,包括展示正确地理位置的地图块,等等。
2、权限的不同处理方式
- 浏览器api的安全性通常是通过权限提示来处理的。这样做的目的是为了让用户知道他们访问的网站上发生了什么,并且不太可能成为恶意使用API的人的受害者。
- 第三方API有一个稍微不同的权限系统——它们倾向于使用关键代码来允许开发人员访问API功能。如:谷歌地图API库的URL
https://maps.google.com/maps/api/js?key=AIzaSyDDuGt0E5IEGkcE6ZfrKfUtE9Ko_de66pA
- URL末尾提供的URL参数是一个开发人员密钥——应用程序的开发人员必须应用它来获取一个密钥,然后以一种特定的方式将其包含在代码中,以允许访问API的功能。对于谷歌映射(以及其他谷歌api),您可以在谷歌云平台上申请一个密钥。
五、绘图
一、网络图形
- 当浏览器开始支持 HTML 画布元素 和相关的 Canvas API(由苹果公司在 2004 年前后发明,后来其他的浏览器开始跟进) -大约在 2006 - 2007 年,Mozilla 开始测试 3D 画布。后来演化为 WebGL,它获得了各大浏览器厂商的认可,于是大约在 2009 - 2010 年间得到了标准化。WebGL 可以让你在 web 浏览器中生成真正的 3D 图形。
- 画布的基本功能有良好的跨浏览器支持。以下是例外:IE 8 及以下不支持 2D 画布,IE 11 及以下不支持WebGL。
二、开始使用 <canvas>
1、创建画布并确定尺寸
<canvas class="myCanvas">
<p>添加恰当的反馈信息。</p>
</canvas>
- 我们为
<canvas>
元素添加了一个 class,使得在网页中选择多个画布时会容易些。不明确指定宽高的画布,默认尺寸为 300 × 150 像素。
var canvas = document.querySelector('.myCanvas');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
- 如果现在保存文件,浏览器中什么也不会显示,这并没有问题,但是滚动条还是可见的,这就是问题了。原因是我们的“全窗尺寸画布”包含
<body>
元素的外边距(margin),使得文档比窗口略宽。 为使滚动条消失,需要删除<body>
元素的 margin 并将 overflow 设置为 hidden。在文档的<head>
中添加以下代码即可:
<style> body { `margin: 0;` `overflow: hidden;` } </style>
2、获取画布上下文(canvas context)并完成设置
- 我们需要获得一个对绘画区域的特殊的引用(称为“上下文”)来在画布上绘图。可通过 HTMLCanvasElement.getContext() 方法获得基础的绘画功能,需要提供一个字符串参数来表示所需上下文的类型
- 这里我们需要一个 2d 画布:
var ctx = canvas.getContext('2d');
- 注:可选上下文还包括 WebGL(webgl)、WebGL 2(webgl2)等等,但本文暂不涉及。
- 好啦,现在已经万事具备!ctx变量包含一个 CanvasRenderingContext2D 对象,画布上所有绘画操作都会涉及到这个对象。
- 开始前我们先初尝一下 canvas API。在 JS 代码中添加以下两行,将画布背景涂成黑色:
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, width, height);
- 这里我们使用画布的 fillStyle 属性(和CSS属性 色值 一致)设置填充色,然后使用 fillRect 方法绘制一个覆盖整个区域的矩形(前两个参数是矩形左上顶点的坐标,后两个参数是矩形的长宽,现在你知道 width 和 height 的作用了吧)。
三、2D 画布基础
- 所有绘画操作都离不开 CanvasRenderingContext2D 对象(这里叫做 ctx)。许多操作都需要提供坐标来指示绘图的确切位置。画布左上角的坐标是(0, 0),横坐标(x)轴向右延伸,纵坐标(y)轴向下延伸。
- 绘图操作可基于原始矩形模型实现,也可通过追踪一个特定路径后填充颜色实现。
a、简单矩形
- 1、画布模板:
<!DOCTYPE html> <html> <head> `<meta charset="utf-8">` `<title>Canvas</title>` `<style>` `body {` `margin: 0;` `overflow: hidden;` `}` `</style>` </head> <body> `<canvas class="myCanvas">` `<p>Add suitable fallback here.</p>` `</canvas>` `<script>` `var canvas = document.querySelector('.myCanvas');` `var width = canvas.width = window.innerWidth;` `var height = canvas.height = window.innerHeight;` `var ctx = canvas.getContext('2d');` `ctx.fillStyle = 'rgb(0,0,0)';` `ctx.fillRect(0,0,width,height);` `</script>` </body> </html>
- 2、在 JS 代码末尾添加下面两行:
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillRect(50, 50, 100, 150);
- 保存并刷新,画布上将出现一个红色的矩形。其左边和顶边与画布边缘距离均为 50 像素(由前两个参数指定),宽 100 像素、高 150 像素(由后两个参数指定)。
- 3、然后再添加一个绿色矩形。在 JS 代码末尾添加下面两行:
ctx.fillStyle = 'rgb(0, 255, 0)';
ctx.fillRect(75, 75, 100, 100);
- 这里引出了一个新问题:绘制矩形、线等操作按出现的顺序依次进行。就像粉刷墙面时,两层重叠时新层总会覆盖旧层。这一点是无法改变的,因此在绘制图形时一定要慎重考虑顺序问题。
- 4、还可以通过指定半透明的颜色来绘制半透明的图形,比如使用
rgba()
。 a 指定了“α 通道”的值,也就是颜色的透明度。值越高透明度越高,底层的内容就越清晰。ctx.fillStyle = 'rgba(255, 0, 255, 0.75)';
ctx.fillRect(25, 100, 175, 50);
b、描边(stroke)和线条宽度
- 你可以使用 strokeStyle 属性来设置描边颜色,使用 strokeRect 来绘制一个矩形的轮廓。
- 1、在上文的 JS 代码的末尾添加以下代码:
ctx.strokeStyle = 'rgb(255, 255, 255)';
ctx.strokeRect(25, 25, 175, 200);
- 2、默认的描边宽度是 1 像素,可以通过调整 lineWidth 属性(接受一个表示描边宽度像素值的数字)的值来修改。在上文两行后添加以下代码:
ctx.lineWidth = 5;
- 注意:
lineWidth
应在strokeRect
才会生效。
3、绘制路径
- 可以通过绘制路径来绘制比矩形更复杂的图形。路径中至少要包含钢笔运行精确路径的代码以确定图形的形状。画布提供了许多函数用来绘制直线、圆、贝塞尔曲线,等等。
- 一些通用的方法和属性将贯穿以下全部内容:
- beginPath():在钢笔当前所在位置开始绘制一条路径。在新的画布中,钢笔起始位置为 (0, 0)。
- moveTo():将钢笔移动至另一个坐标点,不记录、不留痕迹,只将钢笔“跳”至新位置。
- fill():通过为当前所绘制路径的区域填充颜色来绘制一个新的填充形状。
- stroke():通过为当前绘制路径的区域描边,来绘制一个只有边框的形状。
- 路径也可和矩形一样使用 lineWidth 和 fillStyle / strokeStyle 等功能。
- 以下是一个典型的简单路径绘制选项:
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.beginPath();
ctx.moveTo(50, 50);
// 绘制路径
ctx.fill();
(1)画线
- 在画布上绘制一个等边三角形。
- 1、首先,在代码底部添加下面的辅助函数。它可以将角度换算为弧度,在为 JavaScript 提供角度值时非常实用,JS 基本上只接受弧度值,而人类更习惯用角度值。
function degToRad(degrees) {
return degrees * Math.PI / 180;
};
- 2、在画布模板添加下面的内容。此处为我们为三角形设置了颜色,准备绘制,然后将钢笔移动至 (50, 50)(没有绘制任何内容)。然后准备在新的坐标开始绘制三角形。
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.beginPath();
ctx.moveTo(50, 50);
- 3、接下来在脚本中添加以下代码:
ctx.lineTo(150, 50);
var triHeight = 50 * Math.tan(degToRad(60));
ctx.lineTo(100, 50+triHeight);
ctx.lineTo(50, 50);
ctx.fill();
- 逐行解释:
- 首先绘制一条直线,终点坐标为 (150, 50)。此时路径沿 x 轴向右行走 100 像素。
- 用三角函数来计算等边三角形的高。我们要绘制的三角形是朝下的。等边三角形每个角均为 60°,为计算高的值,我们可以将三角形从正中心分割为两个直角三角形,每个直角三角形的三个角分别为 90°、60°、30°。对于边:
- 最长的边称为 斜边。
- 紧挨 60° 角的边称为 临边,显然地,它的长度是刚才绘制的线的一半,即 50 像素。
- 60° 角对面的边称为 对边,即三角形的高,需要计算得到。
- 通过基本三角函数可得:临边长度乘以角的正切等于对边长度。于是可得三角形的高为
50 * Math.tan(degToRad(60))
。由于Math.tan()
接受数值的单位为弧度,于是我们用刚才的degToRad()
函数将 60° 换算为弧度。
- 4、有了三角形的高,我们来绘制另一条线,终点坐标为
(100, 50+triHeight)
。X 坐标值很简单,应在刚才绘制的水平线两顶点正中间位置。Y 值应为 50 加上三角形的高,因为高即三角形底边到顶点的距离 - 5、下一条线的终点坐标为绘制整个三角形的起点坐标。
- 6、最后,运行
ctx.fill()
来终止路径,并为图形填充颜色。
(2)画圆
- 可在画布中绘制圆的方法—— arc() ,通过连续的点来绘制整个圆或者弧(arc,即局部的圆)。
- 1、在代码中添加以下几行,以向画布中添加一条弧。
ctx.fillStyle = 'rgb(0, 0, 255)';
ctx.beginPath();
ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);
ctx.fill();
arc()
函数有六个参数。前两个指定圆心的位置坐标,第三个是圆的半径,第四、五个是绘制弧的起、止角度(给定 0° 和 360° 便能绘制一个完整的圆),第六个是绘制方向(false 是顺时针,true 是逆时针)。- 注:0° 设定为水平向右。
- 2、我们再来画一条弧:
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true);
ctx.lineTo(200, 106);
ctx.fill();
- 模式基本一样,但有两点不同:
- 将 arc() 的最后一个参数设置为 true,意味着弧将逆时针绘制,也就意味着即使起、止角度分别设置为 -45°、45°,我们还是得到了区域外的一条 270° 的弧。如果把 true 改为false 重新运行,将得到 90° 的弧。
- 在调用 fill() 前,我们绘制了一条终点为圆心的直线。然后我们就渲染出一个惟妙惟肖的吃豆人模型。如果删除这条线(试试呗)再重新运行代码,你只能得到一个起止点间被砍掉一块的圆。这向我们展示了画布的另一个重要事项:如果要填充一个未完成(也就是没有首尾相接)的路径,浏览器将在起、止点件绘制一条直线,然后直接填充。
- learn more: 用画布绘图 入门课程
(3)文本
- 以下两个函数用于绘制文本:
- fillText() :绘制有填充色的文本。
- strokeText():绘制文本外边框(描边)
- 这两个函数有三个基本的参数:需要绘制的文字、文本框(顾名思义,围绕着需要绘制文字的方框)左上顶点的X、Y坐标。
- 还有一系列帮助控制文本渲染的属性:比如用于指定字体族、字号的 font,它的值和语法与 CSS 的 font 属性一致。
- 在 JS 代码底部添加以下内容:
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
ctx.font = '36px arial';
ctx.strokeText('Canvas text', 50, 50);
ctx.fillStyle = 'red';
ctx.font = '48px georgia';
ctx.fillText('Canvas text', 50, 150);
- 将绘制两行文字,一行描边文字一行填充颜色的文字
- 绘制文本 获得关于画布文本选项的更多信息
(4)在画布上绘制图片
- 可在画布上渲染外部图片,简单图片文件、视频帧、其他画布内容都可以。这里我们只考虑简单图片文件的情况:
- 1、 drawImage() 方法可将图片绘制在画布上。 最简单的版本需要三个参数:需要渲染的图片、图片左上角的X、Y坐标。
- 2、将图片源嵌入画布中,代码如下:
var image = new Image();
image.src = 'firefox.png';
- 这里使用 Image() 构造器创建了一个新的 HTMLImageElement对象。返回对象的类型与非空 元素的引用是一致的。然后将它的 src 属性设置为 Firefox 的图标。此时浏览器将开始载入这张图片。
- 3、这次我们尝试用
drawImage()
函数来嵌入图片,应确保图片先载入完毕,否则运行会出错。可以通过 onload 事件处理器来达成,该函数只在图片调用完毕后才会调用。在上文代码末尾添加以下内容:
image.onload = function() {
ctx.drawImage(image, 50, 50);
}
- 4、还有更多方式。如果仅需要显示图片的某一部分,或者需要改变尺寸,该怎么做呢?复杂版本的
drawImage()
可解决这两个问题。请更新ctx.drawImage()
一行代码为:
ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175);
- 第一个参数不变,为图片引用
- 参数 2、3 表示裁切部分左上顶点的坐标,参考原点为原图片本身左上角的坐标。原图片在该坐标左、上的部分均不会绘制出来。
- 参数 4、5 表示裁切部分的长、宽。
- 参数 6、7 表示裁切部分左上顶点在画布中的位置坐标,参考原点为画布左上顶点。
- 参数 8、9 表示裁切部分在画布中绘制的长、宽。本例中绘制时与裁切时面积相同,你也可以定制绘制的尺寸。
四、循环和动画
1、 创建一个循环
- 不学习动画你就无法体会画布的强大。画布是提供可编程图形的。
- 在画布中使用循环是件有趣的事,你可以在 for 循环中运行画布命令,和其他 JS 代码一样。
- 1.在画布模板js末尾创建新方法translate(),可用于移动画布的原点。
ctx.translate(width/2, height/2);
- 这会使原点 (0, 0) 从画布左上顶点移动至画布正中心。这个功能在许多场合非常实用,就像本示例,我们的绘制操作都是围绕着画布的中心点展开的。
- 2、在 JS 代码末尾添加以下内容:
function degToRad(degrees) {
return degrees * Math.PI / 180;
};
function rand(min, max) {
return Math.floor(Math.random() * (max-min+1)) + (min);
}
var length = 250;
var moveOffset = 20;
for(var i = 0; i < length; i++) {
}
- 这里我们实现了一个与上文三角形示例中相同的
degToRad()
函数、一个返回给定范围内随机数rand()
函数、length 和 moveOffset 变量(见下文),以及一个空的 for 循环。 - 4、此处的理念是利用 for 循环在画布上循环迭代绘制好玩儿的内容。请将以下代码添加进 for 循环中:
ctx.fillStyle = 'rgba(' + (255-length) + ', 0, ' + (255-length) + ', 0.9)';
ctx.beginPath();
ctx.moveTo(moveOffset, moveOffset);
ctx.lineTo(moveOffset+length, moveOffset);
var triHeight = length/2 * Math.tan(degToRad(60));
ctx.lineTo(moveOffset+(length/2), moveOffset+triHeight);
ctx.lineTo(moveOffset, moveOffset);
ctx.fill();
length--;
moveOffset += 0.7;
ctx.rotate(degToRad(5));
- 在每次迭代中:
- 设置 fillStyle 为略透明的紫色渐变色。渐变由每次迭代时 length 值的改变实现。随着循环的运行, length 值逐渐变小,从而使连续的三角形颜色逐渐变亮。
- 开始路径。
- 将钢笔移动至坐标 (moveOffset, moveOffset);该变量定义了每次要绘制新三角形时需要移动的距离。
- 画一条直线,终点坐标为 (moveOffset+length, moveOffset)。即一条长度为 length 与 X 轴平行的线。
- 计算三角形的高,方法同上。
- 向三角形底部顶点方向绘制一条直线,然后向三角形的起始点绘制一条直线。
- 调用
fill()
为三角形填充颜色。 - 更新次序变量,准备绘制下一个三角形。length 的值减一,使三角形每次迭代都变小一些;小幅增加 moveOffset 的值,使得下一个三角形略微错位;用一个新函数 rotate() 来旋转整块画布,在绘制下个三角形前画布旋转 5°。
2、动画
- 在重度画布应用(比如游戏或实时可视化)中恒定循环是至关重要的支持组件。如果期望画布显示的内容像一部电影,屏幕最好能够以 60 帧每秒的刷新率实时更新,这样人眼看到的动作才更真实、更平滑。
- 一些 JavaScript 函数可以让函数在一秒内重复运行多次,这里最适合的就是 window.requestAnimationFrame()。它只取一个参数,即每帧要运行的函数名。
- 下一次浏览器准备好更新屏幕时,将会调用你的函数。如果你的函数向动画中绘制了更新内容,则在函数结束前再次调用 requestAnimationFrame(),动画循环得以保留。
- 只有在停止调用 requestAnimationFrame() 时,或 requestAnimationFrame() 调用后、帧调用前调用了 window.cancelAnimationFrame() 时,循环才会停止。
- 注:动画结束后在主代码中调用 cancelAnimationFrame() 是良好习惯,可以确保不再有等待运行的更新。
- 浏览器自行处理诸如“使动画匀速运行”、“避免在不可见的内容浪费资源”等复杂细节问题。
- “弹球”示例。以下是让弹球持续运行的循环代码:
function loop() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillRect(0, 0, width, height);
while(balls.length < 25) {
`var ball = new Ball();`
`balls.push(ball);`
}
for(i = 0; i < balls.length; i++) {
`balls[i].draw();`
`balls[i].update();`
`balls[i].collisionDetect();`
}
requestAnimationFrame(loop);
}
loop();
- 我们在代码底部运行了一次
loop()
函数,它启动了整个循环,绘制了第一帧动画。接着loop()
函数接管了requestAnimationFrame(loop)
的调用工作,即运行下一帧、再下一帧……的动画。 - 请注意每一帧我们都整体清除画布并重新渲染所有内容。(每帧创建一个新球(25 个封顶),然后绘制每个球,更新它们的位置,检查是否撞到了其它球。)向画布中绘制的新图形不能像DOM 元素那样单独操作。你无法再画布中单独操作某一个球,因为只要绘制完毕了,它就是画布的一部分,而不是一个单独的球。你需要擦除再重画,可以将整帧擦除再重画整个画面,也可通过编程选择最小的部分进行擦除和重画。
- 一般地,在画布上制作动画需要以下步骤:
- 1、清除画布内容(可用 fillRect() 或 clearRect())。
- 2、(在需要时)用 save() 保存状态。(在进行下一步前保存所更新的设置,一般在复杂环境中用到)
- 3、绘制动画图形。
- 4、使用 restore() 恢复第 2 步中保存的状态。
- 5、调用 requestAnimationFrame() 准备下一帧动画。
- 注:save() 和 restore() 这里暂不展开,可以访问 变形 教程(及后续内容)来获取详细信息。
3、一个简单的人物动画
- 1、画布模板,下载 walk-right.png 并放在同一文件夹。
- 2、在 JS 代码末尾添加下面一行,再次将画布的原点设置为中心点。
ctx.translate(width/2, height/2);
- 3、创建一个新的 HTMLImageElement 对象,把它的 src 设置为所需图片,添加一个 onload 事件处理器,使 draw() 函数在图片载入后触发。
var image = new Image();
image.src = 'walk-right.png';
image.onload = draw;
- 4、添加一些变量,来追踪精灵图在屏幕上的位置,以及当前需要显示的精灵图的序号。
var sprite = 0;
var posX = 0;
- 我们来解释一下“精灵图序列。图中包含六个精灵,它们组成了一趟完整的行走序列。每个精灵的尺寸为 102 × 148 像素。为了整齐的显示一个精灵,可以通过 drawImage() 来从序列中裁切出单独的精灵并隐藏其他部分,就像上文中操作 Firefox 图标的方法。切片的 X 坐标应为 102 的倍数,Y 坐标恒为 0。切片尺寸恒为 102 × 148 像素。
- 5、在代码末尾添加一个空的 draw() 函数,用来添加一些代码:
function draw() {
};
- 6、本节剩余部分都在这个 draw() 中展开。首先,添加以下代码,清除画布,准备绘制新的帧。注意由于我们刚才将原点设置为 width/2, height/2,这里需要将矩形左上顶点的坐标设置为 -(width/2), -(height/2)。
ctx.fillRect(-(width/2), -(height/2), width, height);
- 7、下一步,我们使用 drawImage()(9参数版本)来绘制图形,添加以下代码:
ctx.drawImage(image, (sprite*102), 0, 102, 148, 0+posX, -74, 102, 148);
- image 指定需要嵌入的图片。
- 参数 2、3 指定切片左上顶点在原图的位置坐标,X 值为 sprite(精灵序列 0 - 5)乘 102,Y 值恒为 0。
- 参数 4、5 指定切片尺寸:102 × 148 像素。
- 参数 6、7 指定切片在画布绘制区域的坐上顶点坐标。X 坐标位置为 0 + posX,意味着我们可以通过修改 posX 的值来修改绘制的位置。
- 参数 8、9 指定图片在画布中的尺寸。这里需要图片保持原始尺寸,因此我们指定宽、高值为 102、148。
- 8、现在,我们在每帧绘制完毕(部分完毕)后修改 sprite 的值。在
draw()
函数底部添加以下内容:
if (posX % 13 === 0) {
`if (sprite === 5) {`
`sprite = 0;`
`} else {`
`sprite++;`
`}`
}
- 将整个功能块放置在 if (posX % 13 === 0) { ... } 内。用“模(%)运算符”(即 求余运算符)来检测 posX 是否可以被 13 整除。如果整除,则通过增加 sprite 的值转至下一个精灵(到 5 号精灵时归零)。这实际上意味着每隔 13 帧才更新一次精灵,每秒大约更新 5 帧(requestAnimationFrame() 每秒最多调用 60 帧)。我们故意放慢了帧率,因为精灵图只有六个,且如果每秒显示 60 帧的话,这个角色就会快到起飞。
- 外部程序块中用一个 if...else 语句来检测 sprite 的值是否为 5(精灵序号在 0 - 5 间循环,因此 5 代表最后一个精灵)。 如果最后一个精灵已经显示,就把 sprite 重置为 0,否则加 1。
- 9、下一步要算出每帧 posX 的值,在上文代码末尾添加以下内容:
if(posX > width/2) {
`newStartPos = -((width/2) + 102);`
`posX = Math.ceil(newStartPos / 13) * 13;`
`console.log(posX);`
} else {
`posX += 2;`
}
- 用另一个 if ... else 来检测 posX 的值是否超出了 width/2,那意味着角色走到了屏幕右侧边缘。如果这样就计算出一个让角色出现在屏幕左侧边缘的 X 坐标,然后将 posX 设置为最接近这个数的 13 的倍数。这里必须限定 13 的倍数这个条件,这是因为 posX 不可能是 13 的倍数,若不限定的话上一段代码就不会运行了。
- 如果角色没有走到屏幕边缘,只需为 posX 加 2。这将让他在下次绘制时更靠右些。
- 10、最后,通过在
draw()
函数末尾添加 requestAnimationFrame() 调用以实现动画的循环。
window.requestAnimationFrame(draw);
4、简单的绘图应用
- 动画循环与用户输入(本例中为鼠标移动)结合起来
- 来看看代码的精华部分:先,用 curX、curY 和 pressed 这三个变量来跟踪鼠标的 X、Y 坐标和点击状态。当鼠标移动时,触发一个函数作为 onmousemove 事件处理器,其应捕获当前的 X 和 Y 值。再用 onmousedown 和 onmouseup 事件处理器来修改鼠标键按下时 pressed 的值(按下为 true,释放为 false)。
var curX;
var curY;
var pressed = false;
document.onmousemove = function(e) {
curX = (window.Event) ? e.pageX : e.clientX +
(document.documentElement.scrollLeft ?
document.documentElement.scrollLeft : document.body.scrollLeft);
curY = (window.Event) ? e.pageY : e.clientY +
(document.documentElement.scrollTop ? document.documentElement.scrollTop
: document.body.scrollTop);
}
canvas.onmousedown = function() {
pressed = true;
};
canvas.onmouseup = function() {
pressed = false;
}
- 在按下“Clear canvas”(清除画布)按钮时,我们运行一个简单的函数来清除整个画布的内容至纯黑色,和刚才的方法一致:
clearBtn.onclick = function() {
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, width, height);
}
- 这次的绘图循环非常简单,如果 pressed 为 true,则绘制一个圆,该圆以颜色选择器中设定的颜色为背景,以滑动选择器设定的数值为半径。
function draw() {
if(pressed) {
`ctx.fillStyle = colorPicker.value;`
`ctx.beginPath();`
`ctx.arc(curX, curY-85, sizePicker.value, degToRad(0), degToRad(360), false);`
`ctx.fill();`
}
requestAnimationFrame(draw);
}
draw();
- 注:range 和 color 两个
<input>
的类型有良好的跨浏览器支持,但 Safari 暂不支持 color,IE 10 以下版本两者均不支持。如果你的浏览器不支持这些输入类型,它们将降格为简单文字输入区域,可以直接输入合法的数字来表示半径和颜色的值。
五、WebGL
- 2D 内容告一段落,现在简单了解一下 3D 画布。3D 画布内容可通过的 WebGL API 实现,尽管它和 2D canvas API 都可在 元素上进行渲染,但两者是彼此独立的。
- WebGL 基于 OpenGL 图形编程语言实现,可直接与 GPU 通信,基于此,编写纯 WebGL 代码与常规的 JavaScript 不尽相同,更像 C++ 那样的底层语言,更加复杂,但无比强大。
1、使用库
- 由于 3D 绘图的复杂性,大多数人写代码时会使用第三方 JavaScript 库(比如 Three.js、PlayCanvas 或 Babylon.js)。大多数库的原理都基本类似,提供创建基本的、自定义性状的功能、视图定位摄影和光效、表面纹理覆盖,等等。库负责 与 WebGL 通信,你只需完成更高阶工作。
- 接触任何一个库都意味着要学一套全新的API(这里是第三方的版本),但与纯 WebGL 编程都大同小异。
2、创建魔方
- 看一个简单的示例,用一套 WebGL 库(这里我们选择 Three.js,最流行的 3D 绘图库之一)来创建我们在本文开头看到的旋转魔方。
- 1、首先,下载 index.html、metal003.png 并保存在同一个文件夹。图片将用于魔方的表面纹理。
- 2、在 main.js 中添加新的代码
var scene = new THREE.Scene();
- Scene() 构造器创建一个新的场景,表示即将显示的整个 3D 世界。
- 3、下一步,我们需要一部摄影机来看到整个场景。在 3D 绘图语境中,摄影机表示观察者在世界里的位置,可通过下面代码创建一部摄影机:
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
- PerspectiveCamera() 构造器有四个参数:
- 观察区域:镜头视角大小,用角度表示。
- 屏幕宽高比:一般情况下,宽高比等于屏幕的宽比上屏幕的高。使用其他的值会使场景扭曲(也许正是你需要的,但一般都不是)。
- 近裁切面:停止渲染前对象离摄影机的最近距离。设想一下,举起一个手指,逐渐移近双眼,某个点后就在也看不到这根手指了。
- 裁切面:停止渲染前离摄像机最远的对象的距离。
- 将摄像机的位置设定为距 Z 轴 5 个距离单位的位置。与 CSS 类似,在屏幕之外你(观察者)的位置。
- 4、第三个重要参数是渲染器。我们用它来渲染给定的场景,可通过给定位值得摄影机观察。现在我们使用 WebGLRenderer() 构造器创建一个渲染器供稍后使用。添加以下代码:
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
- 第一行创建一个新的渲染器,第二行设定渲染器在当前摄影机视角下的尺寸,第三行将渲染好的 对象加入HTML的 中。现在渲染器绘制的内容将在窗口中显示出来。
- 5、下一步,在画布中创建魔方。把以下代码添加到 JS 文件中:
var cube;
var loader = new THREE.TextureLoader();
loader.load( 'metal003.png', function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2);
var geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
var material = new THREE.MeshLambertMaterial( { map: texture, shading: THREE.FlatShading } );
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
draw();
});
- 首先,创建一个全局变量 cube,这样就可以在代码任意位置访问我们的魔方。
- 然后,创建一个 TextureLoader 对象,并调用
load()
。 这里load()
包含两个参数(其它情况可以有更多参数):需要调用的纹理图(PNG 文件)和纹理加载成功后调用的函数。 - 函数内部,我们用 texture 对象的属性指明我们要在魔方的每个面渲染 2 × 2 的图片,然后创建一个 BoxGeometry 对象和一个 MeshLambertMaterial 对象,将两者作为 Mesh 的参数来创建我们的魔方。 Mesh 一般就需要两个参数:一个几何(形状)和一个素材(形状表面外观)。
- 最后,将魔方添加进场景中,调用我们的 draw() 函数开始动画。
- 6、定义
draw()
函数前,我们需要先为场景打光,以照亮场景中的物体。请添加以下代码:
var light = new THREE.AmbientLight('rgb(255, 255, 255)'); // soft white light
scene.add(light);
var spotLight = new THREE.SpotLight('rgb(255, 255, 255)');
spotLight.position.set( 100, 1000, 1000 );
spotLight.castShadow = true;
scene.add(spotLight);
- AmbientLight 对象是可以轻度照亮整个场景的柔光,就像户外的阳光。而 SpotLight 对象是直射的硬光,就像闪光灯和手电筒(或者它的英文字面意思——聚光灯)。
- 7、最后,在代码末尾添加我们的
draw()
函数:
function draw() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(draw);
}
- 这段代码很直观,每一帧我们都沿 X 轴 和 Y 轴将魔方轻微转动,然后按摄像机视角渲染场景,最后调用 requestAnimationFrame() 来准备下一帧。
- 你可以 到 Github 下载最终代码。
五、另阅
- 这里只涉及到画布最为基本的内容,以下内容帮你探索更多:
- Canvas 教程Canvas 教程:一个详尽的教程系列,更细致深入地讲解了 2D 画布所需的知识。必读。
- WebGL 教程:纯 WebGL 编程教程系列。
- 用 Three.js 创建一个简单的示例:Three.js 基础教程。我们还提供 PlayCanvas 和 Babylon.js 的基础教程。
- 游戏开发:MDN web 游戏开发目录页。提供与 2D、3D画布相关的实用教程和技术,可参考“技术”和“教程”菜单项。
六、示例
- Violent theramin:用 Web 音频 API 创建声音,用画布显示漂亮的视觉效果以配合音乐。
- Voice change-o-matic:用画布为 Web 音频 API 产生的音效提供实时的视觉效果。
六、视频和音频 API
- 学习如何通过浏览器API来控制视频和音频的播放。
1、HTML5视频和音频
- 和元素允许我们把视频和音频嵌入到网页当中。就像我们在音频和视频内容文中展示的一样,一个典型的实现如下所示:
<video controls>
<source src="rabbit320.mp4" type="video/mp4">
<source src="rabbit320.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is a
<a href="rabbit320.mp4">link to the video</a>
instead.</p>
</video>
- controls属性,它会启用默认的播放设置。如果没有指定该属性,则播放器中不会显示相关控件:
- 使用原生浏览器控件的一个很大的问题在于,它们在各个浏览器中都不相同 — 对于跨浏览器的支持并不是很好!另一个问题是,在大多数浏览器中原生控件难以通过键盘来操作。
- 你可以通过隐藏本地控件(通过删除controls属性),然后使用HTML,CSS和JavaScript编写自己的代码来解决这两个问题。 在下一节中,我们将看到如何通过一些可用的工具来实现。
2、HTMLMediaElement API
- 作为HTML5规范的一部分,HTMLMediaElement API提供允许你以编程方式来控制视频和音频播放的功能—例如 HTMLMediaElement.play(), HTMLMediaElement.pause(),等。该接口对和两个元素都是可用的,因为在这两个元素中要实现的功能几乎是相同的。让我们通过一个例子来一步步演示一些功能。
html
- 打开HTML.index文件。你将看到一些功能;HTML由视频播放器和它的控件所控制:
<div class="player">
<video controls>
`<source src="video/sintel-short.mp4" type="video/mp4">`
`<source src="video/sintel-short.mp4" type="video/webm">`
`<!-- fallback content here -->`
</video>
<div class="controls">
`<button class="play" data-icon="P" aria-label="play pause toggle"></button>`
`<button class="stop" data-icon="S" aria-label="stop"></button>`
`<div class="timer">`
`<div></div>`
`<span aria-label="timer">00:00</span>`
`</div>`
`<button class="rwd" data-icon="B" aria-label="rewind"></button>`
`<button class="fwd" data-icon="F" aria-label="fast forward"></button>`
</div>
</div>
- 播放器被嵌入在元素之中,所以如果有需要的话,可以把它作为一个单元整体来设置其样式。
<video>
元素层包含两个<source>
元素,这样可以根据浏览器来加载其所支持的视频格式。- 四个
<button>
— play/pause, stop, rewind, and fast forward. - 每个
<button>
都有一个class, 一个data-icon
属性来决定在每个按钮上显示什么和一个aria-label
属性为每一个按钮提供容易理解的描述, 即使我们没有在tags内提供可读的标签。当用户关注这些元素时含有aria-label
属性的内容也会被读出来。 - 有一个设定的计时器/进度条
<div>
用来记录已经播放的时长。为了展示效果, 提供了两个记录的装置 — 一个<span>
包含了分钟和秒,和一个额外的<div>
用来创建一个垂直的随着时间增加而增长的进度条。完成版本看上去是这样的, 点击查看完成版本.
- 播放器被嵌入在
css
.controls
的样式
.controls {
visibility: hidden;
opacity: 0.5;
width: 400px;
border-radius: 10px;
position: absolute;
bottom: 20px;
left: 50%;
margin-left: -200px;
background-color: black;
box-shadow: 3px 3px 5px black;
transition: 1s all;
display: flex;
}
.player:hover .controls, player:focus .controls {
opacity: 1;
}
- 我们从设置为hidden的自定义控件visibility开始。稍后在我们的JavaScript中, 我们将控件设置为 visible, 并且从
<video>
元素中移除controls 属性。这是因为, 如果JavaScript由于某种原因没有加载, 用户依然可以使用原生的控件播放视频。 - 默认情况下,我们将控件的opacity设置为0.5 opacity,这样当您尝试观看视频时,它们就不会分散注意力。 只有当您将鼠标悬停/聚焦在播放器上时,控件才会完全不透明。
- 我们使用Flexbox(display: flex)布置控制栏内的按钮,以简化操作。
- 按钮图标
@font-face {
font-family: 'HeydingsControlsRegular';
src: url('fonts/heydings_controls-webfont.eot');
src: url('fonts/heydings_controls-webfont.eot?#iefix') format('embedded-opentype'),
`url('fonts/heydings_controls-webfont.woff') format('woff'),`
`url('fonts/heydings_controls-webfont.ttf') format('truetype');`
font-weight: normal;
font-style: normal;
}
button:before {
font-family: HeydingsControlsRegular;
font-size: 20px;
position: relative;
**content: attr(data-icon);**
color: #aaa;
text-shadow: 1px 1px 0px black;
}
- 用
::before
选择器在每个<button>
元素之前显示内容 - 使用 content 属性将各情况下要显示的内容设置为
data-icon
属性的内容。例如在播放按钮的情况下,data-icon
包含大写的“P”。 - 使用
font-family
将自定义Web字体应用于我们的按钮上。在该字体中“P”对应的是“播放”图标,因此播放按钮上显示“播放”图标。 - 图标字体:** 减少HTTP请求**,因为您不需要将这些图标作为图像文件下载。同时具有出色的可扩展性,以及您可以使用文本属性来设置它们的样式 —— 例如 color 和 text-shadow。
- 进度条的 CSS
.timer {
line-height: 38px;
font-size: 10px;
font-family: monospace;
text-shadow: 1px 1px 0px black;
color: white;
flex: 5;
position: relative;
}
.timer div {
position: absolute;
background-color: rgba(255,255,255,0.2);
left: 0;
top: 0;
width: 0;
height: 38px;
z-index: 2;
}
.timer span {
position: absolute;
z-index: 3;
left: 19px;
}
- 我们将外部 .timer
<div>
设为flex:5,这样它占据了控件栏的大部分宽度。 我们还设置 position: relative,这样我们就可以根据它的边界方便地定位元素,而不是<body>
元素的边界。 - 内部
<div>
position:absolute 绝对定位于外部<div>
的顶部。 它的初始宽度为0,因此根本无法看到它。随着视频的播放,JavaScript将动态的增加其宽度。 <span>
也绝对位于计时器/进度条 timer 栏的左侧附近。- 我们还对内部
<div>
和<span>
定义适当数值的 z-index ,以便进度条显示在最上层,内部<div>
显示在下层。 这样,我们确保我们可以看到所有信息 —— 一个box不会遮挡另一个。
js
- 1、在与 index.html 文件相同的目录下创建新的JavaScript文件。命名为 custom-player.js。
- 2、在此文件的顶部,插入以下代码:
var media = document.querySelector('video');
var controls = document.querySelector('.controls');
var play = document.querySelector('.play');
var stop = document.querySelector('.stop');
var rwd = document.querySelector('.rwd');
var fwd = document.querySelector('.fwd');
var timerWrapper = document.querySelector('.timer');
var timer = document.querySelector('.timer span');
var timerBar = document.querySelector('.timer div');
- 3、接下来,在代码的底部插入以下内容:
media.removeAttribute('controls');
controls.style.visibility = 'visible';
- 这两行从视频中删除默认浏览器控件,并使自定义控件可见。
播放和暂停视频
- 1、播放视频:当按钮被点击就触发 playPauseMedia 函数
play.addEventListener('click', playPauseMedia);
function playPauseMedia() {
if(media.paused) {
`play.setAttribute('data-icon','u');`
`media.play();`
} else {
`play.setAttribute('data-icon','P');`
`media.pause();`
}
}
- 2、停止视频
stop.addEventListener('click', stopMedia);
media.addEventListener('ended', stopMedia);
function stopMedia() {
media.pause();
media.currentTime = 0;
play.setAttribute('data-icon','P');
}
- 在HTMLMediaElement API里是没有stop()的,只有pause()
- 3、前进和后退
rwd.addEventListener('click', mediaBackward); fwd.addEventListener('click', mediaForward); var intervalFwd; var intervalRwd; function mediaBackward() { clearInterval(intervalFwd); fwd.classList.remove('active'); if (rwd.classList.contains('active')) {`rwd.classList.remove('active');``clearInterval(intervalRwd);``media.play();` } else {`rwd.classList.add('active');``media.pause();``intervalRwd = setInterval(windBackward, 200);` } } function mediaForward() { clearInterval(intervalRwd); rwd.classList.remove('active'); if (fwd.classList.contains('active')) {`fwd.classList.remove('active');``clearInterval(intervalFwd);``media.play();` } else {`fwd.classList.add('active');``media.pause();``intervalFwd = setInterval(windForward, 200);` } } function windBackward() { if (media.currentTime <= 3) { rwd.classList.remove('active'); clearInterval(intervalRwd); stopMedia(); } else { media.currentTime -= 3; } } function windForward() { if (media.currentTime >= media.duration - 3) {`fwd.classList.remove('active');``clearInterval(intervalFwd);``stopMedia();` } else {`media.currentTime += 3;` } }
更新过去的时间
media.addEventListener('timeupdate', setTime); function setTime() { var minutes = Math.floor(media.currentTime / 60); var seconds = Math.floor(media.currentTime - minutes * 60); var minuteValue; var secondValue; if (minutes < 10) { `minuteValue = '0' + minutes;` } else { `minuteValue = minutes;` } if (seconds < 10) { `secondValue = '0' + seconds;` } else { `secondValue = seconds;` } var mediaTime = minuteValue + ':' + secondValue; timer.textContent = mediaTime; var barLength = timerWrapper.clientWidth * (media.currentTime/media.duration); timerBar.style.width = barLength + 'px'; }
调整播放跟停止
加入stopMedia()、playPauseMedia()
rwd.classList.remove('active');
fwd.classList.remove('active');
clearInterval(intervalRwd);
clearInterval(intervalFwd);
总结:
document.onclick = function(e) {
console.log(e.x) + ',' + console.log(e.y)
}
另见:
- HTMLMediaElement
- Video and audio content
- Audio and video delivery — detailed guide to delivering media inside the browser, with many tips, tricks, and links to further more advanced tutorials.
- Audio and video manipulation — detailed guide to manipulating audio and video, e.g. with Canvas API, Web Audio API, and more.
- and audio reference pages.
- Media formats supported by the HTML audio and video elements.
七、客户端存储
- 如何使用客户端存储 API 来存储应用数据
- 由JavaScript APIs组成,允许你在客户端存储数据(比如在用户的机器上),而且可以在需要的时候重新取得需要的数据。好处:
- 个性化网站偏好(比如显示一个用户选择的窗口小部件,颜色主题,或者字体)。
- 保存之前的站点行为 (比如从先前的session中获取购物车中的内容, 记住用户是否之前已经登陆过)。
- 本地化保存数据和静态资源可以使一个站点更快(至少让资源变少)的下载, 甚至可以在网络失去链接的时候变得暂时可用。
- 保存web已经生产的文档可以在离线状态下访问。
- 经常客户端和服务端存储是结合在一起使用的。例如,你可以从数据库中下载一个由网络游戏或音乐播放器应用程序使用的音乐文件,将它们存储在客户端数据库中,并按需要播放它们。用户只需下载音乐文件一次——在随后的访问中,它们将从数据库中检索。
- 客户端存储 API 可以存储的数据量是有限的(可能是每个API单独的和累积的总量);具体的数量限制取决于浏览器,也可能基于用户设置。参考浏览器存储限制和清理标准
1、传统方法:cookies
- 早期的网络时代开始,网站就使用 cookies 来存储信息,以在网站上提供个性化的用户体验。它们是网络上最早最常用的客户端存储形式。
- 因为在那个年代,有许多问题——无论是从技术上的还是用户体验的角度——都是困扰着 cookies 的问题。这些问题非常重要,以至于当第一次访问一个网站时,欧洲居民会收到消息,告诉他们是否会使用 cookies 来存储关于他们的数据,而这是由一项被称为欧盟 Cookie 条例的欧盟法律导致的。
- 由于这些原因,我们不会在本文中教你如何使用cookie。毕竟它过时、存在各种安全问题,而且无法存储复杂数据,而且有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。
- cookie的唯一优势是它们得到了非常旧的浏览器的支持,所以如果您的项目需要支持已经过时的浏览器(比如 Internet Explorer 8 或更早的浏览器),cookie可能仍然有用,但是对于大多数项目来说,您不需要再使用它们了。
- 为什么仍然有新创建的站点使用 cookies?这主要是因为开发人员的习惯,使用了仍然使用cookies的旧库,以及存在许多web站点,提供了过时的参考和培训材料来学习如何存储数据。
2、新流派:Web Storage 和 IndexedDB
- 现代浏览器有比使用 cookies 更简单、更有效的存储客户端数据的 API。
- Web Storage API 提供了一种非常简单的语法,用于存储和检索较小的、由名称和相应值组成的数据项。当您只需要存储一些简单的数据时,比如用户的名字,用户是否登录,屏幕背景使用了什么颜色等等,这是非常有用的。
- IndexedDB_API 为浏览器提供了一个完整的数据库系统来存储复杂的数据。这可以用于存储从完整的用户记录到甚至是复杂的数据类型,如音频或视频文件。
3、未来:Cache API
- 一些现代浏览器支持新的 Cache API
- 这个API是为存储特定HTTP请求的响应文件而设计的,它对于像存储离线网站文件这样的事情非常有用,这样网站就可以在没有网络连接的情况下使用。
- 缓存通常与 Service Worker API 组合使用,尽管不一定非要这么做。
- 简单的例子:离线文件存储
4、存储简单数据 — web storage
- Web Storage API 非常容易使用 — 你只需存储简单的 键名/键值 对数据 (限制为字符串、数字等类型) 并在需要的时候检索其值。
(1)基本语法
a、访问 GitHub 上的 web storage blank template b、打开你浏览器开发者工具的 JavaScript 控制台、 c、所有的 web storage 数据都包含在浏览器内两个类似于对象的结构中: sessionStorage 和 localStorage。
- 一种方法,只要浏览器开着,数据就会一直保存 (关闭浏览器时数据会丢失) ,而第二种会一直保存数据,甚至到浏览器关闭又开启后也是这样。我们将在本文中使用第二种方法,因为它通常更有用。 d、Storage.setItem() 方法允许您在存储中保存一个数据项——它接受两个参数:数据项的名字及其值。试着把它输入到你的JavaScript控制台
localStorage.setItem('name','Chris');
e、Storage.getItem() 方法接受一个参数——你想要检索的数据项的名称——并返回数据项的值。
var myName = localStorage.getItem('name');
myName
- 在输入第二行时,您应该会看到 myName变量现在包含name数据项的值。
f、Storage.removeItem() 方法接受一个参数——你想要删除的数据项的名称——并从 web storage 中删除该数据项。
localStorage.removeItem('name');
var myName = localStorage.getItem('name');
myName
- 第三行现在应该返回 null — name 项已经不存在于 web storage 中。
(2)数据会一直存在
- web storage 的一个关键特性是,数据在不同页面加载时都存在(甚至是当浏览器关闭后,对localStorage的而言)
- 在不同的浏览器中再次打开Web Storage 空白模板
localStorage.setItem('name','Chris');
var myName = localStorage.getItem('name');
myName
- 你应该看到 name 数据项返回
- 现在关掉浏览器再把它打开
var myName = localStorage.getItem('name');
myName
你应该看到,尽管浏览器已经关闭,然后再次打开,但仍然可以使用该值。
(3)为每个域名分离储存
- 每个域都有一个单独的数据存储区(每个单独的网址都在浏览器中加载). 你 会看到,如果你加载两个网站(例如google.com和amazon.com)并尝试将某个项目存储在一个网站上,该数据项将无法从另一个网站获取。
- 这是有道理的 - 你可以想象如果网站能够查看彼此的数据,就会出现安全问题!
(4)更复杂的例子
示例将允许你输入一个名称,然后该页面将刷新,以提供个性化问候。这种状态也会页面/浏览器重新加载期间保持,因为这个名称存储在Web Storage 中。
示例文件:personal-greeting.html 这包含一个具有标题,内容和页脚,以及用于输入您的姓名的表单的简单网站。
a、首先创建对所有需要在此示例中操作的HTML功能的引用 - 我们将它们全部创建为常量,因为这些引用在应用程序的生命周期中不需要更改。
// 创建所需的常量 const rememberDiv = document.querySelector('.remember'); const forgetDiv = document.querySelector('.forget'); const form = document.querySelector('form'); const nameInput = document.querySelector('#entername'); const submitBtn = document.querySelector('#submitname'); const forgetBtn = document.querySelector('#forgetname'); const h1 = document.querySelector('h1'); const personalGreeting = document.querySelector('.personal-greeting');
接下来,我们需要包含一个小小的事件监听器,以在按下提交按钮时阻止实际的提交表单动作自身,因为这不是我们想要的行为。
// 当按钮按下时阻止表单提交 form.addEventListener('submit', function(e) { e.preventDefault(); });
现在我们需要添加一个事件监听器,当单击“Say hello”按钮时,它的处理函数将会运行。这些注释详细解释了每一处都做了什么,但实际上我们在这里获取用户输入到文本输入框中的名字并使用setItem()
将它保存在网络存储中,然后运行一个名为nameDisplayCheck()
的函数来处理实际的网站文本的更新。
// run function when the 'Say hello' button is clicked submitBtn.addEventListener('click', function() { // store the entered name in web storage localStorage.setItem('name', nameInput.value); // run nameDisplayCheck() to sort out displaying the // personalized greetings and updating the form display nameDisplayCheck(); });
此时,我们还需要一个事件处理程序。以便在单击“Forget”按钮时运行一个函数——且仅在单击“Say hello”按钮(两种表单状态来回切换)后才显示。在这个功能中,我们使用removeItem()
从网络存储中删除项目name,然后再次运行nameDisplayCheck()
以更新显示。
// run function when the 'Forget' button is clicked forgetBtn.addEventListener('click', function() { // Remove the stored name from web storage localStorage.removeItem('name'); // run nameDisplayCheck() to sort out displaying the // generic greeting again and updating the form display nameDisplayCheck(); });
nameDisplayCheck()
函数:在这里,我们通过使用localStorage.getItem('name')
作为测试条件来检查name 数据项是否已经存储在Web Storage 中。如果它已被存储,则该调用的返回值为true; 如果没有,它会是false。如果是true,我们会显示个性化问候语,显示表格的“forget”部分,并隐藏表格的“Say hello”部分。如果是false,我们会显示一个通用问候语,并做相反的事。
// define the nameDisplayCheck() function function nameDisplayCheck() { // check whether the 'name' data item is stored in web Storage if (localStorage.getItem('name')) { ` // If it is, display personalized greeting` `let name = localStorage.getItem('name');` `h1.textContent = 'Welcome, ' + name;` `personalGreeting.textContent = 'Welcome to our website, ' + name + '! We hope you have fun while you are here.';` ` // hide the 'remember' part of the form and show the 'forget' part` `forgetDiv.style.display = 'block';` `rememberDiv.style.display = 'none';` } else { ` // if not, display generic greeting` `h1.textContent = 'Welcome to our website ';` `personalGreeting.textContent = 'Welcome to our website. We hope you have fun while you are here.';` ` // hide the 'forget' part of the form and show the 'remember' part` `forgetDiv.style.display = 'none';` `rememberDiv.style.display = 'block';` } }
最后:我们需要在每次加载页面时运行nameDisplayCheck()函数。如果我们不这样做,那么个性化问候不会在页面重新加载后保持。
document.body.onload = nameDisplayCheck;
注意: 在完成版本的源代码中, <script src="index.js" **defer**></script>
一行里, defer 属性指明在页面加载完成之前,<script>元素的内容不会执行。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论