JSONP简介

本文简介:浏览器请求数据的方法,文件当作数据库、用 imagescript 标签发送请求,以及JSONP的实现方法。

本文是以NodeJS入门NodeJS实现一个服务器为基础来讲解的。

一、文件数据库

1、什么是数据库

  • 文件系统是一种数据库
  • MySQL 是一种数据库

只要能够长久的存储数据,就是数据库。在搭建一个文件数据库之前,我们先回顾一下《NodeJS入门》中讲到的相关内容,其中文件目录结构如下:
目录结构
代码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//==========server.js==============
var http = require("http");
var fs = require("fs");
var url = require("url");
var port = process.env.PORT || 8888;

var server = http.createServer((req,res)=>{
let temp = url.parse(req.url,true);
let path = temp.pathname;
let query = temp.query;
let method = req.method;

if(path == '/'){
let str = fs.readFileSync('./index.html','utf-8');
res.setHeader('Content-Type','text/html;charset=utf-8')
res.end(str)
}else if(path == '/style.css'){
let str = fs.readFileSync('./style.css','utf-8');
res.setHeader('Content-Type','text/css')
res.end(str)
}else if(path == '/main.js'){
let str = fs.readFileSync('./main.js','utf-8');
res.setHeader('Content-Type','applocation/javascript')
res.end(str)
}else{
res.statusCode = 404
res.end()
}
})
server.listen(port);
console.log(
"监听 " +
port +
" 成功\n打开 http://localhost:" +
port
);
//==========index.html==============
<!DOCTYPE>
<html>
<head>
<link rel='stylesheet' href='/style.css'>
</head>
<body>
<h4>您的账户余额是<span id="mount">100</span></h4>
<button id="btn">付款</button>
<script type='text/javascript' src='/main.js'></script>
</body>
</html>
//==========style.css==============
body{
background:#ededed;
}
h1{
color:#f00;
}
h2{
color:#666;
}
//==========main.js==============
console.log(1);

在当前目录打开终端,并输入: localhost:8888

2、添加绑定事件

针对以上代码,为button绑定事件,每点击一次,余额减少一元。则 html 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE>
<html>
<head>
<link rel='stylesheet' href='/style.css'>
</head>
<body>
<h4>您的账户余额是<span id="mount">100</span></h4>
<button id="btn">付款</button>
<script type='text/javascript' src='/main.js'></script>
<script>
btn.addEventListener('click',function(){
let num = parseInt(mount.innerText);
let newNum = num - 1;
mount.innerText = newNum;
})
</script>
</body>
</html>

以上实现了点击 button 则余额减少一元。那么此时问题就来了,每次刷新页面的时候,初始余额仍旧为 100 元,我们需要系统能够记住点击 button 后的余额。解决方法是要么使用数据库,要么使用文件系统。此处我们以文件系统为例。

3、建立文件系统

在当前目录下新建 db 文件,可以无后缀。文件中的内容为 100 。为了将 db 中的内容传递到页面中,我们在页面中给余额部分加一个占位符,即将 <span id="mount">100</span> 改为 <span id="mount">$$mount$$</span> 。至于 server.js 则修改如下:

1
2
3
4
5
6
7
8
9
10
...
if(path == '/'){
let num = fs.readFileSync('./db','utf8')
let str = fs.readFileSync('./index.html','utf8');
str = str.replace('&&&mount&&&',num);
res.setHeader('Content-Type','text/html;charset=utf-8');
res.write(str);
res.end();
}
...

以上代码能够实现数据从文件系统读取,那问题又来了,点击了几次 button 之后再重新刷新页面,余额初始值仍旧是 100 元。此时就需要我们在点击的时候,将余额减少一元的信息发送到后台进行数据处理,并修改文件系统的余额值。

4、通过 form 表单提交数据

1) 页面构建

修改 DOM 结构:

1
2
3
4
5
6
...
<h4>您的账户余额是<span id="mount">&&&mount&&&</span></h4>
<form action="/pay" method="post">
<button id="btn" type="submit">付款</button>
</form>
...

此处需要在页面中删除 button 的绑定事件。
修改 server.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
else if(path == '/main.js'){
let str = fs.readFileSync('./main.js','utf8');
res.setHeader('Content-Type','applocation/javascript')
res.write(str);
res.end();
}else if(path === '/pay' && method.toUpperCase() === 'POST'){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
fs.writeFileSync('./db',newNum);
res.write('success');
res.end();
}
...

此处为向后台发送数据,所以使用 POST 方法。
以上,在点击 button 后会跳转到 /pay 页面,并返回 success ,退回到上一页并刷新页面可见余额总数改变。

2) 体验优化

以上能够实现修改后端数据的功能,但是有一点体验比较差,每次都会跳转到 /pay ,如何防止跳转呢?解决方案为:在页面中添加一个 iframe ,并将 form 的 target 指向 iframe ,这样请求成功的话,返回的数据 'success' 会在 iframe 中显示,结构代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE>
<html>
<head>
<link rel='stylesheet' href='/style.css'>
</head>
<body>
<h4>您的账户余额是<span id="mount">&&&mount&&&</span></h4>
<form action="/pay" method="post" target="result">
<button id="btn" type="submit">付款</button>
</form>
<iframe src="about:blank" name="result" frameborder="0" height="200"></iframe>
<script type='text/javascript' src='/main.js'></script>
</body>
</html>

二、image 和 script 发请求

以上方法可以完成数据的修改,但是在页面内引入的 iframe ,一般情况下不建议使用 iframeiframe 会阻塞页面的加载。如果不用 iframe 的话,可以使用 <link><img><script> 标签,这三种标签可以发起请求。

1、 image 发请求

此方法的缺陷是请求方式只能是 GET ,无法使用其他方法。所以修改页面结构代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
<h4>您的账户余额是<span id="mount">&&&mount&&&</span></h4>
<button id="btn">付款</button>
<script type='text/javascript' src='/main.js'></script>
<script>
btn.addEventListener('click',function(){
let image = document.createElement('img');
image.src = '/pay';
image.onload = function(){
alert('success')
}
image.onerror = function(){
alert('fail');
}
})
</script>
...
//=========server.js代码========
else if(path === '/pay'){
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write('success');
}else{
res.statusCode = 400;
res.write('fail');
}
res.end();
}

在页面中通过监听 imageonload事件onerror事件 来区别图片是否已经接收到有效的 src 值。在 server.js 中,我们通过随机数来区别是否请求成功,并向前台发送相应的状态码。
至此,通过 image 请求图片基本完成,但是在实际的运行中,前台一直弹出 'fail' 的对话框,经查,需要返回正确的图片路径才会避免此处错误产生。解决方法,在根目录中添加图片 cat.png ,上述代码修改为:

1
2
3
4
5
6
7
8
9
...
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write(fs.readFileSync('./cat.png'));
}
...

根据以上代码,在图片请求成功后,文件系统数据改变,但是前台余额值并未修改,我们只需要在图片的监听事件 onload 中重新加载页面即可,或者在请求成功的前提下直接修改余额的值:

1
2
3
4
5
6
7
8
9
...
image.onload = function(){
alert('success')
//===重新加载页面===
window.location.reload();
//===修改余额值===
mount.innerText = mount.innerText - 1;
}
...

以上就是通过 image 发起请求。

2、通过 script 发起请求

通过 script 发起请求的做法与上述做法类似,需要新建一个元素 script 并使其的 src 指向 /pay ,有一点需要注意, script标签徐需要添加到页面中才能发起请求,这一点与 image 标签不同。并且返回的 script 中的内容会以 JS 的方式执行。修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//=======index.html======
...
btn.addEventListener('click',function(){
let script = document.createElement('script');
script.src = '/pay';
document.body.appendChild(script);
script.onload = function(){
alert('success')
window.location.reload();
}
script.onerror = function(){
alert('fail');
}
})
...
//========server.js============
...
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
res.setHeader('Content-Type','applocation/javascript')
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write('alert("pay")');
}
...

以上,如果请求成功后,会首先弹出 pay 然后再弹出 success 。显然,这两个功能重复,我们尝试着将弹出框和页面重新加载/局部刷新合并到 server.js 中。 server.js 代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
res.setHeader('Content-Type','applocation/javascript')
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write(`
alert('success');
// window.location.reload();
mount.innerText = mount.innerText - 1;
`);
}
...

至此,以上代码正常运行仍旧存在一个问题:每次点击 button 后,页面添加了一个 script 标签,点击多少次添加多少了,为解决这个问题,需要在 scriptonloadonerror 事件中删除当前添加的标签:

1
2
3
4
5
6
7
script.onload = function(e){
e.currentTarget.remove();
}
script.onerror = function(e){
alert('fail');
e.currentTarget.remove();
}

删除了 script 标签后,变量 script 仍旧存在内存中。以上就是 SRJ(Server Rendered JavaScript) ,即为服务器返回的 Javascript 。

三、请求另外网站的 script

在实际项目中,我们可以在本地请求一些 CDN 加速器上的 JS 文件,而本地的域名与 CDN 加速器的域名不一致,但仍旧可完成 JS 请求,所以说, script 请求是无域名限制的。 CSS样式表同理。
针对这个代码我们开两个服务,首先打开终端输入 sudo vi /etc/hosts 修改 hosts 文件,在文件最后添加两行代码:

1
2
127.0.0.1   summer.com
127.0.0.1 winter.com

然后在文件目录启动两个服务,分别输入 PORT=8001 node serverPORT=8002 node server ,端口分别是 8001 、 8002 。在浏览器中可输入 summer:8001winter:8002 。因为这两个域名和端口不同,所以这是两个不同的网站(不明白为何是不同网站,参考浏览器同源策略)。
两个服务用的代码一样,我们要从 summer.com 发起请求,去请求 winter.com 的接口,那么 script 的 src 值改为 script.src = 'http://winter.com:8001/pay'; 。打开 summer.com ,点击按钮,如果请求成功的话,通过控制台的 Network 可以发现我们请求的 Domain 为 winter.com 。
以上实现的基础,需要后端了解前端的页面逻辑功能,前后端耦合度高。为了便捷开发,需要对此进行解耦。具体操作为,我们在请求数据的时候,在请求成功时调用的方法以参数形式传递给后端,后端在处理完数据后,能够成功返回时调用一下这个方法就 ok 了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
...
//====index.html====
window.xxx = function(res){
if(res === 'success'){
mount.innerText = mount.innerText - 1;
}else{
alert('请求失败');
}
}
btn.addEventListener('click',function(){
let script = document.createElement('script');
script.src = 'http://winter.com:8001/pay?callbackName=xxx';
document.body.appendChild(script);
script.onerror = function(){
alert('fail');
}
})
...
//=====server.js======
let temp = url.parse(req.url,true);
let path = temp.pathname;
let query = temp.query;
...
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
res.setHeader('Content-Type','applocation/javascript')
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write(`
${query.callbackName}.call(undefined,'success');
`);
}else{
res.statusCode = 400;
res.write(`
${query.callbackName}.call(undefined,'fail');
`);
}
...

以上实现解耦和数据前后端数据传递。

四、JSONP

JSONP 要解决的问题是两个网站之间的交流,根据上面的例子,我们使用 <script> 标签就能够解决,并且不受域名限制。根据上一个例子,如果返回的数据格式为:

1
2
3
4
{
"success" : true,
"left" : 123
}

则可以成为这种数据请求方式为 JSONP ( JSON + padding ) ,以上的左花括号和右花括号分别成为左 padding 和右 padding。好吧,这个名字确实有点简单。
简单的叙述下 JSONP 的数据请求流程:

1
2
3
4
5
6
7
8
请求方: 浏览器( summer.com )
响应放: 服务器( winter.com )
1、请求方创建一个 script , script 的 src 指向响应方,并在请求中传递一个参数 "?callbackName=xxx" ,其中 xxx 为请求方自定义的方法
2、响应方根据查询参数 callbackName ,构造形如:
1) xxx.call(undefined,'返回的数据')
2) xxx('返回的数据')
3、浏览器接到响应并执行 xxx.call(undefined,'返回的数据')
4、请求方就拿到了想要的数据并进行 DOM 操作等

以上有几条行业约定:
1、callbackName -> callback / jQuery_callback
2、xxx -> 随机数
解释一下这两条,第一条,我们在代码中写到的 callbackName ,在实际代码中约定俗成写成 callback 或者 jQuery_callback 即可。关于第二条,也就是我么定义的 window.xxx 方法的名字使其随机生成。请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
...
//======index.html======
btn.addEventListener('click',function(){
let script = document.createElement('script');
let funcName = 'summer' + parseInt(Math.random() * 100000 , 10 );
window[funcName] = function(res){
if(res === 'success'){
mount.innerText = mount.innerText - 1;
}else{
alert('请求失败');
}
}
script.src = 'http://winter.com:8001/pay?callback='+funcName;
document.body.appendChild(script);
script.onload = function(e){
e.currentTarget.remove();
delete window[funcName];
}
script.onerror = function(e){
alert('fail');
e.currentTarget.remove();
delete window[funcName];
}
})
...
//======server.js=======
if(Math.random() > .5){
let num = fs.readFileSync('./db','utf8')
let newNum = parseInt(num) - 1;
res.setHeader('Content-Type','applocation/javascript')
fs.writeFileSync('./db',newNum);
res.statusCode = 200;
res.write(`
${query.callback}.call(undefined,'success');
`);
}else{
res.statusCode = 400;
res.write(`
${query.callback}.call(undefined,'fail');
`);
}
...

总结:其实以上功能在 jQuery 的 ajax 中已经封装的很好了,请求方式为 jsonp 就能实现以上 JSONP 请求数据功能。 JSONP 并不是 ajax 也不是 ajax 的一种,只是 jQuery 将 JSONP 的实现归到 ajax 方法中。

五、Tips

1、JSONP 为什么不支持 POST 请求?

  • JSONP 是通过 script 动态创建的
  • 动态创建 script ,只能使用 GET