Stories

Detail Return Return

「一起造輪子」從1.9k的jsonp庫出發🏗一起實現jsonp - Stories Detail

1. 什麼是jsonp?

下方是維基百科對JSON的解釋
image.png

從這個解釋中,我們可以知道,完成jsonp需要的步驟主要有以下兩點:

  1. 向頁面中插入一個帶有請求鏈接的<script>標籤
  2. 通過回調函數,獲取需要的JSON數據

2. jsonp庫是如何實現的?

jsonp是一個star數1.9k的倉庫,實現了一個簡單的jsonp方法

jsonp倉庫傳送門

2.1 傳入參數

  • url

傳入的url就是需要請求的鏈接地址

  • opts

param:傳入的是綴在鏈接後的參數,默認為callback

timeout:請求超時時間,默認為60000

prefix:全局回調函數名稱的前綴,默認為__jp

name:全局回調函數的名字,默認由前綴和自增數字生成

  • fn

回調函數的第一個參數是err,如果失敗返回錯誤:Timeout,如果成功返回null。
第二個參數是data,也就是最終請求的內容

調用該函數時,還會返回一個取消函數,如果希望取消請求,直接調用返回方法即可。

2.2 分析代碼

2.2.1 定義變量

image.png

count為計數器,noop為空函數(後面在重置全局函數時會用到)。


image.png

將2.1中定義的默認值,在代碼裏初始化,並且定義了變量。

2.2.2 設置超時定時器 & 清理頁面中的代碼

image.png

將頁面插入的<script>標籤代碼刪除,並將全局的回調方法置為空方法。如果有定時器則刪除定時器


image.png

調用超時後,清除清除頁面中的代碼。如果有回調函數,將會拋出Timeout報錯。


image.png

定義了返回的取消函數,本質上是調用cleanup函數清理全局頁面中的代碼。

2.2.3 將回調函數掛載到全局

image.png

將回調函數掛載到全局,返回數據後調用cleanup函數清理全局頁面中的代碼,並將數據返回給傳入的fn函數

2.2.4 處理請求地址

image.png

處理請求地址,將encodeURIComponent後的參數拼接至url

2.2.5 掛載<script>並返回取消函數

image.png

創建<script>標籤,並掛載到頁面上。最後返回取消函數。

使用target.parentNode.insertBefore的原因是由於target.appendChild兼容性不佳。按照提交者的説法是:

make IE<=8 happy😁

3.如何實現一個自己的jsonp?

通過分析上面的代碼,我們不難發現,主要是完成以下幾個功能

  1. 實現請求超時報錯
  2. 實現將回調函數掛載至window
  3. 實現處理url請求
  4. 實現創建script標籤,並插入頁面中

第一、四部分的代碼,我們可以繼續使用。

第二部分的代碼,實際上還是無法保證回調函數的名稱不與全局的方法衝突,因此需要生成一個唯一的函數名稱,如果檢查名稱有衝突則知道生成一個唯一的名稱為止。

第三部分的代碼,在處理的請求中,傳入的參數用的是string,但是平時開發常用的多為對象,因此在這裏需要支持傳入對象後並處理成字符串。

3.1 生成唯一函數名代碼

function getRandomKey(length = 6) {
    let randomKey = '';

    for (let i = 0; i < length; i++) {
        // 生成0~9和a-z的隨機字符串
        randomKey += ((Math.random() * 36) | 0).toString(36);
    }

    return randomKey;
}

function checkRandomKey(key, obj) {
    // 檢查當前生成的key值是否已經存在於obj中
    return obj[key] === undefined
        ? key
        : checkRandomKey(getRandomKey(), obj);
}

checkRandomKey(getRandomKey(), window);

將會在window上檢測生成的隨機字符串是否已被佔用,如果被佔用,則再生成一個。

3.2 拼接對象類型的參數

for (var key in params) {
    param += `${key}=${encodeURIComponent(params[key])}&`;
}

將代碼拼接成字符串,並且使用encodeURIComponent進行轉義。

3.3 優化傳入參數

url參數併入opts中,並將opts改名為config(比較喜歡axios的設計,所以叫了一樣的名字😁),fn修改為callback

4. 最終代碼

function jsonp(config, callback) {
    let {url, params, name, prefix = '_jsonp_callback_', timeout = 60000} = config;

    const target = document.getElementsByTagName('script')[0] || document.head;
    let script;
    let timer;
    let callbackFunctionName;
    let paramsString = '';

    // 定義空函數
    function noop() {
    }

    // 生成隨機key值
    function getRandomKey(length = 6) {
        let randomKey = '';

        for (let i = 0; i < length; i++) {
            // 生成0~9和a-z的隨機字符串
            randomKey += ((Math.random() * 36) | 0).toString(36);
        }

        return randomKey;
    }

    function checkRandomKey(key, obj) {
        // 檢查當前生成的key值是否已經存在於obj中
        return obj[key] === undefined
            ? key
            : checkRandomKey(getRandomKey(), obj);
    }

    // 確定掛在window上的回調函數名稱
    callbackFunctionName = name || checkRandomKey(getRandomKey(), window);

    // 清理不需要的代碼
    function cleanup() {
        if (script.parentNode) script.parentNode.removeChild(script);
        window[callbackFunctionName] = noop;
        if (timer) clearTimeout(timer);
    }

    // 取消調用
    function cancel() {
        if (window[callbackFunctionName]) cleanup();
    }

    // 設置定時器
    if (timeout) {
        timer = setTimeout(function () {
            cleanup();
            if (callback) callback(new Error('Timeout'));
        }, timeout);
    }

    // 將傳入的params轉化為字符串
    if (params) {
        for (var key in params) {
            paramsString += `${key}=${encodeURIComponent(params[key])}&`;
        }
    }

    // 拼接默認的callback內容
    paramsString += `callback=${prefix}${callbackFunctionName}`;

    // 將回調函數設置到window上
    window[callbackFunctionName] = function (data) {
        cleanup();
        if (callback) callback(null, data);
    };

    // 將請求參數拼接至url上
    url += (~url.indexOf('?') ? '&' : '?') + paramsString;
    url = url.replace('?&', '?');

    // 創建一個script標籤並插入到頁面中
    script = document.createElement('script');
    script.src = url;
    target.parentNode.insertBefore(script, target);

    // 返回取消函數
    return cancel;
}

至此我們完成了我們自己的jsonp輪子。如果發現有問題,歡迎評論區留言。

5. 參考資料

Add a new Comments

Some HTML is okay.