# 面試系列文 - Ajax / 同源問題及解法
## 前言
目前大多是動態網站,透過互動的方式,讓使用者的體驗更好。而隨著動態網站興起,開始會將前後端分開,讓前後端各自著墨自己的領域。
此次要說明前端和後端交換資料(API),Ajax的興起,隨之造成的同源問題,以及該如何解決同源。
## 目標
希望在閱讀本篇後,理解
* API
* Ajax
* 一點 Same-Origin Policy
* 一點 CORS
* JSONP
* Fetch()
## API
### 定義
全名Application Programming Interface,中文翻作應用程式**介面**。
介面就是用來串接用的,而 API 就是在一套規範下,**程式跟程式之間的串接**。
例如 : Facebook 登入,利用Facebook 提供 的 API,等於說是 Facebook 向外提供給大家的一套介面、一套標準,任何想要接入 Facebook 服務的開發者們,都可以遵循著那套規範拿到自己想要的資料,這個東西就叫做 API。
### 怎麼串呢?
API 的串接,一定要有文件你才知道怎麼串,不然根本串不起來,因為你連要傳什麼參數都不知道。
就如 : [Facebook 登入](https://developers.facebook.com/?locale=zh_TW),你必須先看他的官方**標準**(通常都有範例)。在標準下做自己程式與Facebook程式串接,這裡應該說Facebook資料和自己資料庫串接,在標準下去客製化設定。
## 怎麼利用程式去發送 Request ?
> Client 會發 request 給 server,server 會 response 給 Client 。
知道這套標準,知道怎麼拿資料後,該怎麼利用自己撰寫的程式(JS)去發送 Reuqest ?
1. 傳統JS串法 - XMLHttpRequest 物件
3. 使用 request library
4. Ajax
### 使用 request library
npm 完 request 這套 [library](https://github.com/request/request),直接以例子帶入,
```
const request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});
```
1. 宣告一個變數來引入 request 之後
1. request 的第一個參數是要串接 API 的 url,第二個參數是箭頭函式,可以取得 response, error, statusCode 和 body 等資料都放在這裡。
2. 可以用Promise、async/await方式達到非同步
## Ajax
### 定義
全名 Asynchronous JavaScript And XML,中文 非同步的JavaScript與XML技術。
以前兩種方式來說,每次傳送資料之後都要換頁面。但很多時候,只是需要從 server 拿資料而已,就只需要畫面**部分有改變**而已,不需要整個畫面都變動。這個時候我們可以利用 JavaScript 發一個 request 到 server,然後得到我們要的資料,就只要改變部分畫面就好了。
### 如何實作?
**非同步**,當JS一行行向下執行時,執行完之後就不管它,繼續執行下一行。所以非同步的 Function 不能直接透過 return 把結果傳回來,因為發送 Request 之後就會執行到下一行,這個時候可能根本就還沒有 Response。所以必須透過Callback Function,回呼函式。
```
// 假設有個發送 Request 的函式叫做 sendRequest
var result = sendRequest('https://api.twitch.tv/kraken/games/top?client_id=xxx');
// 上面 Request 發送完之後就執行到這一行,所以 result 不會有東西
// 因為 Response 根本沒有回來
console.log(result);
```
```
// 假設有個發送 Request 的函式叫做 sendRequest
sendRequest('https://api.twitch.tv/kraken/games/top?client_id=xxx', callMe);
function callMe (response) {
console.log(response);
}
// 或者寫成匿名函式
sendRequest('https://api.twitch.tv/kraken/games/top?client_id=xxx', function (response) {
console.log(response);
});
```
## 串接後。災難開始
自己處理前後端都還好,但...串接別的 API ,出現 error!!!!!!
```
XMLHttpRequest cannot load
http://odata.tn.edu.tw/ebookapi/api/getOdataJH/?level=all.
No 'Access-Control-Allow-Origin' header is present on the
requested resource. Origin 'null' is therefore not allowed access.
```
從 error 關鍵字得知,No 'Access-Control-Allow-Origin'。
## Same Origin Policy
同源政策 基於網絡安全的考量,避免有駭客惡意呼叫其他人的網絡服務。若沒有這個政策保護,別人就可以任意修改和存取你網頁裏的資源。
意思就是說如果你現在這個網站的跟你要呼叫的 API 的網站「不同源」的時候,瀏覽器一樣會幫你發 Request,但是會把 Response 給擋下來,不讓你的 JavaScript 拿到並且傳回錯誤。
#### 如何直接判斷同源??
判斷是否同源,就看這兩個網址在以下的部分是否相同:
* scheme (通訊協定,比如:http, https是不一樣的!)
* domain
* port (埠號,如有指定)

MDN的例子:

簡單來說,不同 domain 就是不同源,http和 https 就是不同源,port 不同就是不同源。當我們接別人的 API 時,多數就是不同源的情況。
要注意一點:**我的請求(request)的確是有發出去,我的瀏覽器之後也收到回應(response)**。但因瀏覽器的同源政策,它把回應擋下來了,不會把拿到的回應給JavaScript去做另一些的處理。
### 同源政策並非完全禁止跨來源存取
但在某些情況下,即使兩個網站是「不同源」,也可以允許存取的。例如以下情況:
* 跨來源寫入(Cross-origin writes)
例如允許:表單送出(form)、連結(link)、重新導向(redirect)
* 跨來源嵌入(Cross-origin embedding)
例如允許:嵌入圖片`<img>`、影片`<video>`、`<iframe>`、放在`<script>`裏的程式碼、CSS stylesheet `<link rel="stylesheet" href="...">`等等。然而,雖然我的**網頁可以顯示到這些資源,但我的JavaScript並不能讀取這些資源的內容**。
## 解決方案
### CORS
全名 Cross-Origin-Resource Sharing,中文 : 跨來源資源共用。
如果你想開啟跨來源 HTTP 請求的話,Server 必須在 Response 的 Header 裡面加上Access-Control-Allow-Origin。
瀏覽器收到 Response 之後,會先檢查Access-Control-Allow-Origin裡面的內容,如果裡面有包含現在這個發起 Request 的 Origin 的話,就會允許通過,讓程式順利接收到 Response。
```
Content-Type: application/json
Content-Length: 71
Connection: keep-alive
Server: nginx
Access-Control-Allow-Origin: * //任何一個 Origin 都接受
Cache-Control: no-cache, no-store, must-revalidate, private
Expires: 0
Pragma: no-cache
Twitch-Trace-Id: e316ddcf2fa38a659fa95af9012c9358
X-Ctxlog-Logid: 1-5920052c-446a91950e3abed21a360bd5
Timing-Allow-Origin: https://www.twitch.tv
```
* Simple request 簡單請求
沒有自訂義、Get,直接發出請求
* Preflight Request 預檢請求
自定義(帶有一些關於想發的請求的一些資訊,如:POST、`Authorization`)、client-id=...,發出兩個請求,
發出請求前的一個「預檢請求」,這個預檢請求是負責查問伺服器,問它是否批准我們發出請求給它,批准後才發出該請求。
### 舉例說明
假設今天某個 Server 提供了一個 API 網址叫做:`https://example.com/data/16`,你只要對它發送 `GET`,就能夠拿到 id 是 16 的資料,只要對它發送 `DELETE`,就可以把這筆資料刪除。
如果今天沒有 Preflight Request 這個機制的話,就可以在隨便一個 Domain 的網頁上面發送一個 DELETE 的 Request 給這個 API。而瀏覽器的 CORS 機制,會幫你發送 Request,但是 Response 會被瀏覽器擋住。
因此呢,儘管沒有 Response,但是 Server 端的確收到了這個 Request,因此就會把這筆資料給**刪除**。
如果有 Preflight Request 的話,在發送出去收到結果的時候,就會知道這個 API 並沒有提供 CORS,因此真的 DELETE 請求就不會送出,到這邊就結束了。
意思就是先用一個 OPTIONS 的請求去確認之後的 Request 能不能送出,這就是 Preflight Request 的目的。
### JSONP
全名 JSON with Padding,是資料格式JSON的一種“使用模式”。
### JSONP 是什麼?
JSONP的做法就是,在一個 `<script>` tag 裏的放入伺服器端提供的網址,之後在另一個`<script>` tag 裏宣告一個函式,函式名字是由伺服器端提供,也可以在伺服器端所提供的網址裏找到,例如它提供了 `https://...callback=abc` 這個網址,那麼該函式的名字就是 `abc` 。
```
<script>
function randomuserdata(response){
console.log(response);
}
</script>
<script src="https://randomuser.me/api/?gender=female&nat=us&callback=randomuserdata"></script>
```
JSONP 利用 `<script>` 裡面放資料,透過指定好的 Callback Function 把資料給帶回去。
把第一段的 `<script>` 那邊想成是 Server 的回傳值,Server 通常會提供一個 callback 的參數讓 client 端帶過去,把 JavaScript 物件整個傳到 Function 裡面,你就可以在 Function 裡面拿到資料。。
```
<script>
receiveData({
data: 'test'
});
</script>
<script>
function receiveData (response) {
console.log(response);
}
</script>
```
不過 JSONP 只適用於 GET 請求(要帶的那些參數永遠都只能用附加在網址上的方式 GET 帶過去),無法做到 POST,所以首選還是上面提及的 CORS 的方法。
### Fetch API
The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible feature set.
類似於 jQuery $.ajax ,不過兩者亦有不同概念之處。
* fetch 會使用 ES6 的 Promise 作回應
* then 作為下一步
* catch 作為錯誤回應 (404, 500…)
```
fetch('https://randomuser.me/api/', {})
.then((response) => {
// 這裡會得到一個 ReadableStream 的物件
console.log(response);
// 可以透過 blob(), json(), text() 轉成可用的資訊
return response.json();
}).then((jsonData) => {
console.log(jsonData);
}).catch((err) => {
console.log('錯誤:', err);
});
```
* ReadableStream
Fetch API 的 Response 物件中的 body 屬性提供了一個 ReadableStream 的實體,這個階段我們無法直接讀取資料內容,而 ReadableStream 物件中可用以下對應的方法來取得資料,如 : arrayBuffer()、blob()、formData()、json()、text()
## 參考資料
* [輕鬆理解 Ajax 與跨來源請求](https://blog.huli.tw/2017/08/27/ajax-and-cors/)
* [JavaScript基本功修練:Day28 - Fetch練習(GET和POST請求)](https://ithelp.ithome.com.tw/articles/10252941)
## 後記
以往在學習技能時,習慣不斷輸入,而忽視了輸出,導致對於某項技術明明知道,卻又無法清楚地和別人介紹,在面試期間深深感到無力,決心好好消化之前的技術筆記,學習重新輸出。
主要針對相關性技術或是名詞作介紹,偶爾可能會看到之前的專案心得,歡迎一起討論學習,雙手合十,感恩。
###### tags: `面試大哉問`、`JavaScript` 、 `CORS` 、`WEB`