本文是基于之前的一个server小任务的延伸,以历史的一些进程,来模拟当时前端与数据库“沟通”的过程,最后引入JSONP。
首先我们有一个server.js的脚本。
var http = require('http')
var fs = require('fs')
var url = require('url')
var port = process.argv[2]
if(!port){
console.log('请指定端口号好不啦?\nnode server.js 8888 这样不会吗?')
process.exit(1)
}
var server = http.createServer(function(request, response){
var temp = url.parse(request.url, true);
var path = temp.pathname;
var query = temp.query;
var method = request.method;
/******** 从这里开始看,上面不要看 ************/
if(path === "/") {
var string = fs.readFileSync('./index.html', 'utf-8');
var amount = fs.readFileSync('./db', "utf-8");
console.log("amount", amount)
string = string.replace('&&amount&&', amount);
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.write(string);
response.end();
} else if(path === "/style.css") {
var string = fs.readFileSync('./style.css', 'utf-8');
response.setHeader('Content-type', 'text/css; charset=utf-8');
response.write(string);
response.end();
} else if(path === "/main.js") {
var string = fs.readFileSync('./main.js', 'utf-8');
response.setHeader('Content-type', 'text/javascript; charset=utf-8;');
response.write(string);
response.end();
} else {
response.statusCode = 404;
response.setHeader("Content-Type", "text/html;charset=utf-8");
response.write('找不到对应的路径!');
response.end();
}
/******** 代码结束,下面不要看 ************/
})
server.listen(port)
console.log('监听 ' + port + ' 成功\n请用在空中转体720度然后用电饭煲打开 http://localhost:' + port)
之前我们写前端的时候,都只是在浏览器环境来操作。比如:
html代码:
<p>您的余额是<span id="amount">100</span></p>
<button id="click">Click!</button>
<script src="./main.js"></script>
main.js代码:
click.addEventListener("click", function(e) {
amount.innerText = amount.innerText - 1;
});
image.png
当我们点击按钮后,虽然会在页面上显示数字变化(从100变为99),但是随着我们刷新页面,数字又会变为100. 这时应该把这个数字记录存在一个“容器”里,每次点击后我们应该将“容器”中把这个数字记录修改掉,然后显示修改后的数值,就算页面刷新,再次从“容器”中获取这个数值,也是修改后的最新数值。这个容器就是数据库。
我们首先创建一个名为“db”的文件,然后里面写上数字“100”。把上面写死的100换成“db”文件里面的100.修改server.js文件。
if(path === "/") {
var string = fs.readFileSync('./index.html', 'utf-8');
var amount = fs.readFileSync('./db', "utf-8"); // 读取db文件中的内容赋值给amount
string = string.replace('&&amount&&', amount); // 然后将html中&&amount&&用amount值替换
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.write(string);
response.end();
}
<p>您的余额是<span id="amount">&&amount&&</span></p>
我们想想怎么从页面中发出一个请求?因为不需要获取什么数据,应该是POST请求最好。
首先我们想到的就是使用form标签。
<p>您的余额是<span id="amount">&&amount&&</span></p>
<form action="/pay" method="POST">
<input id="pay" type="submit" name="pay" value="付款">
</form>
image.png
因为路径跳转到了/pay,在server.js没有响应的地址。我们来加这个响应地址。
if(path === "/pay" && method.toUpperCase() === "POST") {
var amount = fs.readFileSync("./db", "utf-8"); // 读取db文件中的数值
var newAmount = amount - 1; // 因为付款了,减去1
fs.writeFileSync("./db", newAmount); // 再把新数字放入db文件
response.write("success"); 响应success
response.end();
}
image.png
然后返回之前的页面刷新,余额变成99。但是这样的用户体验很差,每次需要用户自己去手动返回?在这个基础上,我们用iframe来承载response。
<p>您的余额是<span id="amount">&&amount&&</span></p>
<form action="/pay" method="POST" target="result">
<input id="pay" type="submit" name="pay" value="付款">
</form>
<iframe src="about:blank" name="result" frameborder="0"></iframe>
image.png
这样虽然不用跳转,但是还是要刷新。那我们又想:能不能不用iframe?那又可以使用什么方式发送请求呢?可以有CSS的link,a标签,img标签和script标签。(我们使用后面两种)
那我们能不能动态创建一个img来发请求呢?
pay.addEventListener("click", function(e) {
var img = document.createElement('img');
img.src = "/pay";
});
image.png
我们可以看到img可以发请求,虽然是GET请求。这就给我们的需求创造了可能。
然后我们修改server.js中的pay路径脚本,同时运用Math.random来增加一种“打钱失败”情况。我们通过什么方式来通知用户操作成功或者失败呢?可以在js里面增加img.onload或者img.onerror来达到效果。
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
if(Math.random() > 0.5) {
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "image/jpg");
response.statusCode = 200;
response.write("success");
} else {
response.statusCode = 400;
response.write("fail");
}
response.end();
}
pay.addEventListener("click", function(e) {
var img = document.createElement('img');
img.src = "/pay";
img.onload = function() {
alert("打钱成功");
};
img.onerror = function() {
alert("打钱失败");
}
});
image.png
我们看到虽然状态码是200,但是还是显示“打钱失败”,说明img没有顺利加载。后来发现其实是需要在成功状况时,在response中写入一个img。
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
if(Math.random() > 0.5) {
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "image/jpg");
response.statusCode = 200;
response.write(fs.readFileSync("./dog.jpg")); // 加上这句。
response.write("success");
} else {
response.statusCode = 400;
response.write("fail");
}
response.end();
}
image.png
确认后页面数字还是必须刷新后才有变化,再次优化,我们就在js里的img.onload得知成功后,直接在前端页面上改变。
pay.addEventListener("click", function(e) {
var img = document.createElement('img');
img.src = "/pay";
img.onload = function() {
alert("打钱成功");
amount.innerText = amount.innerText - 1;
};
img.onerror = function() {
alert("打钱失败");
}
});
然后我们再用script标签来发请求。
pay.addEventListener("click", function(e) {
var script = document.createElement('script');
script.src = "/pay";
document.body.appendChild(script);
script.onload = function() {
alert("打钱成功!");
};
script.onerror = function() {
alert("打钱失败!");
}
});
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "text/javascript");
response.statusCode = 200;
response.write('alert("success")');
response.end();
}
此时我们发现一个问题,就是alert的success和打钱成功都执行了。原来script标签会执行response里面的内容。这么说我们就可以把script.onload里面内容都写到response里面。
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "text/javascript");
response.statusCode = 200;
response.write(`
amount.innerText = amount.innerText - 1;
`);
response.end();
}
但是我们又发现一个问题,多次点击付款后,会在html代码那里插入多个script。
image.png
于是我们在js中加上删除script语句。
pay.addEventListener("click", function(e) {
var script = document.createElement('script');
script.src = "/pay";
document.body.appendChild(script);
script.onload = function(e) {
e.currentTarget.remove();
};
script.onerror = function(e) {
alert("打钱失败!");
e.currentTarget.remove();
}
});
然后说一个重要概念,script的src可以请求不同域名。那我们做两个网站,让这两个网站交流。
frank.com:8001 和 jack.com:8002 之间。
pay.addEventListener("click", function(e) {
var script = document.createElement('script');
script.src = "http://jack.com:8002/pay";
document.body.appendChild(script);
script.onload = function(e) {
e.currentTarget.remove();
};
script.onerror = function(e) {
alert("打钱失败!");
e.currentTarget.remove();
};
});
image.png
如图,当前域名在frank.com:8001访问了jack.com:8002/pay。但是这有个问题,这里的响应需要frank.com的程序员要清楚jack.com网站细节,有很大的耦合性。
于是我们想到把执行的callbackName放入src域名中,然后把执行的函数放在js中。
window.xxx = function(result) {
if(result === "success") {
amount.innerText = amount.innerText - 1;
}
};
pay.addEventListener("click", function(e) {
var script = document.createElement('script');
script.src = "http://jack.com:8002/pay?callbackName=xxx";
document.body.appendChild(script);
script.onload = function(e) {
e.currentTarget.remove();
};
script.onerror = function(e) {
alert("打钱失败!");
e.currentTarget.remove();
};
});
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "text/javascript");
response.statusCode = 200;
response.write(`
${query.callbackName}.call(undefined, 'success'); // 通过query获取函数名
`);
response.end();
}
然后我们把「${query.callbackName}.call(undefined, 'success'); 」中的“success”换做JSON, 并且在JS里更新在window.callbackName的使用。
if(path === "/pay") {
var amount = fs.readFileSync("./db", "utf-8");
var newAmount = amount - 1;
fs.writeFileSync("./db", newAmount);
response.setHeader("Content-type", "text/javascript");
response.statusCode = 200;
response.write(`
${query.callbackName}.call(undefined, {
"success": true,
"amount": ${newAmount}
});
`);
response.end();
}
pay.addEventListener("click", function(e) {
var script = document.createElement('script');
var functionName = "jason" + Math.floor(Math.random() * 100000, 10); // 随机构建一个函数名
window[functionName] = function(result) {
if(result.success) {
amount.innerText = result.amount;
}
}
script.src = `http://jack.com:8002/pay?callbackName=${functionName}`;
document.body.appendChild(script);
script.onload = function(e) {
e.currentTarget.remove();
delete window[functionName]; // script结束后删除这个函数
};
script.onerror = function(e) {
alert("打钱失败!");
e.currentTarget.remove();
delete window[functionName]; // script结束后删除这个函数
};
});
但是其实jQuery已经在js那里封装好了。
pay.addEventListener("click", function(e) {
// 使用jQuery
$.ajax({
url: "http://jack.com:8002/pay",
jsonp: "callback",
dataType: "jsonp",
success: function( response ) {
if(response.success) {
amount.innerText = response.amount;
}
}
});
// 不使用jQuery
// var script = document.createElement('script');
// var functionName = "jason" + Math.floor(Math.random() * 100000, 10);
// window[functionName] = function(result) {
// if(result.success) {
// amount.innerText = result.amount;
// }
// }
// script.src = `http://jack.com:8002/pay?callbackName=${functionName}`;
// document.body.appendChild(script);
// script.onload = function(e) {
// e.currentTarget.remove();
// delete window[functionName];
// };
// script.onerror = function(e) {
// alert("打钱失败!");
// e.currentTarget.remove();
// delete window[functionName];
// };
});
大体总结下:
JSONP
请求方:frank.com 前端工程师(浏览器)
响应方:jack.com 后端工程师(服务器)
1. 请求方创建script,src指向响应方,同时加上查询条件:?callback=yyy
2. 响应方根据查询条件,构建形如:
yyy.call(undefined, "你要的数据");
yyy.("你要的数据")
这样的响应
3. 浏览器收到响应就会执行yyy.call(undefined, "你要的数据");
4. 那么请求方就知道他要的数据了。
约定:
1. callbackName是一个随机数。
2. callbackName 一般约定俗成是“callback”。
网友评论