1. 什麼是jsonp?
下方是維基百科對JSON的解釋
從這個解釋中,我們可以知道,完成jsonp需要的步驟主要有以下兩點:
- 向頁面中插入一個帶有請求鏈接的
<script>標籤 - 通過回調函數,獲取需要的JSON數據
2. jsonp庫是如何實現的?
jsonp是一個star數1.9k的倉庫,實現了一個簡單的jsonp方法
2.1 傳入參數
- url
傳入的url就是需要請求的鏈接地址
- opts
param:傳入的是綴在鏈接後的參數,默認為callback
timeout:請求超時時間,默認為60000
prefix:全局回調函數名稱的前綴,默認為__jp
name:全局回調函數的名字,默認由前綴和自增數字生成
- fn
回調函數的第一個參數是err,如果失敗返回錯誤:Timeout,如果成功返回null。
第二個參數是data,也就是最終請求的內容
調用該函數時,還會返回一個取消函數,如果希望取消請求,直接調用返回方法即可。
2.2 分析代碼
2.2.1 定義變量
count為計數器,noop為空函數(後面在重置全局函數時會用到)。
將2.1中定義的默認值,在代碼裏初始化,並且定義了變量。
2.2.2 設置超時定時器 & 清理頁面中的代碼
將頁面插入的<script>標籤代碼刪除,並將全局的回調方法置為空方法。如果有定時器則刪除定時器
調用超時後,清除清除頁面中的代碼。如果有回調函數,將會拋出Timeout報錯。
定義了返回的取消函數,本質上是調用cleanup函數清理全局頁面中的代碼。
2.2.3 將回調函數掛載到全局
將回調函數掛載到全局,返回數據後調用cleanup函數清理全局頁面中的代碼,並將數據返回給傳入的fn函數
2.2.4 處理請求地址
處理請求地址,將encodeURIComponent後的參數拼接至url上
2.2.5 掛載<script>並返回取消函數
創建<script>標籤,並掛載到頁面上。最後返回取消函數。
使用target.parentNode.insertBefore的原因是由於target.appendChild兼容性不佳。按照提交者的説法是:
make IE<=8 happy😁
3.如何實現一個自己的jsonp?
通過分析上面的代碼,我們不難發現,主要是完成以下幾個功能
- 實現請求超時報錯
- 實現將回調函數掛載至window
- 實現處理url請求
- 實現創建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輪子。如果發現有問題,歡迎評論區留言。