事實上,可以發出非同步請求的方式,並非只有非同步物件,使用<iframe>就是一個例子,建立<img>指定src屬性亦可由瀏覽器請求圖片來源,而建立<script>標籤並指定src屬性,也是一個可運用的方式。
你可以動態建立<script>標籤,指定其src屬性,而後將之附加至DOM樹上,如此瀏覽器就會為你下載src所指定的指令稿。例如:
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'some.js';
document.getElementsByTagName('head')[0].appendChild(script);
script.type = 'text/javascript';
script.src = 'some.js';
document.getElementsByTagName('head')[0].appendChild(script);
上面的程式片段,相當於你在網頁的<head>中寫下:
<script type="text/javascript" src="some.js"></script>
這會執行下載回來的some.js。這也是動態載入JavaScript的一個方式,也是 JSONP 的基礎。
正如 安全限制 中談過的,JavaScript中不允許取得與文件來源不同的資料,非同步物件亦遵守這個同源策略(Same-origin policy),然而許多時候,仍必須要能跨站取得資源,不然的話,現在許多跨站網站服務就無法運行了。
突破窘境的方式,就是使用方才介紹過動態下載執行指令稿的方式。例如,如果原本的指令稿有這樣的函式:
function doSomething(json) {
...
}
...
}
如果使用以上概念,src傳回的指令稿,是像這樣的形式:
doSomething({"name":"Justin","age":35});
那麼就會呼叫doSomething()並傳入{"name":"Justin","age":35}。這就是JSONP的原理,JSONP全名JSON with Padding,有Padding的JSON資料,Padding的部份基本上是指呼叫的函式。
如果你的服務端設計為,可接受這樣的請求參數:
id=E123456&jsoncallback=handler
由某個請求參數決定要傳回的函式,例如指定jsoncallback名稱 為handler,伺服端就傳回:
handler({"name":"Justin","age":35});
那麼客戶端就更有彈性可以決定傳回的JSON要由哪個函式處理。以下是個實際的例子,你可以在本機使用這個網頁,輸入ID(1、2、3是有資料的)取得資料:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<script type="text/javascript" src="js/json2.js"></script>
<script type="text/javascript">
window.onload = function() {
function param(obj) {
var pairs = [];
for(var name in obj) {
var pair = encodeURIComponent(name) + '=' +
encodeURIComponent(obj[name]);
pairs.push(pair.replace('/%20/g', '+'));
}
return pairs.join('&');
}
function getScript(url, callback) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// 跨瀏覽器處理 script 下載完成後的事件
script.onload = script.onreadystatechange = function() {
if (!this.readyState ||
this.readyState === "loaded" ||
this.readyState === "complete") {
this.onload = this.onreadystatechange = null;
document.getElementsByTagName('head')[0]
.removeChild(this);
callback();
}
};
document.getElementsByTagName('head')[0]
.appendChild(script);
}
function jsonp(option, callbackName) {
// 沒有url或伺服端要求的callbackName就結束
if(!option.url || !callbackName) {
return;
}
var data = option.data || {};
// 建立暫時的函式
data[callbackName] = 'XD' + jsonp.jsc++;
window[data[callbackName]] = function(json) {
option.callback(json);
};
var url = option.url + '?' + param(data);
// 取得 script 檔案
getScript(url, function() {
// script 下載並執行完後移除暫時的函式
window[data[callbackName]] = undefined;
try {
delete window[data[callbackName]];
}
catch(e) {}
});
}
jsonp.jsc = new Date().getTime();
document.getElementById('test').onclick = function() {
jsonp({
url : 'https://openhome.cc/Gossip/' +
'JavaScript/samples/JSONP-1.php',
data : {
id : document.getElementById('id').value,
},
callback : function(person) {
document.getElementById('result').innerHTML =
person.name + ',' + person.age;
}
}, 'jsoncallback');
};
};
</script>
</head>
<body>
ID:<input id="id">
<button id="test">JSONP 測試</button>
<span id="result"></span>
</body>
</html>
在 http://api.flickr.com/services/feeds/photos_public.gne 這個網址,提供的就是JSONP服務,你可以用jsoncallback參數指定回呼函式。下面的範例可以使用標籤搜尋Filick上的圖片並顯示在網頁中:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<script type="text/javascript" src="js/json2.js"></script>
<script type="text/javascript">
window.onload = function() {
function param(obj) {
var pairs = [];
for(var name in obj) {
var pair = encodeURIComponent(name) + '=' +
encodeURIComponent(obj[name]);
pairs.push(pair.replace('/%20/g', '+'));
}
return pairs.join('&');
}
function getScript(url, callback) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// 跨瀏覽器處理 script 下載完成後的事件
script.onload = script.onreadystatechange = function() {
if (!this.readyState ||
this.readyState === "loaded" ||
this.readyState === "complete") {
this.onload = this.onreadystatechange = null;
document.getElementsByTagName('head')[0]
.removeChild(this);
callback();
}
};
document.getElementsByTagName('head')[0]
.appendChild(script);
}
function jsonp(option, callbackName) {
// 沒有url或伺服端要求的callbackName就結束
if(!option.url || !callbackName) {
return;
}
var data = option.data || {};
// 建立暫時的函式
data[callbackName] = 'XD' + jsonp.jsc++;
window[data[callbackName]] = function(json) {
option.callback(json);
};
var url = option.url + '?' + param(data);
// 取得 script 檔案
getScript(url, function() {
// script 下載並執行完後移除暫時的函式
window[data[callbackName]] = undefined;
try {
delete window[data[callbackName]];
}
catch(e) {}
});
}
jsonp.jsc = new Date().getTime();
document.getElementById('search').onclick = function() {
jsonp({
url : 'http://api.flickr.com/services/' +
'feeds/photos_public.gne',
data : {
tagmode : 'any',
format : 'json',
tags : document.getElementById('tags').value
},
callback : function(data) {
var images = document.getElementById('images');
// 先清空所有的圖片
var length = images.childNodes.length;
for(var i = 0; i < length; i++) {
images.removeChild(images.firstChild);
}
// Flick 傳回 JSNOP,可以先自行研究一下當中的資料
// items 是個陣列,每個元素有個 media 特性
// media 特性上的 m 就是圖片網址
var items = data.items;
for(var i = 0; i < items.length; i++) {
var img = document.createElement('img');
img.src = items[i].media.m;
images.appendChild(img);
}
}
}, 'jsoncallback');
};
};
</script>
</head>
<body>
<input id="tags"><br>
<button id="search">搜尋</button>
<div id="images"></div>
</body>
</html>