【javascript】谈谈HTML5: Web-Worker、canvas、indexedDB、拖拽事件
前言:作为一名Web开发者,可能你并没有对这个“H5”这个字眼投入太多的关注,但实际上它早已不知不觉进入到你的开发中,并且总有一天会让你不得不正视它,了解它并运用它
打个比方:《海贼王》中的主角路飞在“顶上战争两年前”,会在一些危急关头“不经意”地使用霸王色霸气,但对”霸气“的结构体系和具体运用都不太了解,这让他在香波地群岛等诸多重大战役中大吃苦头。此后, 他不惜花费两年时间跟随雷利修炼霸气。因为,如果不去了解这个崭新的战斗方法的话,他们在残酷的新世界一天也生存不了。
为什么学习HTML5?
咳咳, 回到主题,为什么我们要学HTML5呢?
1.了解HTML5的囊括范围的一大好处是:当你不小心使用了一个H5的东东的时候(例如你试图通过百度找到的答案解决一个紧张的需求),你会很及时的关注它的兼容性
2.H5有些新增的特性也许你从没接触过,也感觉无需用到它。但就在不久的将来,你可能就会用到,甚至依赖于它(毕竟这就是HTML的未来)
H5中的知识点分布
在下面, 我将学习H5中的知识点分成两类:主要知识点和针对特定功能的知识点,其中对主要知识点的部分,从学习成本的角度对其进行了难度分级
(仅属个人观点!如有改进意见,欢迎讨论)
一.主要知识点
(从需求层面上来说,普及范围相对较广)
相对容易的部分:
1.在线和离线事件(Online/Offline) (相对容易)
2. 众多的新增元素 如<output>, <progress>等 (相对容易)
3. history关于历史状态管理的API (相对容易)
4 Storage(localStorage和sessionStorage) (相对容易)
相对较难的部分:
5. Web Worker (相对较难)
6. canvas (相对较难)
7. indexedDB (相对较难)
8. 拖放操作 (相对较难)
9. Web Sockets (相对较难)
二. 针对特定功能的知识点
(对需求来说,主要针对某一方面的特殊需求场景)
1. 对音视频的支持
2. Camera API (操作摄像头)
3. WebGL (3D图像)
4. 地理位置定位 (geolocation对象)
本文主要讲述H5中主要知识点中,学习成本相对较高的四个点(仅个人观点):
一.Web Worker
二.canvas
三.indexedDB
四.拖放操作
【注意】因为下面介绍的H5的特性在一些比较老的浏览器里可能遇到兼容性问题,所以你在使用前必须要能力检测,例如这样
if(window.Worker){ // 使用Worker }
Web Worker
Web Worker的机制让你能够创建一个在后台线程运行的脚本,这个脚本不会对我们当前执行任务的脚本造成任何干扰(例如阻塞),同时Web Worker提供了一套API使你能够在当前脚本和后台脚本间进行数据的互相传输(worker)
“一套API, 两个对象”
我们现在已知的关于Web Worker的机制是: 有一个当前脚本, 和一个在后台运行的worker脚本,所以我们问题的关键就落在了这两个脚本的通信(数据交互)上
通过
var worker = new Worker("./worker.js");
生成了“两个对象”(你可能会问:为什么是两个不是一个呢?请往下看)
“第一个”对象是我们在当前脚本中通过构造函数显式创建出来的worker对象,它拥有一套API:postMessage和onmessage,通过postMessage方法可以向worker脚本(上文worker.js)发送数据, 通过onmessage方法可以从worker脚本接收数据
“第二个”对象是在Web Worker脚本(上文的worker.js)中隐式创建出来的全局变量对象,它叫DedicatedWorkerGlobalScope(这个时候在work.js全局变量对象是它而不是Window!!),而它也拥有一套API:postMessage和onmessage,通过postMessage方法可以向当前执行任务的脚本发送数据, 通过onmessage方法可以从当前执行任务的脚本接收数据
【注意】关于DedicatedWorkerGlobalScope
1. 它是在Web Worker脚本中生成的特殊的全局变量对象,也就是在全局执行环境中使用this指向的不是Window而是它
2. 它不能像Windows那样通过变量名直接访问,但在Web Worker脚本中你能通过this取到它
所以现在数据传递方向有两条:
1. 调用当前脚本中worker对象的postMessage方法, 然后在Web Worker脚本(上文的worker.js)中通过onmessage这个回调方法接收数据
2. 调用Web Worker脚本中的this.postMessage方法(this指向DedicatedWorkerGlobalScope),然后在当前脚本中worker对象的onmessage回调方法接收数据
看到这里可能有点懵,来让我们通过一个例子看看1中的数据传递:
先看示例吧,这是我们的目录结构
├─worker.js
├─main.js
└─index.html
index.html:
<html> <head> <meta charset="utf-8" /> <button id="work-button">传递数据</button> </head> <body> <script type="text/javascript" src="./main.js"></script> </body> </html>
main.js:
var button = document.querySelector("#work-button"); if(window.Worker){ var worker = new Worker("./worker.js"); button.onclick = function () { worker.postMessage("你好,我是当前脚本"); } }
worker.js:
this.onmessage = function (e) { console.log('work接收到的数据为:', e.data); }
点击按钮后,在main.js中调用worker对象的postMessage方法, 这个数据就被发给了work.js中的全局变量对象DedicatedWorkerGlobalScope, 所以我们在work,js中通过this.onmessage接收数据并输出
postMessage中参数传递给onmessage中event.data
【注意】postMessage传递的参数会被“原封不动”地传递给onmessage中event对象的data属性
例如:
postMessage([1,2,3]) ——> this.onmessage = function (e) { } 中 e.data === [1,2,3]
postMessage({a:1,b: 2}) ——> this.onmessage = function (e) { } 中 e.data === {a:1,b: 2}
当前任务脚本和worker脚本完整的通信流程
我们上面的例子展现的是从当前任务脚本向worker脚本传递数据,那么同样的道理,我们也能从work脚本向当前任务脚本传递数据(方式相同)
例子:
├─worker.js
├─main.js
└─index.html
index.html:
同上
main.js:
var button = document.querySelector("#work-button"); if(window.Worker){ var worker = new Worker("./worker.js"); button.onclick = function () { worker.postMessage("你好,我是当前脚本"); worker.onmessage = function (e) { console.log('当前脚本接收到的数据:',e.data) } } }
worker.js:
this.onmessage = function (e) { console.log('work接收到的数据为:', e.data); this.postMessage("你好,我是worker发来的数据") }
demo如下
点击传递数据输出:
canvas
cancas是H5新增的一个标签,把canvas翻译过来就是画布,顾名思义,这是用来”画画的“,那画画的”画笔“是什么呢? 它就是和canvas元素对象对应的一个”上下文对象“(context),这里的这个上下文对象可能和你印象中的”上下文“有较大的差异,它只是个单纯的包含了一系列“绘画”方法的对象,下面我们介绍的关于canvas的内容都要围绕这个"canvas上下文对象"展开
我们可以通过这种方式取得canvas上下文对象:
假设这是我们的HTML:
<canvas id="canvas"></canvas>
这样取得上下文对象:
let canvas = document.getElementById("canvas"); // 首先取得canvas元素对象 let ctx = canvas.getContext("2d"); //通过getContext()取得关键的上下文对象,2d表示画布是“平面”的
绘制基本形状
下面展现的是上下文对象的一些绘制图形的方法(它们都可以被ctx调用)
fillRect(x, y, width, height) // 绘制一个填充的矩形 strokeRect(x, y, width, height) // 绘制一个矩形的边框
上面的x,y代表相对于canvas画布左上角的横纵坐标:
例子:
html部分:
<canvas id="canvas" width="200px" height="100px"> 你的浏览器不支持canvas </canvas>
JS部分:
let canvas = document.getElementById("canvas"); if(canvas.getContext){ let ctx = canvas.getContext("2d"); ctx.fillRect(20,20,40,40); // 绘制矩形 }
【注意】. canvas标签内的内容(例如上面的文本)是否呈现取决于浏览器是否支持canvas,如果支持,则不出现,如果不支持,则会呈现出来
demo:
给画笔添加颜色和样式
我们以上面的为基础稍作修改:
let ctx = canvas.getContext("2d"); ctx.fillStyle = "#0081F0"; // 给上下文对象这支画笔添加填充颜色 ctx.fillRect(20,20,40,40);
demo:
绘制文本
let canvas = document.getElementById("canvas"); if(canvas.getContext){ let ctx = canvas.getContext("2d"); ctx.font = "26px serif"; // 设置文字大小和样式 ctx.fillText("外婆的",20,20); // “实心”的文本 ctx.strokeText("彭湖湾",20,60); // “空心”的文本 }
demo:
这里要稍微提一下, 也许上面的那些绘制图形,绘制文本的操作对你来说都没有触动,因为它们离我们的直接需求似乎还有一定的距离,但我想接下来的这几个上下文API你或许有些兴趣。
例如我们可能有一个需求是载入已有的图片,对它截图(裁剪)后保存为一张新的图片,这个时候我们就可以使用到canvas的绘制图片,裁剪图片,保存图片的API了
直接绘制已有图片
通过canvas上下文对象的drawImage方法可直接绘制图片
drawImage(image, x, y) // 其中 image 是 image 或者 canvas 对象
我们可以通过下面的一段代码动态获取img元素对象
let img = new Image(); img.onload = function () { // 运行这个函数的时候可以确保img已经被加载好了 }; img.src = "./beach.jpg" // 指定src后图片开始加载
废话不多说,直接上demo!
在相同目录下有这么一张图片
JS代码:
let canvas = document.getElementById("canvas"); let img = new Image(); img.onload = function () { if(canvas.getContext){ let ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0) } }; img.src = "./beach.jpg"
demo:
我们发现, 图片加载完成后被写入了画布当中!
图片裁剪功能
canvas上下文对象的clip方法可根据路径对canvas画布进行裁剪
让我们在原来的基础上添加一点东西:
let canvas = document.getElementById("canvas"); let img = new Image(); img.onload = function () { if(canvas.getContext){ let ctx = canvas.getContext("2d"); ctx.beginPath(); // 开始绘制路径 ctx.arc(100,100,100,0,Math.PI*2,true); // 绘制一个起点(100,100),半径为100的圆 ctx.clip(); // 裁剪 ctx.drawImage(img, 0, 0); // 画图 } }; img.src = "./beach.jpg"
【注意】clip方法的调用要在drawImage方法之前,否则不能成功! 也就是说要“先裁剪,再画图”
canvas的保存和导出
我们通过document.getElementById("canvas")取得的画布对象,有一个toDataURL()方法,可将当前画布作为一张图片,并返回其base64编码格式的数据,这在保存图片的时候非常有用
toDataURL接受两个参数:图片类型和质量参数
canvas.toDataURL(图片类型,质量参数)
canvas.toDataURL() // 默认返回的是png图片 canvas.toDataURL('image/jpeg') // 返回jpeg图片 canvas.toDataURL('image/jpeg', quality) // 创建一个JPG图片。你可以有选择地提供从0到1的品质量,1表示最好品质
看下面的例子
let canvas = document.getElementById("canvas"); let img = new Image(); img.onload = function () { if(canvas.getContext){ let ctx = canvas.getContext("2d"); ctx.beginPath(); // 开始绘制路径 ctx.arc(100,100,100,0,Math.PI*2,true); // 绘制一个起点为(100,100),半径为100的圆 ctx.clip(); // 裁剪 ctx.drawImage(img, 0, 0); // 画图 let src = canvas.toDataURL('image/png') console.log(src); } }; img.src = "./beach.jpg"
控制台输出了base64格式的数据:
我们通过网上的还原软件看看会把这个base64数据还原成什么图片:
正是我们想要的图片
indexedDB — — H5的“浏览器数据库”
indexedDB是存在于浏览器中的数据库,它和一般的数据库一样有写改删查的功能,不同之处在于:常见的数据库一般是在服务器上,并且要求我们的应用在线时才可以工作,而indexedDB使得在离线的时候读取数据成为了可能。下面,我就给大家介绍一下这个“驻扎”在浏览器上的特殊的数据库吧
使用open方法创建/打开数据库
我们首先要做的事情,当然是创建(或打开)一个数据库,这要用到indexedDB对象的open方法
它接收两个参数: 数据库名称和数据库版本(第二个参数是可选的)
indexedDB.open([ 数据库名称 ], [数据库版本])
调用open方法时候,如果对应名称的数据库不存在,则创建一个新的数据库,如果已存在,则打开已存在的那个数据库
需要说明的是, indexedDB里面绝大多数操作都是异步的, 上述的indexedDB.open并不会立即创建一个数据库, 你需要在异步的回调里面判断数据库是否创建成功,并对可能出现的错误做判断和处理
只有在onsuccess回调中,你才能通过request.result取得创建成功的数据库
var request = indexedDB.open("XXX", 1); request.onsuccess = function () { // request.result === 你通过open创建的数据库 }
通过open返回的request对象有三个回调:
onsuccess 每次创建/打开数据库时候都会调用
onerror 创建/打开数据库发生错误的时候调用
onupgradeneeded 数据库版本变化的时候调用 (onupgradeneeded 是我们唯一可以修改数据库结构的地方)
open一个indexedDB数据库后,一般在onupgradeneeded回调中初始化(或修改)数据库结构(划重点!!)
这包括两个方面的操作:
1. 通过db.createObjectStore创建对象存储空间,并取得ObjectStore对象(类似于SQL数据库中的建表操作)
2. 通过调用ObjectStore.createIndex创建该存储空间内的索引( 以便于提高查询时候的速度)
具体的可看下面的例子:
<script type="text/javascript"> if(!window.indexedDB) { alert("你的浏览器还不能支持indexedDB哦!") } var request = indexedDB.open("phwDataBase", 1); var db request.onsuccess = function () { // 将成功创建的数据库对象赋给db db = request.result; } request.onerror = function () { var errorDescribe = request.errorCode; // 打印错误 console.log(errorDescribe); } request.onupgradeneeded = function (e){ // 取得更新后的数据库对象, 并赋给db db = request.result; // 创建名为people数据存储空间, 第二个参数里的keyPath相当于“主键” var objectStore = db.createObjectStore("people", { keyPath: "id" }); // 创建索引, 加快查询速度 objectStore.createIndex("name", "name", {unique: false}); } </script>
运行一下, 然后让我们看看效果:
打开chrome的Application面板,点击左栏的Storage下的indexedDB使其展开
就可以看到我们新创建的phwDataBase数据库, 以及它内部的people数据存储空间了
(右边展示的是people数据存储空间的具体内容,因为现在什么数据都还没添加,所以key和value两列下是没有内容的)
看了上面的代码你可能会有些疑惑
onupgradeneeded 和onsuccess回调的关系是怎样的? 为什么我们必须在.onupgradeneeded中初始化数据库的结构,而不是在onsuccess中?
这主要是由两个回调调用的时机决定的:
1.对 onsuccess回调,在每次数据库创建/打开的时候都会调用(不仅是第一次创建的时候会调用,每次打开的时候也都会调用)
2. 对onupgradeneeded回调,在open提供第二个版本参数的前提下:
2.1 第一次调用open方法创建一个新的数据库的时候,onupgradeneeded一定会被调用
2.2 第二次或以后open该数据库,只在版本参数改变的时候, onupgradeneeded才会被调用
【注意】在缺少第二个版本参数的情况下,onupgradeneeded永远不会被调用!!
所以说:
1.open数据库的时候,最(yi)好(ding)要带上第二个参数(版本参数)
2. 修改数据库结构(例如创建和删除对象存储空间以及构建和删除索引。)要在onupgradeneeded回调中运行
(很显然每次打开都会被调用的onsuccess并不适合用于初始化数据库结构)
indexedDB的具体操作
首先说一下,在下面的展示例子中,我们的HTML是这样的
<button id="add-button">增加数据</button> <button id="delete-button">删除数据</button> <button id="get-button">获取数据</button> <button id="show-all-button">遍历全部数据</button> <button id="index-button">通过索引获取数据</button>
demo:
这里要说明一下的是,indexedDB的操作是以事务为基础的。 所以,对存储空间(objectStore)的操作都要基于事务来进行。 具体点说,就是需要先通过db.transaction()方法取得transaction对象,然后再通过 transaction.objectStore()方法取得目标objectStore,再然后才能调用objectStore的API进行“写改删查”
打个比方, 如果说我们存储的数据是粮食的话, objectStore就是一个个并排的粮仓,你可以往里面运粮食,也可以把粮食运出去, 但你对“粮食”做任何行为前, 都要和粮仓门前的守卫—— transaction(事务)“打声招呼”,得到准许才能进入粮仓
有两个方法要说一下
1. transaction方法
transaction 方法 一般接受两个参数,并返回一个事务对象。
1.1第一个参数是一个数组, 一个我们希望事务能够操作的objectStore所组成的数组,如果你希望这个事务能够操作所有的objectStore,那么传入空数组[]即可
1.2 第二个参数是一个字符串, 默认是“onlyread”, 如果我们有需要对数据进行写操作的需求的话可传入“readwrite”
例如我们下面的一行代码:
var transaction = db.transaction(["people"],"readwrite");
2. transaction.objectStore方法
这个方法接受一个参数: 指定的objectStore的名称, 方法返回的是获取到的objectStore
例如我们下面的一行代码:
var objectStore = transaction.objectStore("people");
写操作
写操作的关键在于objectStore.add(XXX);方法,其中XXX是我们初始化objectStore时候写入的“主键”
也就是 var objectStore = db.createObjectStore("people", { keyPath: "id" }); (这段代码在上面)中keyPath的值——id
function addData () { // 确保这个时候异步的open方法已经完成,并取得数据库对象 if(!db){ return; } // 我们要写入的数据 var data = [ {id: '1',name:'a', age: 10}, {id: '2',name:'b', age: 20}, {id: '3',name:'c', age: 30} ]; // 创建事务,并使其可读可写 var transaction = db.transaction(["people"],"readwrite"); transaction.oncomplete = function () { alert("添加事务已经完成") } transaction.onerror = function () { alert("出现错误") } // 通过事务对象取得people存储空间 var objectStore = transaction.objectStore("people"); for(let d of data) { // 调用add方法添加数据 objectStore.add(d); } } var addButton = document.getElementById("add-button"); addButton.onclick = addData
demo:
点击“增加数据”后弹出
再看看application面板下的indexedDB:
我们已经成功添加了三条数据进去了
删操作
删操作的关键在于objectStore.delete(XXX);方法,其中XXX是我们初始化objectStore时候写入的“主键”
也就是 var objectStore = db.createObjectStore("people", { keyPath: "id" }); 中keyPath的值——id
function deleteData () { if(!db){ return; } var transaction = db.transaction(["people"],"readwrite"); var objectStore = transaction.objectStore("people"); objectStore.delete("1"); transaction.oncomplete = function () { alert("删除事务已经完成") } } var deleteButton = document.getElementById("delete-button"); deleteButton.onclick = deleteData;
点击上面的“删除数据”按钮(删除id = 1的数据)
再来看看, id为1的那一行已经被删除了
查操作
删操作的关键在于objectStore.get(XXX);方法
function getData () { if(!db){ return; } var transaction = db.transaction(["people"], "readwrite"); var objectStore = transaction.objectStore("people"); var request = objectStore.get("2"); request.onsuccess = function () { alert(JSON.stringify(request.result)); } } var getButton = document.getElementById("get-button"); getButton.onclick = getData;
demo:
点击“获取数据”按钮,弹出
(这里固定查找id为2的数据)
遍历全部数据
遍历数据需要用到游标
通过 objectStore.openCursor()可创建一个游标对象(cursor), 这个cursor对象包含两个属性值: key和value
key就是我们一直说的那个“主键”, 而value是我们存入的时候的那个对象,通过 cursor.continue方法可以使得游标向下移动
function showAllData () { if(!db){ return; } var transaction = db.transaction(["people"], "readwrite"); var objectStore = transaction.objectStore("people"); console.log("遍历开始") objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if(cursor) { console.log(cursor.key, cursor.value); cursor.continue(); } } } var showAllButton = document.getElementById("show-all-button"); showAllButton.onclick = showAllData;
点击“遍历全部数据”按钮,看看控制台
通过索引查找
我们通过objectStore.get方法,通过查找主键的方式查找对应的对象数据的方式是很快的。
但如果我们通过非主键的数据去查找对应的那个对象就非常慢了,这个时候我们需要创建一个索引并通过索引来查找, 从而获得较快的速度:
function getByIndex () { if(!db){ return; } var transaction = db.transaction(["people"], "readwrite"); var objectStore = transaction.objectStore("people"); var index = objectStore.index("name"); var request = index.get("c"); request.onsuccess = function (event) { alert(JSON.stringify(request.result)); } } var indexButton = document.getElementById("index-button"); indexButton.onclick = getByIndex;
点击“通过索引获取数据”按钮:
好! 现在让我们对indexedDB做一个小小的总结:
1. indexedDB是面向对象的, 与传统的以二维表为基础的数据库不同
2. IndexedDB是一个事务型数据库系统
3. indexedDB大多数API都是异步的,这意味着调用一个方法你不能马上得到关键的那个对象,而在对应的success回调中才能取得
拖放事件
一个典型的拖放操作是这样开始的:用户用鼠标选中一个可拖动的(draggable)元素,移动鼠标到一个可放置的(droppable)元素,然后释放鼠标。 在操作期间,会触发一系列的拖放类型的事件
其中我们主要关心的事件有三个:
1. ondragstart 发生在可拖拽(draggable)的元素上, 在元素被拖动的时候调用
2. ondragover 发生在可放置(droppable)的元素上, 当某被拖动的对象在可放置对象范围内(上方)时触发此事件
3. ondrop 发生在可放置(droppable)的元素上,当释放鼠标使可拖拽元素“放进”可放置元素内的瞬间触发。
需思考的问题:
1. 如何使得被拖拽元素可拖拽?(因为元素默认的行为是不可拖拽的),以及如何使得被放置的容器元素可放置? (因为元素默认是不可放置的)
对前者, 我们可以为元素设置draggable属性,并且设置为true
对后者, 我们可以在被放置的容器元素中的ondragover事件里通过event.preventDefault();阻止默认行为——禁止放置
2.如何实现“脱 — 放”过程的数据传递?
这里首先需要知道的是,当我们拖动一个图片到另一个地方的时候,我们是不能“直接把图片拖拽进去”的,也就是说,我们还是要通过以下的思路实现拖放:
在被放置的元素中取得被拖拽元素的相关数据(如id),然后通过appendChild之类的API实现添加被拖拽的元素,从而模拟整个拖拽的过程
也就是说, 拖拽其实可分为三个过程: 拖动—传递被拖动元素的数据(如id)—在容器元素中添加该元素
关键在于如何在被拖动元素和被放置元素中传递数据,这可以通过event.dataTransfer对象来实现
dataTransfer可以通过setData方法添加拖动数据,并通过getDate方法取得拖动数据,我们可以在
ondragstart事件和ondrop事件中调用这两个方法, 实现关键性的数据传递。
具体请看下面的例子:
<div> <img id = "myImg" src="./clock.jpg" draggable="true" ondragstart="dragstart(event)" /> </div> <div id="targetDiv" ondragover="dragover(event)" ondrop="drop(event)"> </div> <script type="text/javascript"> function dragstart (event) { event.dataTransfer.setData("text/plain", event.target.id) } function drop (event) { // 阻止默认行为——禁止在浏览器中打开新的链接 event.preventDefault(); var imgId = event.dataTransfer.getData("text/plain"); var targetDiv = document.getElementById("targetDiv"); targetDiv.appendChild(document.getElementById(imgId)); } function dragover (event) { // 组织默认行为——禁止放置 event.preventDefault(); } </script>
拖拽前
拖拽后
参考资料:
【完】
我叫彭湖湾,请叫我胖湾