HTML 新增标签
在学习画布标签之前,需要对 CSS 样式,和 JavaScript 脚本有一定的基本了解,否则无法读懂 Canvas 画布 API
元素拖放
在 HTML5 中,任何元素都能够被拖放
要使得一个元素是可拖动的,必须将它的 draggable
属性设置为 true
<div draggable="true">我是可以被拖动的</div>
在拖动元素的过程中,会触发相关的事件,与源对象(被拖动元素)相关的事件如下:
ondragstart
:开始拖动元素时触发ondrag
:元素正在拖动时触发ondragend
:完成元素拖动后触发
<!DOCTYPE html>
<html>
<head>
<title>Drag Demo</title>
<head>
<style>
#source-object {
width: 50px;
height: 50px;
border: 1px solid red;
background-color: red;
}
</style>
<script>
function dragstart(event) { console.log('dragstart') }
function drag(event) { console.log('drag') }
function dragend(event) { console.log('dragend') }
</script>
</head>
</head>
<body>
<div id="source-object"
draggable="true"
ondragstart="dragstart(event)"
ondrag="drag(event)"
ondragend="dragend(event)" >
</div>
</body>
</html>
另外,我们还能监听在元素释放时触发的事件,与目标对象(其实就是释放区域)相关的事件如下:
ondragenter
:当被拖动元素进入释放区域时触发ondragover
:当被拖动元素在释放区域移动时触发ondragleave
:当被拖动元素离开释放区域时触发ondrop
:当被拖动元素在释放区域释放时触发
<!DOCTYPE html>
<html>
<head>
<title>Drop Demo</title>
<head>
<style>
#source-object {
width: 50px;
height: 50px;
border: 1px solid red;
background-color: red;
}
#target-object {
width: 100px;
height: 100px;
border: 1px solid blue;
background-color: blue;
}
</style>
<script>
function dragenter(event) { console.log('dragenter') }
function dragover(event) {
// dragover 事件的默认行为是阻止数据或元素放置到其他元素中
// 如果想要避免这种情况,就必须阻止 dragover 的默认行为
// 只有这样,才能触发 drop 事件
event.preventDefault()
console.log('dragover')
}
function dragleave(event) { console.log('dragleave') }
function drop(event) {
// 对于图片等元素,drop 事件的默认行为是以链接的形式打开
// 如果想要避免这种情况,就必须阻止 drop 的默认行为
event.preventDefault()
console.log('drop')
}
</script>
</head>
</head>
<body>
<div>请将红色方框移入蓝色方框</div>
<br />
<div id="source-object" draggable="true"></div>
<br />
<div id="target-object"
ondragenter="dragenter(event)"
ondragover="dragover(event)"
ondragleave="dragleave(event)"
ondrop="drop(event)" >
</div>
</body>
</html>
拖放事件还有一个新的属性 dataTransfer
,用于在源对象和目标对象之间传递数据
dataTransfer
它是一个对象,常用的方法有两个:
setData(key, value)
:在源对象设置数据getData(key)
:在目标对象获取数据
<!DOCTYPE html>
<html>
<head>
<title>Drag And Drop Demo</title>
<head>
<style>
#source-object {
width: 50px;
height: 50px;
border: 1px solid red;
background-color: red;
}
#target-object {
width: 100px;
height: 100px;
border: 1px solid blue;
background-color: blue;
}
</style>
<script>
// 源对象事件
function dragstart(event) {
event.dataTransfer.setData('elemId', event.target.id)
}
// 目标对象事件
function dragover(event) {
event.preventDefault()
}
function drop(event) {
event.preventDefault()
var elemId = event.dataTransfer.getData('elemId')
var elem = document.getElementById(elemId)
event.target.appendChild(elem)
}
</script>
</head>
</head>
<body>
<div>请将红色方框移入蓝色方框</div>
<br />
<div id="source-object"
draggable="true"
ondragstart="dragstart(event)" >
</div>
<br />
<div id="target-object"
ondragover="dragover(event)"
ondrop="drop(event)">
</div>
</body>
</html>
最后来一个拖放在实际应用中的例子,实现文件拖拽上传功能
<!DOCTYPE html>
<html>
<head>
<title>Drag And Drop Demo</title>
<head>
<style>
#file-input {
width: 300px;
height: 100px;
border-radius: 50px;
border: 1px dashed black;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>
<script>
window.onload = function() {
var fileInput = document.getElementById('file-input')
fileInput.ondragover = function(event) {
event.preventDefault()
}
fileInput.ondrop = function(event) {
event.preventDefault()
// 每次只获取第一个文件对象
var file = event.dataTransfer.files[0]
// 检查
let isRightType = checkFileType(file)
let isRightSize = checkFileSize(file)
// 提示
if (isRightType && isRightSize) {
alert('上传文件成功')
} else if (!isRightType) {
alert('只支持 DOCX、TXT、MD 文件格式')
} else if (!isRightSize) {
alert('文件不能大于 2 M')
}
}
function checkFileType(file) {
let allowedTypes = ['DOCX', 'TXT', 'MD']
let fileInfo = file.name.split('.')
let fileType = fileInfo[fileInfo.length - 1].toUpperCase()
return allowedTypes.includes(fileType)
}
function checkFileSize(file) {
let maxSize = 2 * 1024 * 1024
let fileSize = file.size
return fileSize <= maxSize
}
}
</script>
</head>
</head>
<body>
<div id="file-input">请将文件拖动到此处</div>
</body>
</html>
本地缓存
Web Storage 允许在浏览器保存用户数据,具体分为两种,一种是 localStorage,一种是 sessionStorage
对于客户端的存储方式,类似的还有早期乃至现在还广泛使用的 cookie,它们之间的区别如下:
cookie | localStorage | sessionStorage | |
---|---|---|---|
与服务端的通信 | 在每次请求中都会携带 | 不与服务端通信 | 不与服务端通信 |
数据的生命周期 | 设置的失效时间前有效 | 永久有效,除非主动删除 | 在浏览器关闭前有效 |
数据的作用范围 | 设置的域名及其子域名 | 在所有同源窗口之间共享 | 不能在不同窗口之间共享 |
储存的数据大小 | 一般不超过 4KB | 一般在 5MB 左右 | 一般在 5MB 左右 |
Web Storage API 继承 window
对象,并提供两个新属性,window.localStorage
和 window.sessionStorage
它们都是一个对象,常见的属性和方法如下:
length
:保存的数据条数setItem(key, value)
:保存数据getItem(key)
:获取指定数据removeItem(key)
:删除指定数据clear()
:删除所有数据key(index)
:根据索引获取键名
if (window.localStorage) {
var storage = window.localStorage;
storage.setItem('username', 'admin');
storage.setItem('password', '12345');
let username = storage.getItem('username');
let password = storage.getItem('password');
console.log(username); // admin
console.log(password); // 12345
console.log(storage.length); // 2
storage.removeItem('password');
console.log(storage.length); // 1
storage.clear();
}
最后顺便来补充一下 cookie 和 session 的相关知识
我们知道,HTTP 协议是无状态的,也就是说每次发出的请求都是独立的,这个时候就会造成很多的不便
比如,用户在一个请求中登陆之后,对于他发出的另外一个请求,服务器还是无法识别用户的身份
而 cookie 和 session 的出现都是为了记录用户的信息,从而在多次请求中识别用户的身份
cookie 的运作机制如下:
- 服务器在响应中通过
set-cookie
头部要求浏览器设置 cookie - 浏览器在收到响应后,如果该浏览器支持使用 cookie,那么就会将 cookie 保存到文件
- 浏览器在以后每次发出请求时,都会在请求中通过
cookie
头部带上 cookie 信息 - 服务器在收到请求后,根据 cookie 信息识别用户身份
session 的运作机制如下:
-
服务器创建一个唯一的标识,并将这个标识和对应的 session 信息保存下来,然后将这个标识发给浏览器
保存 session 信息的方式常见的有三种,一种是保存在内存中,一种是保存在文件中,一种是保存在数据库中
-
浏览器在收到这个标识后,在以后每次发出请求时,都会带上这个标识
带上标识的方式常见的有两种,一种是附加在 cookie 中,一种是附加在 URL 参数中
-
服务器在收到请求后,根据标识找到对应的 session 信息识别用户身份
cookie 数据存放在浏览器上,session 数据存放在服务器上,所以相对而言使用 session 更为安全
但是,过多的 session 数据保存在服务器上,会影响服务器的性能,所以需要权衡选择
线程环境
Web Worker 为 JavaScript 创建多线程环境
主线程可以创建 Worker 线程,并将一些高延迟的任务分配给 Worker 线程在后台运行
而主线程只会负责渲染和交互,从而保证页面的流畅性
Web Worker 有两种类型,分别是 Dedicated Web Worker 和 Shared Web Worker
- Dedicated Web Worker:只有一个页面可以使用这个 Web Worker
- Shared Web Worker:多个同源页面可以共享一个 Web Worker,为跨页面通信提供一种解决方案
下面介绍 Dedicated Web Worker
主线程通过构造函数 Worker()
,创建一个 Worker 线程,并在参数中指定需要执行的脚本文件
var worker = new Worker('worker.js')
构造函数返回一个 Worker 对象,便于主线程与 Worker 线程通信,Worker 对象常用的属性和方法如下:
onerror
:用于指定error
事件的监听函数,当 Worker 发生错误时触发onmessage
:用于指定message
事件的监听函数,当接收到传递的数据时触发onmessageerror
:用于指定messageerror
事件的监听函数,当收到的数据无法进行反序列化时触发postMessage()
:传递数据给 Worker 线程terminate()
:终止 Worker 线程
<!DOCTYPE html>
<html>
<head>
<script>
var worker;
function submit() {
if (typeof(Worker) === 'undefined') {
return
}
if (typeof(worker) === "undefined") {
worker = new Worker('worker.js')
}
let input = document.getElementById('data').value
worker.postMessage(input)
worker.onmessage = function(event) {
let result = event.data
document.getElementById('result').innerText = result
worker.terminate()
worker = undefined
}
worker.onerror = function(event) {
console.log('出错的信息', event.message)
console.log('出错的文件', event.filename)
console.log('出错的行数', event.lineno)
console.log('出错的列数', event.colno)
worker.terminate()
worker = undefined
}
}
</script>
</head>
<body>
<input type="text" id="data" name="data" placeholder="Please Enter Something">
<button onclick="submit()">Reverse String</button>
<span id="result"></span>
</body>
</html>
Worker 线程有一个自己的全局对象,self
和 this
都提供对该对象的引用,这个对象常用的属性和方法如下:
onmessage
:用于指定message
事件的监听函数,当接收到传递的数据时触发onmessageerror
:用于指定messageerror
事件的监听函数,当收到的数据无法进行反序列化时触发importScripts()
:用于加载其它脚本postMessage()
:传递数据给主线程close()
:关闭 Worker 线程
// worker.js
this.onmessage = function(event) {
let data = event.data;
let result = reverse(data);
this.postMessage(result);
this.close();
}
function reverse(content) {
return content.split('').reverse().join('');
}
使用 Web Worker 需要注意以下的点:
-
主线程分配给 Worker 线程的脚本文件,不能从本地读取,它必须来自网络
-
主线程分配给 Worker 线程的脚本文件,必须与主线程的脚本文件同源,准确来说应该是与页面文档同源
-
Worker 线程不能访问 window、document、parent 对象,但是可以使用 navigator、location 对象
-
Worker 线程不能使用 alert、confirm,但是可以使用 setTimeout、setInterval、XMLHttpRequest
-
主线程和 Worker 线程不能直接通信,必须通过消息传递,消息传递有两种方式,一种是拷贝,一种是转让
拷贝:拷贝原始数据,产生一份副本,将副本传递给目标线程,这种方法在传递较大的数据时比较消耗性能
转让:将原始数据的所有权转让给目标线程,原来线程不再具有这个数据
在默认的实现中,除 ArrayBuffer 外都是拷贝传递
通讯协议
WebSocket 是一个建立在 TCP 之上进行全双工通讯的应用层协议
我们知道,HTTP 协议采用的是请求/响应模式,服务端是不能主动向客户端推送数据的
若要实现推送功能,一般都是采用 Ajax 轮询,但这样频繁的请求会给服务端带来极大的压力
WebSocket 协议就是为了解决这种类似的场景而出现的,它允许服务端可以主动向客户端推送数据
浏览器和服务器通过一次握手,就能在两者之间创建持续性的连接,进行双向数据传输,直至任意一方将其关闭
目前,基于 SHA 加密方式的握手协议是使用最为广泛的,具体的过程如下:
- 浏览器向服务器发起一个 HTTP 请求,但是这个请求与普通的请求不同,它会附加一些头部信息
- 服务器解析头部信息,返回一个响应,这样就能建立起 WebSocket 连接
一个典型的客户端请求如下:
GET ws://localhost:5000 HTTP/1.1
Host: localhost
Origin: http://localhost:5000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: 随机字符串
Sec-WebSocket-Version: 13
Connection
:必须设置为Upgrade
,表示客户端希望升级协议Upgrade
:必须设置为websocket
,表示希望将协议升级为 WebsocketSec-WebSocket-Key
:随机字符串,与响应中Sec-WebSocket-Accept
的值做对比验证
一个典型的服务端响应如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 根据 Sec-WebSocket-Key 计算得到
Sec-WebSocket-Location: ws://localhost:5000
-
Connection
:如果值为Upgrade
,表示服务端同意升级协议 -
Upgrade
:如果值为websocket
,表示同意将协议升级为 Websocket -
Sec-WebSocket-Accept
:经过服务器确认并加密的Sec-WebSocket-Key
将
Sec-WebSocket-Key
的值拼上一段特殊的字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11然后使用 SHA1 对这个字符串做哈希,再进行 Base64 编码后,才能得到
Sec-WebSocket-Accept
的值
在理解了原理之后,我们下面再来看一下具体要怎么使用 WebSocket
在客户端浏览器中,可以通过构造函数 WebSocket()
创建一个对象,该对象能提供使用 WebSocket 的相关 API
其中,参数 url
用于指定连接地址,参数 protocol
用于指定可接受的子协议
WebSocket(url, [protocol])
构造函数返回一个 WebSocket 对象,该对象常用的属性和方法如下:
-
readyState
:只读属性,表示连接状态0
表示连接尚未建立,1
表示连接已经建立,可以进行通信2
表示连接正在关闭,3
表示连接已经关闭,或者不能打开 -
bufferedAmount
:只读属性,在缓存中等待传输的字节数 -
onopen
:指定open
事件的监听函数,在连接建立时触发 -
onmessage
:指定message
事件的监听函数,在收到数据时触发 -
onerror
:指定error
事件的监听函数,在发生错误时触发 -
onclose
:指定close
事件的监听函数,在连接关闭时触发 -
send()
:发送数据 -
close()
:关闭连接
var ws = new WebSocket('ws://localhost:5000');
ws.onopen = function(event) {
console.log('open');
};
ws.onmessage = function(event) {
console.log('message', event.data);
ws.send('Goodbye');
ws.close();
};
ws.onclose = function(event) {
console.log('close');
};
ws.onerror = function(event) {
console.log('error');
};
而对于服务器而言,WebSocket 只是一种协议而已,不同的语言和不同的框架会有不同的实现
下面我们以 Node.js 为例,写一个简单的 Demo
var express = require('express');
var expressWs = require('express-ws');
var app = express();
expressWs(app);
app.ws('/', function (ws, req){
// ws: WebSocket 实例
// req:Request 实例
ws.send('Hello');
ws.on('message', function (msg) {
console.log(msg);
});
});
var server = app.listen(5000, '127.0.0.1', function() {
console.log('server running at http://127.0.0.1:5000');
});