提及跨域的解決方案,老是會說到 JSONP,可是不少時候都沒有仔細去了解過 JSONP,多是由於如今 JSONP 用的不是不少(多數時候都是配置響應頭實現跨域),也多是由於用 JSONP 的場景通常都是用 jQuery 來實現,因此對 JSONP 知之甚少。html
JSONP 的本意是 JSON with Padding,即填充式 JSON。爲何叫填充式呢?由於服務端不會直接返回 JSON 格式數據給客戶端,它會拼接成一個字符串,這個字符串被拿到客戶端執行。這是對於 JSON 的一種應用。前端
發明 JSONP 的老頭子們發現雖然同源策略(CORS)限制了 ajax 對於其它服務器的訪問,可是並不能限制 HTML 的資源請求。git
好比在 HTML 中,img、script、link 等標籤徹底能夠訪問任何地址的資源。而其中的 script 標籤爲跨域請求提供了一種新的思路,由於 script 請求的是一段可執行的 JavaScript 代碼。咱們能夠把以前直接從服務器返回的數據封裝到 JavaScript 代碼中,而後在前端再使用這些數據。這就是 JSONP 的實現原理。github
使用 jQuery 的 $.ajax
進行 JSONP 請求時,type 屬性老是選擇爲 GET
,若是填爲 POST
,就會報錯,這是爲何呢?其實理解 JSONP 的原理以後,這就很好理解了。緣由就是 script 標籤的資源請求只能是 GET
類型,目前爲止我尚未見過 POST 類型的資源請求~ajax
前端小白不理解 JSONP 的另外一個緣由就在於 JSONP 不僅是前端這一塊的任務,只靠前端是沒法實現的,後端也必須作相應處理。數據庫
前端請求一個專用的接口獲取數據,請求的數據會以 JavaScript 代碼的形式返回,假如數據以下:json
{ "name": "russ", "age": 20 }
那要構形成什麼樣的 JavaScript 代碼才能被前端使用到呢?最容易想到的就是把數據賦值給一個全局變量,或者把數據扔到函數裏面。扔給全局變量的話會致使一些問題(全局暴露、命名衝突、數據處理邏輯分散……),因此把數據扔給一個專門處理數據的函數比較合適。這也是 JSONP 所採用的方案。因此拼接出來的字符串(也是後端返回的 js 代碼)基本以下:後端
callback({ "name": "russ", "age": 20 })
那麼前端就須要在 script 請求返回以前定義好 callback 這個函數,以便在 script 返回以後能夠順利加載執行。這裏須要先後端約定好回調函數的名稱。固然能夠前端傳遞迴調的名稱給後端,後端根據前端傳遞的名稱進行 JavaScript 代碼拼接,jQuery.ajax 就有這種實現,容許前端自定義回調的名稱。api
先講講後端實現,由於後端實現起來比較簡單~跨域
如下實現均使用 Nodejs。
Nodejs 實現代碼以下:
const http = require('http'); const { parse } = require('url'); // 假設這是在數據庫中找到的數據~ const data = { name: 'russ', age: 20, gender: 'male' }; const server = http.createServer((req, res) => { const url = parse(req.url, true); // 解析 url // 只有路由爲 `/user` 的 GET 請求會被響應 if (req.method === 'GET' && url.pathname === '/user') { const { callback } = url.query; // 獲取 callback 回調名稱 if (callback) // 若是傳遞了 callback 參數,說明是 JSONP 請求 return res.end(`${callback}(${JSON.stringify(data)})`); else // 沒傳遞 callback,直接當作不跨域的 GET 請求返回數據 return res.end(JSON.stringify(data)); } return res.end('Not Found'); // 不匹配的路由返回錯誤 }); server.listen(3000, () => { console.log('Server listening at port 3000...'); });
能夠直接在瀏覽器中請求查看結果~
伴隨着不少功能強大的 api 的出現,咱們在不少場景下均可以直接棄用 jQuery(參考nefe/You-Dont-Need-jQuery)。而若是咱們的頁面沒有使用 jQuery 的時候,咱們就須要手動實現 JSONP 了~
前面說過 JSONP 的原理是 script 標籤的資源請求,因此前端的處理就是構造 script 標籤發起請求。
<!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>JSONP</title> </head> <body> <!-- 點擊的時候調用 fetchJSON --> <button onclick="fetchJSON()">Fetch</button> </body> <script> // 定義了 fetchJSON 函數。 function fetchJSON() { // 內部調用 jsonp 函數實現接口的 jsonp 訪問。 jsonp('http://localhost:3000/user?').then(data => { console.log(data); }).catch(err => { console.log(err); }) } function jsonp(url) { let $script = document.createElement('script'), // 先構造一個 script 元素 callbackName = `callback_${Date.now()}`; // 先定義回調名稱,加時間戳防止緩存 // 返回 promise 對象,方便後續處理 return new Promise((resolve, reject) => { // 在發起請求以前,先定義好回調函數 window[callbackName] = (res) => { // 請求結束以後清除全局變量 window[callbackName] = undefined; // 移除以前掛載的 script 元素 document.head.removeChild($script); // 清空 $script $script = undefined; resolve(res); }; // 綁定 src,及請求地址 $script.src = `${url}callback=${callbackName}`; // 綁定 error 處理函數 $script.onerror = err => { reject(err); }; // 掛在 script 元素到 head,此時纔開始發起請求~ document.head.appendChild($script); // 開始請求 }); } </script> </html>
固然上面的代碼沒有作兼容性處理,在低級瀏覽器使用時須要作一下處理,可是其餘原理是同樣的。
大體有兩點:
1、安全性問題
JSONP 會從其它域加載 JavaScript 腳本並直接執行,若是 JavaScript 腳本中包含惡意攻擊代碼,那咱們的網站將會受到威脅。因此當咱們訪問非本身維護的服務器的 JSONP 接口時,須要留心。
2、錯誤處理
script 標籤的 onerror 函數在 HTML5 才定義,而且即便咱們定義了 onerror 處理函數,咱們也不容易捕捉到錯誤發生的緣由。因此這也是一大缺點,至於具體表現能夠單獨運行上面的前端代碼試試,看看錯誤發生時(後端服務未啓動),前端控制檯打印出來的錯誤對象是什麼樣的。