本文從屬於筆者的Web前端中DOM系列文章.
同源策略與跨域
同源策略
可謂同源?URL由協議、域名、埠和路徑組成,如果兩個URL的協議、域名和埠相同,則表示他們同源。瀏覽器的同源策略,限制了來自不同源的”document”或指令碼,對當前”document”讀取或設定某些屬性,即從一個域上載入的指令碼不允許訪問另外一個域的文件屬性。比如一個惡意網站的頁面通過iframe嵌入了銀行的登入頁面(二者不同源),如果沒有同源限制,惡意網頁上的javascript指令碼就可以在使用者登入銀行的時候獲取使用者名稱和密碼。所謂道高一尺魔高一丈,雖然瀏覽器以同源策略限制了我們隨意請求資源,但是從這個策略出現開始就有很多各種各樣的Hacker技巧來。
JSONP
JSONP是較為常用的一種跨域方式,不受到瀏覽器相容性的限制,但是因為它只能以GET動詞進行請求,這樣就破壞了標準的REST風格,比較醜陋。JSONP本質上是利用<script>
標籤的跨域能力實現跨域資料的訪問,請求動態生成的JavaScript指令碼同時帶一個callback函式名作為引數。其中callback函式本地文件的JavaScript函式,伺服器端動態生成的指令碼會產生資料,並在程式碼中以產生的資料為引數呼叫 callback函式。當這段指令碼載入到本地文件時,callback函式就被呼叫。
(1)瀏覽器端構造請求地址
1 2 3 4 5 6 7 |
function resolveJson(result) { console.log(result.name); } var jsonpScript= document.createElement("script"); jsonpScript.type = "text/javascript"; jsonpScript.src = "http://www.qiute.com?callbackName=resolveJson"; document.getElementsByTagName("head")[0].appendChild(jsonpScript); |
標準的Script標籤的請求地址為:請求資源的地址+獲取函式的欄位名+回撥函式名稱,這裡的獲取函式的欄位名是需要和服務端提前約定好,譬如jQuery中預設的獲取函式名就是callback
。而resolveJson
是我們預設註冊的回撥函式,注意,該函式名需要全域性唯一,該函式接收服務端返回的資料作為引數,而函式內容就是對於該引數的處理。
(2)服務端構造返回值
在接受到瀏覽器端 script 的請求之後,從url的query的callbackName獲取到回撥函式的名字,例子中是resolveJson
。
然後動態生成一段javascript片段去給這個函式傳入引數執行這個函式。比如:
1 |
resolveJson({name: 'qiutc'}); |
(3)客戶端以指令碼方式執行服務端返回值
服務端返回這個 script 之後,瀏覽器端獲取到 script 資源,然後會立即執行這個 javascript,也就是上面那個片段。這樣就能根據之前寫好的回撥函式處理這些資料了。
CORS:跨域資源共享
跨域資源共享,Cross-Origin Resource Sharing是由W3C提出的一個用於瀏覽器以XMLHttpRequest方式向其他源的伺服器發起請求的規範。不同於JSONP,CORS是以Ajax方式進行跨域請求,需要服務端與客戶端的同時支援。目前CORS在絕大部分現代瀏覽器中都是支援的:
CORS標準定義了一個規範的HTTP Headers來使得瀏覽器與服務端之間可以進行協商來確定某個資源是否可以由其他域的客戶端請求獲得。儘管很多的驗證與鑑權是由服務端完成,但是本質上大部分的檢查和限制還是應該由瀏覽器完成。一般來說CORS會分為Simple Request,簡單請求與Preflight,需要預檢的請求兩大類。其基本的流程如下:
預檢請求
當瀏覽器的請求方式是HEAD、GET或者POST,並且HTTP的頭資訊中不會超出以下欄位:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
時,瀏覽器會將該請求定義為簡單請求,否則就是預檢請求。預檢請求會在正式通訊之前,增加一次HTTP查詢請求。瀏覽器先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些HTTP動詞和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。預檢請求的傳送請求:
1 2 3 4 5 6 7 8 |
OPTIONS /cors HTTP/1.1 Origin: http://api.qiutc.me Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.qiutc.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... |
“預檢”請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭資訊裡面,關鍵欄位是Origin,表示請求來自哪個源。
除了Origin欄位,”預檢”請求的頭資訊包括兩個特殊欄位:
- Access-Control-Request-Method:該欄位是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。
- Access-Control-Request-Headers:該欄位是一個逗號分隔的字串,指定瀏覽器CORS請求會額外傳送的頭資訊欄位,上例是X-Custom-Header。
預檢請求的返回:
1 2 3 4 5 6 7 8 9 10 11 12 |
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.qiutc.me Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain |
- Access-Control-Allow-Methods:必需,它的值是逗號分隔的一個字串,表明伺服器支援的所有跨域請求的方法。注意,返回的是所有支援的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次”預檢”請求。
- Access-Control-Allow-Headers:如果瀏覽器請求包括Access-Control-Request-Headers欄位,則Access-Control-Allow-Headers欄位是必需的。它也是一個逗號分隔的字串,表明伺服器支援的所有頭資訊欄位,不限於瀏覽器在”預檢”中請求的欄位。
- Access-Control-Max-Age:該欄位可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是20天(1728000秒),即允許快取該條回應1728000秒(即20天),在此期間,不用發出另一條預檢請求。
一旦伺服器通過了”預檢”請求,以後每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭資訊欄位。伺服器的回應,也都會有一個Access-Control-Allow-Origin頭資訊欄位。
簡單請求
對於簡單的跨域請求或者通過了預檢的請求,瀏覽器會自動在請求的頭資訊加上Origin
欄位,表示本次請求來自哪個源(協議 + 域名 + 埠),服務端會獲取到這個值,然後判斷是否同意這次請求並返回。典型的請求頭尾:
1 2 3 4 5 6 7 |
// 請求 GET /cors HTTP/1.1 Origin: http://api.qiutc.me Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... |
如果服務端允許,在返回的頭資訊中會多出幾個欄位:
1 2 3 4 5 |
// 返回 Access-Control-Allow-Origin: http://api.qiutc.me Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: Info Content-Type: text/html; charset=utf-8 |
- Access-Control-Allow-Origin:必須。它的值是請求時Origin欄位的值或者
*
,表示接受任意域名的請求。 - Access-Control-Allow-Credentials:可選。它的值是一個布林值,表示是否允許傳送Cookie。預設情況下,Cookie不包括在CORS請求之中。設為true,即表示伺服器明確許可,Cookie可以包含在請求中,一起發給伺服器。
再需要傳送cookie的時候還需要注意要在AJAX請求中開啟withCredentials屬性:var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
需要注意的是,如果要傳送Cookie,Access-Control-Allow-Origin就不能設為*
,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且原網頁程式碼中的document.cookie
也無法讀取伺服器域名下的Cookie。
- Access-Control-Expose-Headers:可選。CORS請求時,XMLHttpRequest物件的
getResponseHeader()
方
法只能拿到6個基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-
Modified、Pragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡面指定。上面的例子指定,getResponseHeader('Info')
可以返回Info欄位的值。
如果服務端拒絕了呼叫,即不會帶上 Access-Control-Allow-Origin
欄位,瀏覽器發現這個跨域請求的返回頭資訊沒有該欄位,就會丟擲一個錯誤,會被 XMLHttpRequest
的 onerror
回撥捕獲到。這種錯誤無法通過 HTTP 狀態碼判斷,因為回應的狀態碼有可能是200。
postMessage
window.postMessage 是一個用於安全的使用跨源通訊的方法。通常,不同頁面上的指令碼當且僅當執行它們的頁面所處的位置使用相同的協議(通常都是 http)、相同的埠(http預設使用80埠)和相同的主機(兩個頁面的 document.domain 的值相同)只在這種情況下被允許互相訪問。 而window.postMessage 提供了一個受控的機制來安全地繞過這一限制。其函式原型如下:
1 |
windowObj.postMessage(message, targetOrigin); |
windowObj
: 接受訊息的 Window 物件。message
: 在最新的瀏覽器中可以是物件。targetOrigin
: 目標的源,*
表示任意。
呼叫postMessage方法的window物件是指要接收訊息的那一個window物件,該方法的第一個引數message為要傳送的訊息,型別只能為字串;第二個引數targetOrigin用來限定接收訊息的那個window物件所在的域,如果不想限定域,可以使用萬用字元 * 。需要接收訊息的window物件,可是通過監聽自身的message事件來獲取傳過來的訊息,訊息內容儲存在該事件物件的data屬性中。上面所說的向其他window物件傳送訊息,其實就是指一個頁面有幾個框架的那種情況,因為每一個框架都有一個window物件。在討論第種方法的時候,我們說過,不同域的框架間是可以獲取到對方的window物件的,雖然沒什麼用,但是有一個方法是可用的-window.postMessage。下面看一個簡單的示例,有兩個頁面:
1 2 3 4 5 6 7 8 9 10 11 12 |
//在主頁面中獲取子頁面的控制程式碼 var iframe =document.getElementById('iframe'); var iframeWindow = iframe.contentWindow; //向子頁面傳送訊息 iframeWindow.postMessage("I'm message from main page."); //在子頁面中監聽獲取訊息 window.onmessage = function(e) { e = e || event; console.log(e.data); } |
Proxy:服務端跨域
使用代理方式跨域更加直接,因為SOP的限制是瀏覽器實現的。如果請求不是從瀏覽器發起的,就不存在跨域問題了。使用本方法跨域步驟如下:
- 把訪問其它域的請求替換為本域的請求
- 本域的請求是伺服器端的動態指令碼負責轉發實際的請求
不過筆者在自己的開發實踐中發現目前服務端跨域還是很有意義的,特別當我們希望從不支援CORS或者JSONP的服務端獲取資料的時候,往往只能通過跨域請求。
Fetch
JavaScript 通過XMLHttpRequest(XHR)來執行非同步請求,這個方式已經存在了很長一段時間。雖說它很有用,但它不是最佳API。它在設計上不符合職責分離原則,將輸入、輸出和用事件來跟蹤的狀態混雜在一個物件裡。而且,基於事件的模型與最近JavaScript流行的Promise以及基於生成器的非同步程式設計模型不太搭。新的 Fetch API打算修正上面提到的那些缺陷。 它向JS中引入和HTTP協議中同樣的原語。具體而言,它引入一個實用的函式 fetch() 用來簡潔捕捉從網路上檢索一個資源的意圖。Fetch 規範 的API明確了使用者代理獲取資源的語義。它結合ServiceWorkers,嘗試達到以下優化:
- 改善離線體驗
- 保持可擴充套件性
而與jQuery
相比, fetch
方法與 jQuery.ajax()
的主要區別在於:
fetch()
方法返回的Promise物件並不會在HTTP狀態碼為404
或者500
的時候自動丟擲異常,而需要使用者進行手動處理- 預設情況下,fetch並不會傳送任何的本地的cookie到服務端,注意,如果服務端依靠Session進行使用者控制的話要預設開啟Cookie
Installation & Polyfill
window.fetch是基於XMLHttpRequest的瀏覽器的統一的封裝,針對老的瀏覽器可以使用Github的這個polypill。fetch基於ES6的Promise,在舊的瀏覽器中首先需要引入Promise的polypill,可以用這個:
1 |
$ bower install es6-promise |
對於fetch的引入,可以用bower或者npm:
1 2 |
$ bower install fetch $ npm install whatwg-fetch --save |
如果是基於Webpack的專案,可以直接在Webpack的config檔案中引入這種polyfill:
1 2 3 4 5 |
plugins: [ new webpack.ProvidePlugin({ 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' }) ] |
這個外掛的配置主要依靠imports-loader
與exports-loader
,因此也需要匯入它們:
1 |
$ npm i imports-loader exports-loader -S |
如果感覺這種方式比較麻煩,也可以使用 isomorphic-fetch:
1 2 |
npm install --save isomorphic-fetch es6-promise bower install --save isomorphic-fetch es6-promise |
使用的時候也非常方便:
1 2 3 4 5 6 7 8 9 10 11 12 |
require('es6-promise').polyfill(); require('isomorphic-fetch'); fetch('//offline-news-api.herokuapp.com/stories') .then(function(response) { if (response.status >= 400) { throw new Error("Bad response from server"); } return response.json(); }) .then(function(stories) { console.log(stories); }); |
從筆者自己的體驗中,還是非常推薦使用isomorphic-fetch,其一大優勢在於能夠在node裡直接進行單元測試與介面可用性測試。老實說筆者之前用Mocha進行帶真實網路請求的測試時還是比較不方便的,往往需要在瀏覽器或者phatomjs中進行,並且需要額外的HTML程式碼。而在筆者的model.test.js檔案中,只需要直接使用babel-node model.test.js
即可以獲取真實的網路請求,這樣可以將網路測試部分與UI相剝離。
Basic Usage:基本使用
假設fetch
已經被掛載到了全域性的window目錄下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Simple response handling fetch('/some/url').then(function(response) { }).catch(function(err) { // Error :( }); // Chaining for more "advanced" handling fetch('/some/url').then(function(response) { return //... }).then(function(returnedValue) { // ... }).catch(function(err) { // Error :( }); |
Request:請求構造
Request物件代表了一次fetch
請求中的請求體部分,你可以自定義Request
物件:
A Request
instance represents the request piece of a fetch
call. By passingfetch
a Request
you can make advanced and customized requests:
method
– 使用的HTTP動詞,GET
,POST
,PUT
,DELETE
,HEAD
url
– 請求地址,URL of the requestheaders
– 關聯的Header物件referrer
– referrermode
– 請求的模式,主要用於跨域設定,cors
,no-cors
,same-origin
credentials
– 是否傳送Cookieomit
,same-origin
redirect
– 收到重定向請求之後的操作,follow
,error
,manual
integrity
– 完整性校驗cache
– 快取模式(default
,reload
,no-cache
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var request = new Request('/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }); // Now use it! fetch(request).then(function() { /* handle response */ }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fetch('/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }).then(function() { /* handle response */ }); |
URI Encode
注意,fetch方法是自動會將URI中的雙引號進行編碼的,如果在URI中存入了部分JSON,有時候會出現意想不到的問題,譬如我們以GET方法訪問如下的URI:
1 |
[GET] http://api.com?requestData={"p":"q"} |
那麼fetch會自動將雙引號編碼,變成:
1 |
[GET] http://api.com?requestData={%22p%22:%22q%22} |
那麼這樣一個請求傳入到Spring MVC中時是會引發錯誤的,即URI物件構造失敗這個很噁心的錯誤。筆者沒有看過原始碼,不過猜想會不會是Spring MVC看到{
這個字元沒有被編碼,因此預設沒有進行解碼,結果沒想到後面的雙引號被編碼了,為了避免這個無厘頭的錯誤,筆者建議是對URI的Query Parameter部分進行統一的URI編碼:
1 2 3 4 |
//將requestData序列化為JSON var requestDataString = encodeURIComponent(JSON.stringify(requestData).replace(/%22/g, "\"")); //將字串連結 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`; |
Headers:自定義請求頭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// Create an empty Headers instance var headers = new Headers(); // Add a few headers headers.append('Content-Type', 'text/plain'); headers.append('X-My-Custom-Header', 'CustomValue'); // Check, get, and set header values headers.has('Content-Type'); // true headers.get('Content-Type'); // "text/plain" headers.set('Content-Type', 'application/json'); // Delete a header headers.delete('X-My-Custom-Header'); // Add initial values var headers = new Headers({ 'Content-Type': 'text/plain', 'X-My-Custom-Header': 'CustomValue' }); |
常見的請求方法有: append
, has
, get
, set
以及 delete
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var request = new Request('/some-url', { headers: new Headers({ 'Content-Type': 'text/plain' }) }); fetch(request).then(function() { /* handle response */ }); |
POST & body:POST請求
1 2 3 4 5 6 7 8 9 10 11 |
fetch('/users', { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Hubot', login: 'hubot', }) }) |
File Upload:檔案上傳
1 2 3 4 5 6 7 8 9 10 |
var input = document.querySelector('input[type="file"]') var data = new FormData() data.append('file', input.files[0]) data.append('user', 'hubot') fetch('/avatars', { method: 'post', body: data }) |
Cookies
如果需要設定fetch
自動地傳送本地的Cookie,需要將credentials設定為same-origin
:
1 2 3 |
fetch('/users', { credentials: 'same-origin' }) |
該選項會以類似於XMLHttpRequest的方式來處理Cookie,否則,可能因為沒有傳送Cookie而導致基於Session的認證出錯。可以將credentials
的值設定為include
來在CORS情況下傳送請求。
1 2 3 |
fetch('https://example.com:1234/users', { credentials: 'include' }) |
Response:響應處理
在fetch
的then
函式中提供了一個Response
物件,即代表著對於服務端返回值的封裝,你也可以在Mock的時候自定義Response物件,譬如在你需要使用Service Workers的情況下,在Response
中,你可以作如下配置:
type
–basic
,cors
url
useFinalURL
– 是否為最終地址status
– 狀態碼 (ex:200
,404
, etc.)ok
– 是否成功響應 (status in the range 200-299)statusText
– status code (ex:OK
)headers
– 響應頭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Create your own response for service worker testing // new Response(BODY, OPTIONS) var response = new Response('.....', { ok: false, status: 404, url: '/' }); // The fetch's `then` gets a Response instance back fetch('/') .then(function(responseObj) { console.log('status: ', responseObj.status); }); |
The Response
also provides the following methods:
clone()
– Creates a clone of a Response object.error()
– Returns a new Response object associated with a network error.redirect()
– Creates a new response with a different URL.arrayBuffer()
– Returns a promise that resolves with an ArrayBuffer.blob()
– Returns a promise that resolves with a Blob.formData()
– Returns a promise that resolves with a FormData object.json()
– Returns a promise that resolves with a JSON object.text()
– Returns a promise that resolves with a USVString (text).
Handling HTTP error statuses:處理HTTP錯誤狀態
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response } else { var error = new Error(response.statusText) error.response = response throw error } } function parseJSON(response) { return response.json() } fetch('/users') .then(checkStatus) .then(parseJSON) .then(function(data) { console.log('request succeeded with JSON response', data) }).catch(function(error) { console.log('request failed', error) }) |
Handling JSON:處理JSON響應
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fetch('https://davidwalsh.name/demo/arsenal.json').then(function(response) { // Convert to JSON return response.json(); }).then(function(j) { // Yay, `j` is a JavaScript object console.log(j); }); |
Handling Basic Text/HTML Response:處理文字響應
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fetch('/next/page') .then(function(response) { return response.text(); }).then(function(text) { // <!DOCTYPE .... console.log(text); }); |
Blob Responses
如果你希望通過fetch方法來載入一些類似於圖片等資源:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fetch('flowers.jpg') .then(function(response) { return response.blob(); }) .then(function(imageBlob) { document.querySelector('img').src = URL.createObjectURL(imageBlob); }); |
blob()
方法會接入一個響應流並且一直讀入到結束。
Best Practice
筆者在自己的專案中封裝了一個基於ES6 Class的基本的模型請求類,程式碼地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
/** * Created by apple on 16/5/3. */ //自動進行全域性的ES6 Promise的Polyfill require('es6-promise').polyfill(); require('isomorphic-fetch'); /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 基礎的模型類,包含了基本的URL定義 */ export default class Model { //預設的基本URL路徑 static BASE_URL = "/"; //預設的請求頭 static headers = {}; /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 預設建構函式 */ constructor() { this._checkStatus = this._checkStatus.bind(this); this._parseJSON = this._parseJSON.bind(this); this._parseText = this._parseText.bind(this); this._fetchWithCORS = this._fetchWithCORS.bind(this); } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 檢測返回值的狀態 * @param response * @returns {*} */ _checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response } else { var error = new Error(response.statusText); error.response = response; throw error } } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 解析返回值中的Response為JSON形式 * @param response * @returns {*} */ _parseJSON(response) { if (!!response) { return response.json(); } else { return undefined; } } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 解析TEXT性質的返回 * @param response * @returns {*} */ _parseText(response) { if (!!response) { return response.text(); } else { return undefined; } } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 封裝好的跨域請求的方法 * @param packagedRequestURL * @returns {*|Promise.<TResult>} * <a href='http://www.jobbole.com/members/kaishu6296'>@private</a> */ _fetchWithCORS(packagedRequestURL, contentType) { return fetch(packagedRequestURL, { mode: "cors", headers: Model.headers }) .then(this.checkStatus, (error)=> { return error; }) .then(contentType === "json" ? this._parseJSON : this._parseText, (error)=> { return error; }); } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 利用get方法發起請求 * @param path 請求的路徑(包括路徑引數) * @param requestData 請求的引數 * @param action 請求的型別 * @param contentType 返回的型別 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ get({BASE_URL=Model.BASE_URL, path="/", action="GET", contentType="json"}) { //封裝最終待請求的字串 const packagedRequestURL = `${BASE_URL}${(path)}?action=${action}`; //以CORS方式發起請求 return this._fetchWithCORS(packagedRequestURL, contentType); } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 利用get方法與封裝好的QueryParams形式發起請求 * @param path 請求的路徑(包括路徑引數) * @param requestData 請求的引數 * @param action 請求的型別 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ getWithQueryParams({BASE_URL=Model.BASE_URL, path="/", queryParams={}, action="GET", contentType="json"}) { //初始化查詢字串 let queryString = ""; //根據queryParams構造查詢字串 for (let key in queryParams) { //注意,請求引數必須進行URI格式編碼,如果是JSON等特殊格式需要在服務端進行解碼 queryString += `${key}=${encodeURIComponent(queryParams[key])}&`; } //將查詢字串進行編碼 let encodedQueryString = (queryString); //封裝最終待請求的字串 const packagedRequestURL = `${BASE_URL}${path}?${encodedQueryString}action=${action}`; //以CORS方式發起請求 return this._fetchWithCORS(packagedRequestURL, contentType); } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 利用get方法與封裝好的RequestData形式發起請求 * @param path 請求的路徑(包括路徑引數) * @param requestData 請求的引數 * @param action 請求的型別 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ getWithRequestData({path="/", requestData={}, action="GET", contentType="json"}) { //將requestData序列化為JSON //注意要對序列化後的資料進行URI編碼 var requestDataString = encodeURIComponent(JSON.stringify(requestData)); //將字串連結 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`; return this._fetchWithCORS(packagedRequestURL, contentType); } /** * <a href='http://www.jobbole.com/members/cm00000000mol'>@function</a> 考慮到未來post會有不同的請求方式,因此做區分處理 * @param path * @param requestData * @param action * @returns {Promise.<TResult>|*} */ postWithRequestData({path="/", requestData={}, action="POST", contentType="json"}) { //將requestData序列化為JSON //注意要對序列化後的資料進行URI編碼 var requestDataString = encodeURIComponent(JSON.stringify(requestData)); //將字串連結 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`; return this._fetchWithCORS(packagedRequestURL, contentType); } put({path="/", requestData={}, action="put", contentType="json"}) { } delete({path="/", requestData={}, action="DELETE", contentType="json"}) { } } Model.testData = {}; Model.testData.error = {}; |