我已經將這個項目從新命名爲 pjsonp 而且在 npm 上發佈啦,歡迎在你的項目中使用,並在 GitHub 提交 issue 和 pull request。git
npm install pjsonp --save
複製代碼
這篇文章經過實現一個生產環境中可用的,Promise API 封裝的 jsonp 來說解 jsonp 的原理。github
因爲瀏覽器跨域限制的存在,一般狀況下,咱們不能夠經過 AJAX 發起跨域請求。但考慮以下事實:npm
這樣咱們就找到一種跨域方案了:json
function_name(data)
, data 就是咱們想要得到的數據,通常是 JSON 格式function_name
,這個函數是一個閉包,記住了調用位置的做用域鏈,這樣咱們就能夠在這個閉包裏調用業務代碼下面來看實現。跨域
咱們要求調用者這樣調用 pjsonp(url, params, options)
,傳入三個參數:promise
url
:請求的 URL,應該像這樣:http://somehostname[:someport]/to/some/path[?with=true&orWithoutQueries=false]
params
:可選,請求參數。這是一個簡單的 object,包含請求的參數。由於 jsonp 只能用於 GET 請求,因此參數都要寫在 URL 中,而支持這個參數能夠給使用者帶來便利。options
:可選,jsonp 的配置信息。
prefix
:回調函數的前綴,用於生成回調函數名timeout
:超時事件,超時後請求會被撤銷,並向調用者報錯name
:特別指定的回調函數名param
:在請求的 URL 中,回調函數名參數的 keyif (!options) {
options = params
params = {}
}
if (!options) options = {}
// merge default and user provided options
options = Object.assign({}, defaultOptions, options)
const callbackName = options.name || options.prefix + uid++
複製代碼
首先是對參數的處理。因爲 params
只是個添頭功能,因此咱們容許用戶不傳入params
而只傳入 options
,這時就要進行處理。而後咱們將默認的 options
和用戶指定的 options
合併起來(你會發現用 Object.assign
比傳統的 ||
更加簡單!)。最後,產生一個回調函數名。瀏覽器
而後,咱們須要準備一些引用:服務器
let timer
let script
let target
複製代碼
分別指向超時計時器,插入 DOM 中的 script 標籤和插入的位置。閉包
而後幫助調用者準備參數。注意,咱們還要將 &${enc(options.param)}=${enc(callbackName)}
插入到 URL 的末尾,要求服務器在返回的 js 文件中,以 callbackName
做爲回調函數名。app
// prepare url
url += url.indexOf('?') > 0 ? '' : '?'
for (let key in params) {
let value = params[key] || ''
url += `&${enc(key)}=${enc(value)}`
}
url += `&${enc(options.param)}=${enc(callbackName)}`
url = url.replace('?&', '?')
複製代碼
接下來,咱們在 DOM 中插入 script 標籤。
// insert the script to DOM and here we go!
target = document.getElementsByTagName('script')[0] || document.head
script = document.createElement('script')
script.src = url
target.parentNode.appendChild(script, target)
複製代碼
最後咱們返回一個 Promise 對象,爲了簡單起見,咱們只在 Promise 裏寫絕對必要的代碼。咱們在 window[callbackName]
上賦值了一個函數(的引用),從而構成了一個閉包。能夠看到這個函數在被調用的時候,一是會 resolve 收到的 data,這樣調用者就能夠用獲取到的數據來執行他們的代碼了;二是會調用 clean
函數。除了綁定這個函數以外,咱們還設置了一個定時器,超時以後,就會 reject 超時錯誤,同時也調用 clean
函數。
return new Promise((resolve, reject) => {
/** * bind a function on window[id] so the scripts arrived, this function could be. triggered * data would be a JSON object from the server */
window[callbackName] = function(data) {
clean()
resolve(data)
}
if (options.timeout) {
timer = setTimeout(() => {
clean()
reject('[ERROR] Time out.')
}, options.timeout)
}
})
複製代碼
clean
函數很是重要,它負責回收資源。它會去 DOM 中移除這個 script 標籤,清除超時定時器,而且將 window[callbackName]
設置成一個什麼都不作的函數(爲了防止調用非 function 報錯),這樣原來引用的那個閉包就會被垃圾回收掉了,避免了閉包帶來的內存泄露問題。
function clean() {
script.parentNode && script.parentNode.removeChild(script)
timer && clearTimeout(timer)
window[callbackName] = doNothing // use nothing function instead of null to avoid crash
}
複製代碼
以上就是所有的代碼了,結合文章開頭說的 jsonp 的執行原理,很容易就能讀懂。完整代碼:
/** * This module uses Promise API and make a JSONP request. * * @copyright MIT, 2018 Wendell Hu */
let uid = 0
const enc = encodeURIComponent
const defaultOptions = {
prefix: '__jp',
timeout: 60000,
param: 'callback'
}
function doNothing() {}
/** * parameters: * - url: like http://somehostname:someport/to/some/path?with=true&orWithoutParams=false * - params: a plain object so we can help to parse them into url * - options: options to promise-jsonp * - prefix {String} * - timeout {Number} * - name {String}: you can assign the callback name by your self, if provided, prefix would be invalid * - param {String}: the key of callback function in request string * * thanks to Promise, you don't have to pass a callback or error handler * * @param {String} url * @param {Object} options * @param {Object} params * @returns {Promise} */
function pjsonp(url, params = {}, options) {
if (!options) {
options = params
params = {}
}
if (!options) options = {}
// merge default and user provided options
options = Object.assign({}, defaultOptions, options)
const callbackName = options.name || options.prefix + uid++
let timer
let script
let target
// remove a jsonp request, the callback function and the script tag
// this is important for performance problems caused by closure
function clean() {
script.parentNode && script.parentNode.removeChild(script)
timer && clearTimeout(timer)
window[callbackName] = doNothing // use nothing function instead of null to avoid crash
}
// prepare url
url += url.indexOf('?') > 0 ? '' : '?'
for (let key in params) {
let value = params[key] || ''
url += `&${enc(key)}=${enc(value)}`
}
url += `&${enc(options.param)}=${enc(callbackName)}`
url = url.replace('?&', '?')
// insert the script to DOM and here we go!
target = document.getElementsByTagName('script')[0] || document.head
script = document.createElement('script')
script.src = url
target.parentNode.appendChild(script, target)
/** * returns a Promise to tell user if this request succeeded or failed * as less code as possible here for clarity */
return new Promise((resolve, reject) => {
/** * bind a function on window[id] so the scripts arrived, this function could be triggered * data would be a JSON object from the server */
window[callbackName] = function(data) {
clean()
resolve(data)
}
if (options.timeout) {
timer = setTimeout(() => {
clean()
reject('[ERROR] Time out.')
}, options.timeout)
}
})
}
module.exports = pjsonp
複製代碼
這篇文章就到這裏,但願你已經徹底理解了 jsonp 而且會實現它了。歡迎和我交流。