温馨提示:
本文所述内容具有依赖性,可能因软硬条件不同而与预期有所差异,故请以实际为准,仅供参考。
在进行前端交互开发时,为了方便快捷,相信很多人都会选择 JQuery 封装的 Ajax 方法,但有些时候,我们只是需要 JQuery 的 Ajax 请求方法,为了这个方法去引用 30KB 的 jQuery 显然是浪费资源的。
其实,原生 JavaScript 实现 Ajax 并不难,甚至可以直接实现跨域请求 JSONP!一起来看看。
一、AJAX
1、原理步骤
Ajax 的核心是 XMLHttpRequest
,一个完整的 Ajax 请求一般包括以下步骤:
- 实例化 XMLHttpRequest 对象
- 连接服务器
- 发送请求
- 接收响应数据
2、实现方法
将 Ajax 请求封装成 ajax()
方法,接受一个配置对象 params
:
function ajax(params) {
params = params || {};
params.data = params.data || {};
// 判断是ajax请求还是jsonp请求
var json = params.jsonp ? jsonp(params) : json(params);
// ajax请求
function json(params) {
// 请求方式,默认是GET
params.type = (params.type || 'GET').toUpperCase();
// 避免有特殊字符,必须格式化传输数据
params.data = formatParams(params.data);
var xhr = null;
// 实例化XMLHttpRequest对象
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
// IE6及其以下版本
xhr = new ActiveXObjcet('Microsoft.XMLHTTP');
};
// 监听事件,只要 readyState 的值变化,就会调用 readystatechange 事件
xhr.onreadystatechange = function() {
// readyState属性表示请求/响应过程的当前活动阶段,4为完成,已经接收到全部响应数据
if(xhr.readyState == 4) {
var status = xhr.status;
// status:响应的HTTP状态码,以2开头的都是成功
if(status >= 200 && status < 300) {
var response = '';
// 判断接受数据的内容类型
var type = xhr.getResponseHeader('Content-type');
if(type.indexOf('xml') !== -1 && xhr.responseXML) {
response = xhr.responseXML; //Document对象响应
} else if(type === 'application/json') {
response = JSON.parse(xhr.responseText); //JSON响应
} else {
response = xhr.responseText; //字符串响应
};
// 成功回调函数
params.success && params.success(response);
} else {
params.error && params.error(status);
}
};
};
// 连接和传输数据
if(params.type == 'GET') {
// 三个参数:请求方式、请求地址( get 方式时,传输数据是加在地址后的)、是否异步请求(同步请求的情况极少);
xhr.open(params.type, params.url + '?' + params.data, true);
xhr.send(null);
} else {
xhr.open(params.type, params.url, true);
//必须,设置提交时的内容类型
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
// 传输数据
xhr.send(params.data);
}
}
//格式化参数
function formatParams(data) {
var arr = [];
for(var name in data) {
// encodeURIComponent() :用于对 URI 中的某一部分进行编码
arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
};
// 添加一个随机数参数,防止缓存
arr.push('v=' + random());
return arr.join('&');
}
// 获取随机数
function random() {
return Math.floor(Math.random() * 10000 + 500);
}
}
3、使用实例
ajax({
url: 'test.php', // 请求地址
type: 'POST', // 请求类型,默认"GET",还可以是"POST"
data: {'b': '异步请求'}, // 传输数据
success: function(res){ // 请求成功的回调函数
console.log(JSON.parse(res));
},
error: function(error) {} // 请求失败的回调函数
});
二、JSONP
1、同源策略
Ajax 之所以需要“跨域”,原因就是浏览器的同源策,即一个页面的 Ajax 请求只能获取这个页面相同源或者相同域的数据。
Q:什么叫“同源”或者“同域”呢?
A:协议、域名、端口号都必须相同。例如:
http://vircloud.net
和https://vircloud.net
不同,因为协议不同;http://vircloud.net:8080
和http://vircloud.net:1000
不同,因为端口不同;http://localhost:8080
和https://vircloud.net
不同,协议、域名、端口号都不同。
当跨域请求时,一般都会看到这个错误:
XMLHttpRequest cannot load http://vircloud,net/article/?intro=jsonp%E8%AF%B7%E6%B1%82&v=5520. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.
那如何跨域请求呢?这时,JSONP
就登场了!
2、原理步骤
JSONP(JSON with Padding) 是一种跨域请求方式。主要原理是利用了 script
标签可以跨域请求的特性,由其 src
属性发送请求到服务器,服务器返回 JavaScript
代码,浏览器接受响应,然后就直接执行了,这和通过 script
标签引用外部文件的原理是一样的。
JSONP 由两部分组成:回调函数
和数据
,回调函数一般是在浏览器控制,作为参数发往服务器端(当然,你也可以固定回调函数的名字,但客户端和服务器端的名称一定要一致)。当服务器响应时,服务器端就会把该函数和数据拼成字符串返回。
JSONP的请求过程:
- 请求阶段:浏览器创建一个
script
标签,并给其src
赋值(类似https://ip-cn.vircloud.net:8443/test.php?callback=jsonpcallback&v=3116
)。 - 发送请求:当给
script
的src
赋值时,浏览器就会发起一个请求。 - 数据响应:服务端将要返回的数据作为参数和函数名称拼接在一起,格式类似”
jsonpCallback({name: 'abc'})
返回,当浏览器接收到了响应数据,由于发起请求的是script
,所以相当于直接调用jsonpCallback
方法,并且传入了一个参数。
3、实现方法
依旧是 ajax()
方法里添加 JSONP
,后面会将两者整合在一起,JSONP
的配置参数主要多了一个 jsonp
参数,它就是你的回调函数名:
function ajax(params) {
params = params || {};
params.data = params.data || {};
var json = params.jsonp ? jsonp(params) : json(params);
// jsonp请求
function jsonp(params) {
//创建script标签并加入到页面中
var callbackName = params.jsonp;
var head = document.getElementsByTagName('head')[0];
// 设置传递给后台的回调参数名
params.data['callback'] = callbackName;
var data = formatParams(params.data);
var script = document.createElement('script');
head.appendChild(script);
//创建jsonp回调函数
window[callbackName] = function(json) {
head.removeChild(script);
clearTimeout(script.timer);
window[callbackName] = null;
params.success && params.success(json);
};
//发送请求
script.src = params.url + '?' + data;
//为了得知此次请求是否成功,设置超时处理
if(params.time) {
script.timer = setTimeout(function() {
window[callbackName] = null;
head.removeChild(script);
params.error && params.error({
message: '超时'
});
}, time);
}
};
//格式化参数
function formatParams(data) {
var arr = [];
for(var name in data) {
arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
};
// 添加一个随机数,防止缓存
arr.push('v=' + random());
return arr.join('&');
}
// 获取随机数
function random() {
return Math.floor(Math.random() * 10000 + 500);
}
}
注意:因为 script
标签的 src
属性只在第一次设置的时候起作用,导致 script
标签没法重用,所以每次完成操作之后要移除;
4、使用实例
ajax({
url: 'test.php', // 请求地址
jsonp: 'jsonpCallback', // 采用jsonp请求,且回调函数名为"jsonpCallbak",可以设置为合法的字符串
data: {'b': '异步请求'}, // 传输数据
success:function(res){ // 请求成功的回调函数
console.log(res);
},
error: function(error) {} // 请求失败的回调函数
});
在这里后台使用 PHP 处理:
<?php
$data = array('type'=>'jsonp');
$callback = isset($_GET['callback']) ? trim($_GET['callback']) : '';
echo $callback.'('.json_encode($data).')';
注意:别漏了用函数名与数据拼接返回。当然,前面也说过,你可以给定固定回调函数名:
function jsonpCallback() {}
<?php
echo 'jsonpCallback('.$data.
三、实战演练
1、效果展示
参见页面 IP 地址查询。
2、需求分析
通过 iframe 方法检测访客 IP,由于 iframe 限制,无法知晓查询状态,出现错误时显示浏览器自带的错误页,破坏页面协调,所以考虑通过 Ajax + Jsonp 方法,动态实现查询中、查询结果、不支持三个查询状态。
3、实现方法
I. 前端 HTML
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>IP 地址查询 - Virtual Cloud Server, Powered by www.vircloud.net</title>
<meta name="keywords" content="IP地址,真实IP,代理IP,外网IP" />
<meta name="description" content="查询访问因特网时,网站看到的真实 IP 地址。" />
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="bootstrap.min.css" />
<script src="lan.js" type="text/javascript"></script>
</head>
<body>
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
<h5 class="my-0 mr-md-auto font-weight-normal">IP 地址查询</h5>
</div>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
</div>
<div class="container">
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">本地 IP</div>
<div class="card-body">
<p id="lanip" style="width:100%;height:36px;font-family: 微软雅黑;">
<span id="lantips" style="font-size:13px;color:#656565;text-align:center;line-height:74px;">查询中</span>
</p>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header">国内 IP</div>
<div class="card-body">
<p id="cnip">
<iframe src="/loading.html" id="ip-cn" width="100%" height="68" scrolling="no" frameborder="0" marginheight="0" marginwidth="0"></iframe>
</p>
</div>
</div>
</div>
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">国外 IP</div>
<div class="card-body">
<p id="krip">
<iframe src="/loading.html" id="ip-kr" width="100%" height="68" scrolling="no" frameborder="0" marginheight="0" marginwidth="0"></iframe>
</p>
</div>
</div>
</div>
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">特殊 IP</div>
<div class="card-body">
<p id="goip">
<iframe src="/loading.html" id="ip-go" width="100%" height="68" scrolling="no" frameborder="0" marginheight="0" marginwidth="0"></iframe>
</p>
</div>
</div>
</div>
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">IPv6</div>
<div class="card-body">
<p id="jpip">
<iframe src="/loading.html" id="ip-jp" width="100%" height="68" scrolling="no" frameborder="0" marginheight="0" marginwidth="0"></iframe>
</p>
</div>
</div>
</div>
<footer>
<p>检测分析:</p>
<p>0、如果 IP 都无法显示,说明您的上网环境非常安全,且不可追踪;<br>
1、本地:即便是使用了代理也不可避免地泄露,建议安装 WebRTC 拦截器插件;<br>
2、国内:这是您在访问正常国内网站时,网站所能识别到的 IP;<br>
3、国外:这是您在访问正常国外网站时,网站所能识别到的 IP;<br>
4、特殊:这是您在访问特殊网站(如 Google、Facebook、Tumblr 等)时所使用的代理 IP;</p>
</footer>
<br><br>
</div>
<script>
function ajax(params) {
params = params || {};
params.data = params.data || {};
var json = params.jsonp ? jsonp(params) : json(params);
function json(params) {
params.type = (params.type || 'GET').toUpperCase();
params.data = formatParams(params.data);
var xhr = null;
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObjcet('Microsoft.XMLHTTP');
};
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
var status = xhr.status;
if(status >= 200 && status < 300) {
var response = '';
var type = xhr.getResponseHeader('Content-type');
if(type.indexOf('xml') !== -1 && xhr.responseXML) {
response = xhr.responseXML;
} else if(type === 'application/json') {
response = JSON.parse(xhr.responseText);
} else {
response = xhr.responseText;
};
params.success && params.success(response);
} else {
params.error && params.error(status);
}
};
};
if(params.type == 'GET') {
xhr.open(params.type, params.url + '?' + params.data, true);
xhr.send(null);
} else {
xhr.open(params.type, params.url, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.send(params.data);
}
}
function jsonp(params) {
var callbackName = params.jsonp;
var head = document.getElementsByTagName('head')[0];
params.data['callback'] = callbackName;
var data = formatParams(params.data);
var script = document.createElement('script');
head.appendChild(script);
window[callbackName] = function(json) {
head.removeChild(script);
clearTimeout(script.timer);
window[callbackName] = null;
params.success && params.success(json);
};
script.src = params.url + '?' + data;
if(params.time) {
script.timer = setTimeout(function() {
window[callbackName] = null;
head.removeChild(script);
params.error && params.error({
message: '超时'
});
}, time);
}
};
function formatParams(data) {
var arr = [];
for(var name in data) {
arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
};
arr.push('v=' + random());
return arr.join('&');
}
function random() {
return Math.floor(Math.random() * 10000 + 500);
}
}
</script>
<script>
iptarget = ["ip-cn","ip-kr","ip-go","ip-jp"];
function testIP(){
for(var i=0;i<iptarget.length;i++){
// var turl = iptarget[i]+".vircloud.net";
var turl = iptarget[i]+".vircloud.net:8443";
ajax({
url: "https://"+turl+"/test.php",
jsonp: "jsonpcallback",
success: function(res){
console.log(res);
}
});
}
setTimeout(checkError,3000);
}
function checkError(){
for(var i=0;i<iptarget.length;i++){
var iframe = document.getElementById(iptarget[i]);
if(iframe.src.indexOf("loading.html")>-1){
queryServer(i,0);
}
}
}
function queryServer(i,t){
var iframe = document.getElementById(iptarget[i]);
if(t) {
// iframe.src="https://"+iptarget[i]+".vircloud.net/";
iframe.src="https://"+iptarget[i]+".vircloud.net:8443/";
} else {
iframe.src="/error.html";
}
}
if (window.addEventListener) {
window.addEventListener("load", testIP, false);
} else if (window.attachEvent) {
window.attachEvent("onload", testIP);
} else {
window.onload = testIP;
}
</script>
</body>
</html>
II. 后端 Jsonp
<?php
echo 'queryServer(0,1);';
?>
参考文章:
1、《原生 JavaScript 实现 AJAX、JSONP》