# 面試系列文 - 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 (埠號,如有指定) ![](https://i.imgur.com/jd35Hrt.png) MDN的例子: ![](https://i.imgur.com/ITjlv5X.png) 簡單來說,不同 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`