# Fetch 基本使用
fetch
返回一个 promise
,常用方式:
let response = await fetch(url, options); // 解析 response header
let result = await response.json(); // 将 body 读取为 json
options
:method
:HTTP 方法,如 get、post……headers
:请求头,但是有一些 header 是不被允许的,详见规范body
:request body,要以string
,FormData
,BufferSource
,Blob
或UrlSearchParams
对象的形式发送的数据
接收到的 response
属性如下:
response.status
—— response 的 HTTP 状态码,response.ok
—— HTTP 状态码为 200-299,则为true
。response.headers
—— 类似于 Map 的带有 HTTP header 的对象。
注意 ,只有在无法建立一个 HTTP 请求的时候,
fetch
才会reject
。而异常的404、500
等状态不会导致异常
获取 response body
:
response.text()
—— 读取 response,并以文本形式返回 response,response.json()
—— 将 response 解析为 JSON 对象形式,response.formData()
—— 以FormData
对象(form/multipart 编码,参见下一章)的形式返回 response,response.blob()
—— 以 Blob(具有类型的二进制数据)形式返回 response,response.arrayBuffer()
—— 以 ArrayBuffer(低级别的二进制数据)形式返回 response。
# FormData
# 发送表单
FormData
对象可以发送表单对象内容,比如:
<form id="formElem">
<input type="text" name="name" value="John">
<input type="text" name="surname" value="Smith">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/article/formdata/post/user', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
alert(result.message);
};
</script>
这样可以把整个表单对象发送出去。
我们也可以通过以下方法去修改添加表单对象到 FormData
:
formData.append(name, value)
—— 添加具有给定 name 和 value 的表单字段,formData.append(name, blob, fileName)
—— 添加一个字段,就像它是<input type='file'>
,第三个参数 fileName 设置文件名(而不是表单字段名),因为它是用户文件系统中文件的名称formData.set(name,value)
—— set 方法会清楚当前formData
中所有此name
的字段,然后插入新的value
formData.set(name,blob,value)
—— 原理同上formData.delete(name)
—— 移除带有给定 name 的字段,formData.get(name)
—— 获取带有给定 name 的字段值,formData.has(name)
—— 如果存在带有给定 name 的字段,则返回 true,否则返回 false。
FormData
对象是可以循环的 : for(let[key,value] of formData){}
# 发送文件
<input type="file">
允许我们发送一个文件:
<input type="file" id="pic">
<script>
let formData = new FormData()
pic.onchange = function(e){
formData.append("fileIn",this.value)
}
let response = await fetch('/xxx', {
method: 'POST',
body: formData
});
let result = await response.json();
</script>
# 下载进度
可以通过 response.body
,它是 ReadableStream
—— 一个特殊的对象,它可以逐块(chunk)提供 body。在 Streams API 规范中有对 ReadableStream
的详细描述。
然后可以通过他的 getReader()
来获取一个流读取器(stream reader):
// Step 1:启动 fetch,并获得一个 reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');
const reader = response.body.getReader();
// Step 2:获得总长度(length)
const contentLength = +response.headers.get('Content-Length');
// Step 3:读取数据
let receivedLength = 0; // 当前接收到了这么多字节
let chunks = []; // 接收到的二进制块的数组(包括 body)
while(true) {
// read() 会返回done —— 读取完成时是 true,否则 false
//还有 value —— 字节的类型化数组:Uint8Array。
const {done, value} = await reader.read();
if (done) {
break;
}
// 每拿到一块 就放入二进制数组
chunks.push(value);
receivedLength += value.length;
console.log(`Received ${receivedLength} of ${contentLength}`)
}
// 接收已完成,以下是处理过程。
// Step 4:将块连接到单个 Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
let position = 0;
for(let chunk of chunks) {
chunksAll.set(chunk, position); // (4.2)
position += chunk.length;
}
// Step 5:解码成字符串
let result = new TextDecoder("utf-8").decode(chunksAll);
// 我们完成啦!
let commits = JSON.parse(result);
# Fetch 中止
我们有一个 AbortController
对象,他的用法很简单:
let aContro = new AbortController()
// 属性 signal,我们可以在这个属性上设置事件监听器。
let singal = aContro.signal
// 方法 abort()
aContro.abort()
// 事件触发后,signal.aborted 变为 true
alert(signal.aborted); // true
结合 fetch:
// 1 秒后中止
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
let response = await fetch('/article/fetch-abort/demo/hang', {
signal: controller.signal
});
} catch(err) {
if (err.name == 'AbortError') { // handle abort()
alert("Aborted!");
} else {
throw err;
}
}
# Fetch Api
fetch 还有很多 api,详见 现代教程
# CORS 跨域
"CORS" :跨源资源共享(Cross-Origin Resource Sharing)。
# JSONP
JSONP 的原理很简单,就是利用一个 script
标签,然后我们的信息作为回调函数传过去来实现跨域:
function gotWeather({ temperature, humidity }) {
alert(`temperature: ${temperature}, humidity: ${humidity}`);
}
let script = document.createElement('script');
script.src = `http://another.com/weather.json?callback=gotWeather`;
document.body.append(script);
# 简单跨域请求
首先,简单跨域请求需要满足如下条件:
method
—— GET,POST 或 HEADheaders
—— 仅允许自定义下列 header:- Accept,
- Accept-Language,
- Content-Language,
- Content-Type 的值为 application/x-www-form-urlencoded,multipart/form-data 或 text/plain。
如果我们的跨域请求是一个简单请求,在发送 request 的时候,浏览器会自动添加 Origin
header,服务器需要进行对 Origin
的检查,如果同意这个源的 request ,那么在 response 里就会添加一个 Access-Control-Allow-Origin
的响应头,这个响应头的内容可以是我们刚才的 Origin
,或者是一个 *
号 :
而且在这种简单请求中,我们只能访问简单的 response header
:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
其他的都不可访问
# 非简单跨域请求
不满足我们上面说的 request 条件的请求视为非简单请求,比如 method
是 PUT
,发送这样的请求的时候,浏览器会在发送正式请求前 **加一个预检(preflight) **的请求
预检请求使用 OPTIONS
method,它没有 body
,但是有两个关键 header
:
Access-Control-Request-Method
—— 正式请求中带有的非简单请求的method
Access-Control-Request-Headers
—— 正式请求中带有的非简单的HTTP-Header
列表,用,
分隔
而服务器端同意处理也会返回一个状态码为 200
,没有 body
但是有一些特殊 header
的 response:
Access-Control-Allow-Origin
必须为*
或进行请求的源(例如[https://javascript.info](https://javascript.info)
)才能允许此请求。Access-Control-Allow-Methods
必须具有允许的方法。Access-Control-Allow-Headers
必须具有一个允许的 header 列表。- 另外,header
Access-Control-Max-Age
可以指定缓存此权限的秒数。因此,浏览器不是必须为满足给定权限的后续请求发送预检。
预检请求整个过程对于 JavaScript 是不可见的!JavaScript 仅仅获取主请求的 response
# 非简单跨域例子
假设我们现在有一个跨域请求
let response = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret'
}
});
他的 method
、 headers
都不满足简单请求的规则,那么接下来会发生非简单请求:
# 第一步 发送预检请求
浏览器检测到非简单跨域请求后,会自动先发送一个预检
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
包含了上面说的非简单请求的 method
和 非简单的 HTTP-Header
列表
# 第二步 服务器响应预检请求
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
这个 response 表示:
- 请求通过
200
, - 允许的
Origin
是我们 request 的源, - 他不仅允许我们 request 的
PATCH
,还允许PUT/DELETE
等特殊method
,当然GET,POST 或 HEAD
本身就是允许的,response 不用写进去 - 也允许了一些
header
:API-Key,Content-Type,If-Modified-Since,Cache-Control
- 设置了缓存时间一天,后续的请求就不用再触发预检了
# 第三步 发送实际请求
通过了预检,自然是开始发送实际 requset,注意浏览器一定会加上 Origin header
,因为他是跨域的
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
# 第四步 返回实际响应
服务器返回的 response 也必须加上 Access-Control-Allow-Origin
,即时我们已经通过了预检
Access-Control-Allow-Origin: https://javascript.info
至此一个非简单跨域完成
# 凭据
在上面的跨域情况中,无法发送任何 凭据(cookies 或者 HTTP 认证)
这是因为具有凭据的请求比没有凭据的请求要强大得多。如果被允许,它会使用它们的凭据授予 JavaScript 代表用户行为和访问敏感信息的全部权力。
如果需要发送凭据,需要加上 credentials: "include"
:
fetch('http://another.com', {
credentials: "include"
});
而服务器如果同意接受带有凭据的请求,那么需要加上一个 header: Access-Control-Allow-Credentials: true
,当然也别忘记必须有的 Access-Control-Allow-Origin
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
# URL 对象
以往我们打开一个窗口,可能会用如下方式:
window.open("https://zh.javascript.info/url")
这种方式,url 是一串字符串,而现在我们有一个内建的 URL
对象
new URL(url, [base])
如下两个 url 是一样的
let url1 = new URL('https://javascript.info/profile/admin');
let url2 = new URL('/profile/admin', 'https://javascript.info');
alert(url1); // https://javascript.info/profile/admin
alert(url2); // https://javascript.info/profile/admin
也可以根据现有 URL 创建新 URL
let url = new URL('https://javascript.info/profile/admin');
let newUrl = new URL('tester', url);
alert(newUrl); // https://javascript.info/profile/tester
还可以访问他的组件属性
let url = new URL('https://javascript.info/url');
alert(url.protocol); // https:
alert(url.host); // javascript.info
alert(url.pathname); // /url
我们还可以处理 url 的参数:
append(name, value)
—— 按照 name 添加参数,delete(name)
—— 按照 name 移除参数,get(name)
—— 按照 name 获取参数,getAll(name)
—— 获取相同 name 的所有参数(这是可行的,例如 ?user=John&user=Pete),has(name)
—— 按照 name 检查参数是否存在,set(name, value)
—— set/replace 参数,sort()
—— 按 name 对参数进行排序,很少使用,
这些方法处理参数的时候,会自动进行编码 传统的编码一般是用这些方法:
- encodeURI —— 编码整个 URL。
- decodeURI —— 解码为编码前的状态。
- encodeURIComponent —— 编码 URL 组件,例如搜索参数,或者 hash,或者 pathname。
- decodeURIComponent —— 解码为编码前的状态。
# 轮询
# 短轮询
短轮询就是定时地对服务器进行 request
# 长轮询
发送 request -> 消息挂起 -> 服务器 response 返回 -> 客户端处理 response -> 发起下一个 request
async function subscribe() {
let response = await fetch("/subscribe");
if (response.status == 502) {
// 状态 502 是连接超时错误,
// 连接挂起时间过长时可能会发生,
// 远程服务器或代理会关闭它
// 让我们重新连接
await subscribe();
} else if (response.status != 200) {
// 一个 error —— 让我们显示它
showMessage(response.statusText);
// 一秒后重新连接
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// 获取并显示消息
let message = await response.text();
showMessage(message);
// 再次调用 subscribe() 以获取下一条消息
await subscribe();
}
}
subscribe();
# WebSocket
# 创建连接
WebSocket
协议提供了一种在浏览器和服务器之间建立持久连接来交换数据的方法。数据可以作为“数据包”在两个方向上传递,而不会断开连接和其他 HTTP 请求。
let socket = new WebSocket("ws://javascript.info");
ws
是一个特殊的协议 ,还有一个 wss
协议,类似于 HTTPS 他是加密的,建议使用 wss://
open
—— 连接已建立,message
—— 接收到数据,error
—— WebSocket 错误,close
—— 连接已关闭。
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Connection established");
alert("Sending to server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// 例如服务器进程被杀死或网络中断
// 在这种情况下,event.code 通常为 1006
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error] ${error.message}`);
};
# request
以下是由 new WebSocket("wss://javascript.info/chat")
发出的请求的浏览器 header 示例。
GET /chat
Host: javascript.info
Origin: https://javascript.info [客户端页面的源]
Connection: Upgrade [Upgrade —— 表示客户端想要更改协议。]
Upgrade: websocket [websocket —— 请求的协议是 “websocket”]
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== [浏览器随机生成的安全密钥]
Sec-WebSocket-Version: 13 [WebSocket 协议版本,当前为 13]
这些 header 在JavaScript 中不可见。
还有 Sec-WebSocket-Protocol: soap, wamp
代表我们不仅要传输任何数据,还要传输 SOAP 或 WAMP 协议中的数据。这个是我们可设置的:
let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
# 数据传输
WebSocket 通信由 “frames”(即数据片段)组成,可以从任何一方发送,并且有以下几种类型:
- “text frames” —— 包含各方发送给彼此的文本数据。
- “binary data frames” —— 包含各方发送给彼此的二进制数据。
- “ping/pong frames” 被用于检查从服务器发送的连接,浏览器会自动响应它们。
- 还有 “connection close frame” 以及其他服务 frames。
客户端请求 .send()
可以发送文本和二进制数据(包括 Blob,ArrayBuffer 等)
而接收到 response 的时候,文本肯定是字符串的形式,但是如果是个二进制数据,可以通过 socket.binaryType
来设置格式类型,默认值就是 blob
。
socket.binaryType = "arraybuffer"; // 默认值是 blob 可以不设置
socket.onmessage = (event) => {
// event.data 可以是文本(如果是文本),也可以是 arraybuffer(如果是二进制数据)
};
# 缓冲数据
假设我们一次发送了很多数据,但是网很慢,当我们反复 send()
的情况下,数据会缓存进内存, socket.bufferedAmount
可以查询当前缓存了多少字节
// 每 100ms 检查一次 socket
// 仅当所有现有的数据都已被发送出去时,再发送更多数据
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
# 关闭连接
不管是客户端还是服务端都可以主动关闭连接,当需要关闭的时候:
socket.close(code,reason)
code
—— 一个关闭码:- 1000 —— 默认,正常关闭(如果没有指明 code 时使用它),
- 1006 —— 没有办法手动设定这个数字码,表示连接丢失(没有 close frame)。
- 1001 —— 一方正在离开,例如服务器正在关闭,或者浏览器离开了该页面,
- 1009 —— 消息太大,无法处理,
- 1011 —— 服务器上发生意外错误
reason
—— 关闭原因,比如 “work complete”
我们可以在 close
事件中获取他们: event.code / event.reason
# 连接状态
要获取连接状态,可以通过带有值的 socket.readyState 属性:
- 0 —— “CONNECTING”:连接还未建立,
- 1 —— “OPEN”:通信中,
- 2 —— “CLOSING”:连接关闭中,
- 3 —— “CLOSED”:连接已关闭。
# 补充:Web Worker
既然上面提到了 WebSocket,那就顺带提一下 Web Worker
其实是我也不知道应该把它放在哪一块内容比较合适🙄
# 1. 概念
Web Worker 为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。注意,在worker内,不能直接操作DOM节点,也不能使用 window 对象的默认方法和属性。
# 2. 方法
先看一个例子:
- index.html
<body>
<div> 计数: <output id="result">0</output></div>
<button onclick="start()">开始worker</button>
<button onclick="end()">结束worker</button>
<script>
let worker
function start() {
// 检测浏览器是否支持 Web Worker
if (typeof Worker === "undefined") {
alert(`sor,br dont support`)
} else {
if (typeof worker == "undefined") {
worker = new Worker("worker.js")
}
worker.onmessage = function (e) {
result.textContent = e.data
}
worker.onerror = function(e){
console.error(`错误信息:${e.message};错误脚本:${e.filename};错误行号:${e.lineno}`);
}
}
}
function end() {
worker.terminate()
}
</script>
</body>
- worker.js
let i = 0
function count() {
if (++i == 10) close();
postMessage(i)
setTimeout(() => {
count()
}, 500)
}
count()
接下来结合上述代码说明一些方法👇
# 检测浏览器是否支持 Web Worker
if(typeof Worker !== "undefined"){
// 是的! Web worker 支持!
// 一些代码.....
}else{
//抱歉! Web Worker 不支持
}
# 开启一个 Web Worker
if (typeof worker == "undefined") {
worker = new Worker("worker.js")
}
# worker 发送消息
postMessage(i)
# 接收 message
用 message
事件接收, e.data
是获取传来的数据
worker.onmessage = function (e) {
result.textContent = e.data
}
# 终止 Web Worker
- 在主线程上结束
如果你需要从主线程中立刻终止一个运行中的worker,可以调用 worker 的 terminate 方法,worker 线程会被立即杀死,不会有任何机会让它完成自己的操作或清理工作。:
worker.terminate()
- 在 worker 线程中结束
close()
# 错误处理 error
message
可读性良好的错误消息。filename
发生错误的脚本文件名。lineno
发生错误时所在脚本文件的行号。
worker.onerror = function(e){
console.error(`错误信息:${e.message};错误脚本:${e.filename};错误行号:${e.lineno}`);
}
# 补充:常用请求头
协议头 | 说明 | 示例 |
---|---|---|
Accept | 可接受的响应内容类型(Content-Types )。 | Accept: text/plain |
Accept-Charset | 可接受的字符集 | Accept-Charset: utf-8 |
Accept-Encoding | 可接受的响应内容的编码方式。 | Accept-Encoding: gzip, deflate |
Accept-Language | 可接受的响应内容语言列表。 | Accept-Language: en-US |
Accept-Datetime | 可接受的按照时间来表示的响应内容版本 | Accept-Datetime: Sat, 26 Dec 2015 17:30:00 GMT |
Authorization | 用于表示HTTP协议中需要认证资源的认证信息 | Authorization: Basic OSdjJGRpbjpvcGVuIANlc2SdDE== |
Cache-Control | 用来指定当前的请求/回复中的,是否使用缓存机制。 | Cache-Control: no-cache |
Connection | 客户端(浏览器)想要优先使用的连接类型 | Connection: keep-alive``Connection: Upgrade |
Cookie | 由之前服务器通过Set-Cookie (见下文)设置的一个HTTP协议Cookie | Cookie: $Version=1; Skin=new; |
Content-Length | 以8进制表示的请求体的长度 | Content-Length: 348 |
Content-MD5 | 请求体的内容的二进制 MD5 散列值(数字签名),以 Base64 编码的结果 | Content-MD5: oD8dH2sgSW50ZWdyaIEd9D== |
Content-Type | 请求体的MIME类型 (用于POST和PUT请求中) | Content-Type: application/x-www-form-urlencoded |
Date | 发送该消息的日期和时间(以RFC 7231中定义的"HTTP日期"格式来发送) | Date: Dec, 26 Dec 2015 17:30:00 GMT |
Expect | 表示客户端要求服务器做出特定的行为 | Expect: 100-continue |
From | 发起此请求的用户的邮件地址 | From: user@itbilu.com |
Host | 表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略。 | Host: www.itbilu.com:80``Host: www.itbilu.com |
If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要用于像 PUT 这样的方法中,仅当从用户上次更新某个资源后,该资源未被修改的情况下,才更新该资源。 | If-Match: "9jd00cdj34pss9ejqiw39d82f20d0ikd" |
If-Modified-Since | 允许在对应的资源未被修改的情况下返回304未修改 | If-Modified-Since: Dec, 26 Dec 2015 17:30:00 GMT |
If-None-Match | 允许在对应的内容未被修改的情况下返回304未修改( 304 Not Modified ),参考 超文本传输协议 的实体标记 | If-None-Match: "9jd00cdj34pss9ejqiw39d82f20d0ikd" |
If-Range | 如果该实体未被修改过,则向返回所缺少的那一个或多个部分。否则,返回整个新的实体 | If-Range: "9jd00cdj34pss9ejqiw39d82f20d0ikd" |
If-Unmodified-Since | 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Dec, 26 Dec 2015 17:30:00 GMT |
Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 |
Origin | 发起一个针对跨域资源共享的请求(该请求要求服务器在响应中加入一个Access-Control-Allow-Origin 的消息头,表示访问控制所允许的来源)。 | Origin: http://www.itbilu.com |
Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生。 | Pragma: no-cache |
Proxy-Authorization | 用于向代理进行认证的认证信息。 | Proxy-Authorization: Basic IOoDZRgDOi0vcGVuIHNlNidJi2== |
Range | 表示请求某个实体的一部分,字节偏移以0开始。 | Range: bytes=500-999 |
Referer | 表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer 其实是Referrer 这个单词,但RFC制作标准时给拼错了,后来也就将错就错使用Referer 了。 | Referer: http://itbilu.com/nodejs |
TE | 浏览器预期接受的传输时的编码方式:可使用回应协议头Transfer-Encoding 中的值(还可以使用"trailers"表示数据传输时的分块方式)用来表示浏览器希望在最后一个大小为0的块之后还接收到一些额外的字段。 | TE: trailers,deflate |
User-Agent | 浏览器的身份标识字符串 | User-Agent: Mozilla/…… |
Upgrade | 要求服务器升级到一个高版本协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
Via | 告诉服务器,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 itbilu.com.com (Apache/1.1) |
Warning | 一个一般性的警告,表示在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning |