一.什麼是Ftech
“Fetch API提供了一個JavaScript接口,用於訪問和操縱HTTP管道的部分,例如請求和響應。它還提供了一個全局 fetch()方法,該方法提供了一種簡單,合理的方式來跨網絡異步獲取資源。”
簡單來說,在前一階段,我們通常是使用ajax之類的基於XMLHttpRequest來進行發送各種http網絡請求,但是由於XHR其自身的侷限性,導致在其基礎上封裝的原生ajax,或是jquery ajax在碰到回調地獄時都會有心無力。爲了解決這一問題,也是爲了更好擁抱ES6,W3C發佈了fetch這一更底層的API(此處有爭議,官方認爲fetch更底層,更開放,但部分人卻認爲fetch和HTML5的拖拽API一樣浮於表面)。
請注意,fetch規範與jQuery.ajax()主要有兩種方式的不同,牢記:
- 當接收到一個代表錯誤的 HTTP 狀態碼時,從 fetch()返回的 Promise 不會被標記爲 reject, 即使該 HTTP 響應的狀態碼是 404 或 500。相反,它會將 Promise 狀態標記爲 resolve (但是會將resolve的返回值的ok屬性設置爲false),僅當網絡故障時或請求被阻止時,纔會標記爲 reject。
- 默認情況下,fetch 不會從服務端發送或接收任何 cookies, 如果站點依賴於用戶 session,則會導致未經認證的請求(要發送 cookies,必須設置 credentials 選項)。
二.開啓Fetch漫遊之旅
事先說明一下,本文中服務器由node express提供,如果想運行的話大家可以先把node引擎和npm裝起來,該install的依賴install就行了。代碼先貼出來把:
const express = require('express');
const bodyParser = require('body-parser');
const static = require('express-static');
const consolidate = require('consolidate');
const path = require('path');
const multer = require('multer');
const fs = require('fs');
var app = express();
//配置模板引擎
app.set("view engine", "html");
app.set("views", path.join(__dirname, "views"));
app.engine("html", consolidate.ejs);
//轉發靜態資源
app.use("/static", static(path.join(__dirname, "public")));
//解析 request payload數據
app.use(bodyParser.json());
//解析post數據 from-data數據
app.use(
bodyParser.urlencoded({
extended: false
})
);
//解析post文件
var objMulter = multer({dest:'./public/upload'})
app.use(objMulter.any());
app.get('/', (req, res) => {
res.render('fetch.ejs');
})
app.get('/get', (req, res) => {
res.send('get success');
})
app.post('/post', (req, res) => {
//console.log(req)
res.send(req.body);
})
app.post('/ajax/post', (req, res) => {
res.send(req.body);
})
app.use('/file', (req, res) => {
var file = req.files[0];
var old_name = file.path;
var ext_name = path.parse(file.originalname).ext;
var new_name = old_name + ext_name;
fs.rename(old_name, new_name, (err) => {
if(err){
res.status(500).send("error");
}else{
res.status(200).send("success");
}
})
})
var server = app.listen('7008', (req, res) => {
console.log('run success in port:' + server.address().port)
})
我們先來看一個最簡單的fetch get實例:
fetch('/get', {
method: 'get'
}).then(res => res.text())
.then(res => {
console.log(res); // get success
})
get實例很簡單對吧?我們可以再來看看POST實例,從這開始,就會有趣了:
var data = { 'name': 'waw', 'password': 19 }
fetch('/post', {
method: 'post',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: JSON.stringify(data)
}).then(res => res.text())
.then(res => {
console.log(res); // {}
})
可以看到,打印出來是個空的。誒,我發過去的json數據呢??
我們看下圖:
光看這個圖,json參數是發過去了,但是後臺拿不到啊,所以導致後臺返回的也是個空對象。光看這個例子,可能大家還沒什麼感覺,我再來一個常規的:
var post_data = { 'name': 'waw2', 'password': 16 };
$.ajax({
url: '/ajax/post',
type: 'post',
dataType: 'json',
data: {
'data': JSON.stringify(post_data)
},
success: json => {
console.log(json.staus); //{ 'name': 'waw2', 'password': 16 }
}
})
(PS:以上代碼中使用的是jquery ajax,想跑的情自行引用文件,伸手黨請自重)
可以看到,這時我們拿到了後臺返回的,也就是我們發過去的json參數,這是爲什麼?我們再看一張截圖:
和上面的圖做對比,我們能很明顯的看出兩個請求的請求頭的不同:
content-type
的不同。- 發送參數出現在的項不同(一個是
Form Data
,一個是Request Payload
)
很顯然,問題就出在這。後臺對Request Payload
格式的post數據去解析的方式是不同的,解決方案也很簡單,就是加上app.use(bodyParser.json());
這一句代碼就行,在本文中我爲了方便,事先屏蔽了。
那麼Form Data
和Request Payload
是什麼?它們由什麼來決定?大家可以看看我的這篇文章:關於HTTP中formData和Request Payload的區別。好,我們言歸正傳,接着來看fetch的實例。接下來我們看看文件提交:
HTML Dom部分:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Fetch</title>
</head>
<body>
<input type="file" id="File">
<button id="subBtn">提交表單數據</button>
</body>
</html>
JS部分:
$("#subBtn").on('click', function () {
var fomData = new FormData();
var file = document.getElementById('File');
fomData.append('file', file.files[0]);
fetch("/file", {
method: 'put',
body: fomData,
}).then(res => res.json())
.then(res => {
console.log('file', res)
})
})
後臺對應的文件上傳部分,在上面貼出來的代碼中有,我就不贅述了。
fetch裏的文件上傳相對於傳統的文件上傳有一個特點,首先可以看到並不需要<form></form>
標籤來包裹,自然也不需要設置enctype
。更簡潔,也更符合“儘量讓js控制js事件操作”的原則。
三.Fetch的特性
1.Headers接口
一般來說,我們使用 Headers() 構造函數來創建一個 headers 對象。也就是說,我們可以提前創建好一個 headers 對象,然後往裏面塞入我們想要添加的屬性規則,由於它是一個變量,所以方便我們自由的藉助此特性封裝基於fetch的ajax庫。
我偷個懶,就不自己敲了,給大家放上MDN上的demo,很詳細。
2.Body對象
fetch-body對象中提供了5個方法供轉化數據格式使用,分別是:
- arrayBuffer()
- blob()
- json()
- text()
- formData()
下面我來簡單介紹一下這幾個方法的作用:
1.arrayBuffer()
arrayBuffer() 接受一個 Response 流, 並等待其讀取完成. 它返回一個 promise 實例, 並 resolve 一個 ArrayBuffer 對象.
以上是MDN給出的解釋,簡單來說呢,arrayBuffer()
方法是用來處理後臺發過來的二進制文件流的,語法如下:
response.arrayBuffer().then(function(buffer) {
// do something with buffer
)};
關於arrayBuffer()
方法,就不得不提ArrayBuffer對象:
ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區。ArrayBuffer 不能直接操作,而是要通過類型數組對象或DataView對象來操作,它們會將緩衝區中的數據表示爲特定的格式,並通過這些格式來讀寫緩衝區的內容。
簡單來說,ES6提供了一個新的數據類型ArrayBuffer
,讓我們可以操作二進制流的數據;這個接口主要的目的是爲WebGL服務,通過ArrayBuffer
來開啓瀏覽器與顯卡之間的通信,並加快計算的速度。
ArrayBuffer
對象並不能直接來操作,而是需要類型數組對象或DataView對象這兩個東西。類型數組對象是什麼呢?
1.1 類型數組對象
一個 TypedArray 對象描述一個底層的二進制數據緩存區的一個類似數組(array-like)視圖。事實上,沒有名爲TypedArray的全局對象,也沒有一個名爲的 TypedArray構造函數。
MDN上的解釋有些晦澀,我們稱類型數組對象爲TypedArray對象,但是事實上又沒有這個對象,出現這個情況的原因就是它並不是一個實際的接口或者函數,而是一個類或一類方法的泛指,實際上來說,TypedArray對象是指以下這些方法中的一個:
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
用法的話,就類似下面這個例子:
const typedArray1 = new Int8Array(8);
typedArray1[0] = 32;
console.log(typedArray1) //Int8Array [32, 0, 0, 0, 0, 0, 0, 0]
1.2 DataView對象
DataView 視圖是一個可以從 ArrayBuffer 對象中讀寫多種數值類型的底層接口,在讀寫時不用考慮平臺字節序問題¹。
注1:平臺字節序問題是由於不同平臺的字節順序不同產生的;這是計算機領域由來已久的問題之一,在各種計算機體系結構中,由於對於字,字節等存儲機制有所不同,通信雙方交流的信息單元(比特、字節、字、雙字等)應該以什麼樣的順序進行傳送就成了一個問題,所以需要一個統一的規則。
使用語法如下:
new DataView(buffer [, byteOffset [, byteLength]])
參數說明:
- buffer:一個 ArrayBuffer 或 SharedArrayBuffer 對象,DataView 對象的數據源。
- byteOffset(可選):此DataView對象的第一個字節在buffer中的偏移。如果不指定則默認從第一個字節開始。
- byteLength(可選):此 DataView 對象的字節長度。如果不指定則默認與 buffer 的長度相同。
返回值:
一個由 buffer 生成的 DataView 對象。
異常:
RangeError:如果由偏移(byteOffset)和字節長度(byteLength)計算得到的結束位置超出了 buffer 的長度,拋出此異常。
示例如下:
var buffer = new ArrayBuffer(16);
// Create a couple of views
var view1 = new DataView(buffer);
var view2 = new DataView(buffer,12,4); //from byte 12 for the next 4 bytes
view1.setInt8(12, 42); // put 42 in slot 12
console.log(view2.getInt8(0)); //42
console.log(view1.getInt8(12)); //42
2.blog()
通常來說,如果你需要去請求一個圖片來加載,你可以使用blog()
函數來獲取url,並賦值到你Dom層的img標籤中。
Dom:
<body>
<img src="" alt="">
</body>
js:
var myImage = document.querySelector('img');
var myRequest = new Request('/static/image/test.jpg');
fetch(myRequest)
.then(function (response) {
return response.blob();
})
.then(function (myBlob) {
var objectURL = URL.createObjectURL(myBlob); //myBlob typeof object
myImage.src = objectURL;
myImage.onload = function () {
window.URL.revokeObjectURL(objectUrl);
};
});
我簡單解釋一下,blog()
函數將後臺發過來的文件流讀取到,然後通過URL.createObjectURL()
函數從這個blob對象中獲取到對應的url,最後講此url賦值給img的src,再img加載完成後,通過URL.revokeObjectURL()
來回收之前那個url,以解除瀏覽器對它的引用。要注意的是,通常來說,不管你加載多少張圖片,瀏覽器的機制是隻會同時保持對一張圖片的引用,這意味着任何一張圖片的加載都會經歷以下這5個過程:
- createObjectURL.
- 加載img.
- revokeObjectURL以釋放引用 .
- 失去了img ref.
- 爲下一個刪除的圖像重複所有步驟.
所以,最後那步的對象回收是很有必要的,它能保證瀏覽器的內存回收機制正常運行。
這裏藏着一個特有意思的事兒,那就是通過fetch-blob()
獲取到的圖片,和直接通過服務器資源路徑獲取到的圖片有什麼區別嗎?
爲了解決這個困惑,我加了一個普通demo,以和上面的請求方式區別開來:
這是普通img的加載模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Audio</title>
</head>
<body>
<img src="/static/image/test.jpg" alt="">
</body>
</html>
這是fetch-blob()
下img的加載模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>fetch-blob()</title>
</head>
<body>
<img src="" alt="">
</body>
</html>
<script>
var myImage = document.querySelector('img');
var myRequest = new Request('/static/image/test.jpg');
fetch(myRequest)
.then(function (response) {
return response.blob();
})
.then(function (myBlob) {
var objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
myImage.onload = function () {
window.URL.revokeObjectURL(objectUrl);
};
});
</script>
以下是這兩種模式下的請求圖片的區別:
普通模式:
fetch-blob
模式:
從圖中我們可以看到,blob模式相比普通模式,多請求了一個本地文件,此文件的請求路徑,其實也就是我們通過URL.createObjectURL()
生成的url,如下圖:
**還有一點區別需要注意:**普通模式下的img加載,如果你在不設置強強緩存機制的情況下多刷新幾次,你就會發現這個圖片的請求變成了304(from memory cache),也就是不再從服務端加載了。但是如果是fetch-blob
模式的加載,將會一直200,也就是從始至終它都會從服務端加載。
兩種模式的好壞我不做評價,畢竟能解決實際的業務場景的問題的技術就是好技術。
講完了blob()
函數,我又不得不講講blob對象了(無奈臉)。畢竟blob對象纔是支撐fetch body.blob()函數的基建。
我們先來看看什麼是blob對象:
Blob 對象表示一個不可變、原始數據的類文件對象。Blob表示的不一定是JavaScript原生格式的數據。File接口基於Blob,繼承了blob的功能並將其擴展使其支持用戶系統上的文件。
我們看一個示例:
var aFileParts = ['<a id="a"><b id="b">hey!</b></a>']; // 一個包含DOMString的數組
var oMyBlob = new Blob(aFileParts, {type : 'text/html'}); // 得到 blob
var reader = new FileReader();
reader.addEventListener("loadend", function(res) {
console.log(res)
});
reader.readAsArrayBuffer(oMyBlob);
注意:如果你在vscode下運行這段代碼,時候報錯的:ReferenceError: Blob is not defined,所以你只能把它運行在瀏覽器環境下。
blog格式的數據對象,一般通過FileReader來讀取。
3.formData()
Body 對象中的formData()方法將Response對象中的所承載的數據流讀取並封裝成爲一個對象,該方法將返回一個 Promise 對象,該對象將產生一個FormData 對象。
一般來說,我們使用formData()
來格式化文件流數據。
我們看一個示例:
Dom層:
<body>
<input type="file" id="File">
<button id="subBtn">上傳</button>
</body>
js層:
$("#subBtn").on('click', function () {
var fomData = new FormData();
var file = document.getElementById('File');
fomData.append('file', file.files[0]);
fetch("/file", {
method: 'put',
body: fomData,
}).then(res => res.json())
.then(res => {
console.log('file', res)
})
})
可以看到,和一般的上傳模式不同,主要有兩點:
- 不需要
<form></form>
標籤包裹。 - 不需要額外指定
content-type
。
後臺處理文件上傳的代碼在最開始已經貼出來了,有興趣的同學可以去看看。
其他兩個個json(), text()
方法我在這就不給大家介紹了,因爲比較簡單,理解起來也沒什麼難度,大家可以自行去MDN上查看,在文章的結尾我會掛上MDN的參考鏈接。
在《Fetch漫遊指南——篇1》這篇文章中我講了大量延伸內容,爲了文章的可讀性,我覺得這篇文章講到這裏來作爲篇一的收尾已經足夠了,在 《Fetch漫遊指南——篇2》中,我將會介紹以下內容:
- Fetch Response對象
- 自定義request對象
- Fetch的異常捕捉以及封裝
- Fetch的兼容方案以及實操(可不是簡單的把hack方案掛出來哦,博主會詳細的把操作過程中碰到的問題一一講解)