Fetch漫遊指南--篇1

一.什麼是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參數,這是爲什麼?我們再看一張截圖:
在這裏插入圖片描述

和上面的圖做對比,我們能很明顯的看出兩個請求的請求頭的不同:

  1. content-type的不同。
  2. 發送參數出現在的項不同(一個是Form Data,一個是Request Payload)

很顯然,問題就出在這。後臺對Request Payload格式的post數據去解析的方式是不同的,解決方案也很簡單,就是加上app.use(bodyParser.json());這一句代碼就行,在本文中我爲了方便,事先屏蔽了。

那麼Form DataRequest 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個過程:

  1. createObjectURL.
  2. 加載img.
  3. revokeObjectURL以釋放引用 .
  4. 失去了img ref.
  5. 爲下一個刪除的圖像重複所有步驟.

所以,最後那步的對象回收是很有必要的,它能保證瀏覽器的內存回收機制正常運行

這裏藏着一個特有意思的事兒,那就是通過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)
            })
    })

可以看到,和一般的上傳模式不同,主要有兩點:

  1. 不需要<form></form>標籤包裹。
  2. 不需要額外指定content-type

後臺處理文件上傳的代碼在最開始已經貼出來了,有興趣的同學可以去看看。

其他兩個個json(), text()方法我在這就不給大家介紹了,因爲比較簡單,理解起來也沒什麼難度,大家可以自行去MDN上查看,在文章的結尾我會掛上MDN的參考鏈接。

在《Fetch漫遊指南——篇1》這篇文章中我講了大量延伸內容,爲了文章的可讀性,我覺得這篇文章講到這裏來作爲篇一的收尾已經足夠了,在 《Fetch漫遊指南——篇2》中,我將會介紹以下內容:

  • Fetch Response對象
  • 自定義request對象
  • Fetch的異常捕捉以及封裝
  • Fetch的兼容方案以及實操(可不是簡單的把hack方案掛出來哦,博主會詳細的把操作過程中碰到的問題一一講解)

四.參考文章/文獻

1.【MDN】 使用 Fetch
2.【MDN】 JavaScript 標準庫
3.【MDN】 Web API 接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章