【小哥哥, 跨域要不要了解下】JSONP

圈圈Dei圈發表於2018-12-05

系列文章:

打算純前端做一個介面測試工具, 直到遇到

2018-12-05-14-13-31
這個報錯, 觸碰到了知識盲區了, 怎麼辦???
2018-12-05-14-14-58

還好, 有谷哥和度娘. 原來是跨域 隨手整理了一下常用的跨域方式處理方案, 這裡馬上分享給大家 ?

ps: 為了保證前後端編碼的一致性, 本系列文章中涉及部分後端內容. 後端統一使用原生 nodejs 來搞, 請奔走相告.

準備工作

為了託管我們的靜態頁面, 我們需要一個可以提供伺服器環境的外掛, 這裡推薦 live-server, 通過命令 npm i -g live-server 安裝即可. 該外掛支援html檔案熱更新. 那使用者體驗簡直飛起. 一鍵啟動, 只需要在需要託管的目錄執行 live-server . 即可.

9150e4e5ly1fvmnifqdj2g207i07iaa4

ps: live-server 依賴 nodejs, 沒有安裝的小夥伴, 請參照這篇文章安裝 nodejs.

AJAX 訪問介面跨域解決方案

首先, 更正幾個常見的錯誤認識:

  1. 同源策略是瀏覽器的行為, 和 js 關係不大.
  2. 所謂跨域是指請求發起方頁面所在的 url 與訪問的 api 存在協議, 域名, 埠中任意一個不同即視為跨域. 並不單單是指域名.
  3. 跨域這個東西, 日常工作中並不是很常用. 你想, 誰會閒的沒事兒幹總是請求人家別人的 api 去.

jsonp

可能有小夥伴會說. 圈圈, 你扯淡, 既然瀏覽器有跨域限制. 為什麼我司專案從 bootcdn, 引入的 jquery 依然跑在資訊高速路上, 沒有任何低頭的意思?

hhh, ?. 這個質疑提的好. 瀏覽器同源策略禁止的是 ajax 請求. 然鵝, jquery 是一個 js 檔案. 不受該策略的限制.

我尼瑪, 那到底是啥限制啥不限制嘛???

2018-12-05-16-05-22

根據 MDN (自備梯子), 對於瀏覽器的同源策略的解釋, 不受限制的外域資源載入情況有以下幾種:

  • script
  • link
  • img
  • video
  • object embed applet
  • font-face 有的瀏覽器允許, 有的禁止
  • frame

那麼問題來了, 挖掘機學校..., 不好意思走錯片場了. 既然有這麼多方式可以繞過瀏覽器同源策略的限制. 那麼, 是不是我們可以做一點事情呢 ^_^

答: 是的 ?.

那還不抓緊搞起來?

6af89bc8gw1f8rf4zbaemg209105fka1

我們使用第一個特例 script 一步一步實現跨域訪問 (jsonp).

  • 首先, 建立本次文章的專案目錄
    2018-12-05-17-14-53
    目錄中, be 代表是後端專案, fe 代表前端專案. jsonp 目錄說明我們是用 jsonp 的方式實現跨域.
  • 在專案根目錄下執行 live-server ./fe/jsonp/ 啟動前端 web 容器
  • 編輯 ./fe/jsonp/ 目錄下的 index.html 檔案. 程式碼如下:
<!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>
    <h3>jsonp 實現跨域</h3>
</body>
</html>
複製程式碼
  • 瀏覽器訪問 localhost:8080瀏覽器如下圖說明前端 web 容器部署成功.

    2018-12-05-17-22-21

  • 編寫後端程式碼, 編寫 be/jsonp/index.js 檔案, 檔案內容如下

var http = require('http');
var PORT = 8888

// 建立一個 http 服務
var server = http.createServer(function(request, response) {
    response.end('hello world')
})

// 啟動服務, 監聽埠
server.listen(PORT, function() {
    console.log('服務啟動成功, 正在監聽: ', PORT)
})
複製程式碼
  • 編寫完成後命令列執行 node ./be/jsonp/index.js 命令列中出現

    2018-12-05-17-57-06
    說明後端程式啟動成功.此時可以通過瀏覽器訪問 localhost:8888獲得 hello world
    2018-12-05-19-15-36

  • 下來, 我們在前端的 index.html 中嘗試通過 ajax 請求 http://localhost:8888/ 來獲取返回資料, 新增如下程式碼, 新增以後程式碼

<script>
    var xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://localhost:8888/')
    xhr.send()
</script>
複製程式碼

回到瀏覽器, 檢視頁面控制檯, 熟悉的錯誤出現了. Access to XMLHttpRequest at http://localhost:8888/ from origin http://127.0.0.1:8080 has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 這個錯誤說明了, 我們是不能通過 ajax 的方式從 http://127.0.0.1:8080 訪問 http://localhost:8888/ 的.

既然不能通過 ajax 實現跨域的訪問, 同時 mdn 又說 script 標籤不受同源策略的限制. 那麼, 我們嘗試一下用 script 標籤引入 http://localhost:8888/ 試試?

2018-12-05-19-29-19

此時的程式碼, 網路請求沒有問題. 知識報了 js 檔案不合法的問題. 如果我們把介面返回的資料調整為規範的 js 是不是, 嗯哼???

幹起, 修改後端程式碼, 返回的內容由 hello world 改為 console.log('hello world'), 修改後的程式碼(修改完後端程式碼以後切記重啟服務哈 ^_^)

不得了, 不得了, 返回的結果不緊沒有報錯, 甚至可以執行. 我們從後端返回的 hellow world 成功的答應到了控制檯了.

2018-12-05-20-15-32

試想一下, 如果我們通過 js 檔案裡定義一個變數用於存放後端返回給前端的資料, 前端插入一個 script 標籤, 把後端返回的變數定義執行一把. 那樣定義的變數豈不是就可以在全域性可以獲取到後端定義的變數了. 趕緊試一把 ?

首先修改後端程式碼, 只需要調整一行.(修改完後端程式碼以後切記重啟服務哈 ^_^)

response.end("var aaaa = {name: 'quanquan', friend: 'guiling'}");
複製程式碼

其次調整前端程式碼

<script>
    // 第一次因為還沒有引入外部 js 所以列印 undefined
    console.log(window.aaaa)
    // 1 秒後, 外部 js 載入完成, 能列印出後端返回的變數定義
    setTimeout(() => {console.log(window.aaaa)}, 1e3)
</script>
<script src="http://localhost:8888/"></script>
複製程式碼

當前程式碼, 通過這種方式, 我們能夠成功的獲取到後端返回的資料. 但是, 介面這個東西時快時慢. 寫個定時器輪詢? 有點不夠 666, 腫麼辦?

2018-12-05-21-15-37

======================== 思考 5 分鐘 ========================

2018-12-05-21-18-13

======================== 5 分鐘已過 ========================

既然, 寫在 script 標籤上的內容是可以直接執行的. 那麼, 如果我們把變數的定義改寫成一個函式的執行可不可以呢 ^_^, 試試?

後端(修改完後端程式碼以後切記重啟服務哈 ^_^)

response.end("aaaa({name: 'quanquan', friend: 'guiling'})");
複製程式碼

前端

<script>
    // 由於後端返回的內容即將呼叫函式 aaaa, 那我們就預先定義一個唄, 這東西就叫回撥函式
    function aaaa(param) {
        console.log('後端返回的引數是: ', param)
    }
</script>
<script src="http://localhost:8888/"></script>
複製程式碼

結果

2018-12-05-21-30-30

此時程式碼, 目前為止, 我們已經徹底解決了跨域的問題. 很靠譜有木有? 當然木有. 這個玩意兒只是說明了 jsonp 的原理, 並沒有實用性. 下一步, 我們做一點封裝. 讓我們的程式碼更健壯 ??

最後, 修改一把程式碼

前端

// 建立 Jsonp 類
// 初始化時傳入兩個引數, url 是介面的url
// cb 是對於介面返回的引數的處理
function Jsonp(url, cb) {
    this.callbackName = 'jsonp_' + Date.now()
    this.cb = cb
    this.url = url
    this.init()
}

// 初始化方法 用於拼接 url
Jsonp.prototype.init = function() {
    if(~this.url.indexOf('?')) {
        this.url = this.url + '&callback=' + this.callbackName
    } else {
        this.url = this.url + '?callback=' + this.callbackName
    }
    this.createCallback()
    this.createScript()
}

// 建立 script 標籤, src 取介面請求的url
Jsonp.prototype.createScript = function() {
    var script = document.createElement('script')
    script.src = this.url
    script.onload = function() {
        this.remove()
        // 刪除 window 下定義的無用方法
        delete window[this.callbackName]
    }
    document.body.appendChild(script)
}

// 繫結回撥函式
Jsonp.prototype.createCallback = function() {
    window[this.callbackName] = this.cb
}

// 建立 jsonp 例項, 並指定回撥函式
new Jsonp('http://localhost:8888/', function(data) {
    console.log(data)
})
複製程式碼

後端(修改完後端程式碼以後切記重啟服務哈 ^_^)

const http = require('http');
// 新引入了 url 模組, 主要用於解析請求引數
const url = require('url');

const PORT = 8888;

// 建立一個 http 服務
const server = http.createServer((request, response) => {
  // 獲取前端請求資料
  const queryObj = url.parse(request.url, true).query;
  // 這裡把前端傳來的 callback 欄位作為後端返回的回撥函式的函式名稱
  response.end(`${queryObj.callback}({name: 'quanquan', friend: 'guiling'})`);
});

// 啟動服務, 監聽埠
server.listen(PORT, () => {
  console.log('服務啟動成功, 正在監聽: ', PORT);
});
複製程式碼

目前程式碼, 至此我們已經能夠順利的獲取跨域資源了. ??.

下集預告: jsonp 是一種傳統的跨域解決方案, 關於這種方式的優缺點, 請度娘, 下一節, 我們一起學習相對比較現代一點的跨域解決方案. CORS, See You

相關文章