1

jsonp

javascript高级部分:前后端联动,浏览器+服务器

1数据库是什么

文件系统是一种数据库
MySQL 是一种数据库,也是一个软件
只要能长久地存数据,就是数据库

2前后端如何配合?

接下来我们用一个文件充当数据库(实际上数据库的存储内容本质就是一个带有结构的文件),捋一捋前后端交互的过程.

代码在这里
几次代码的变更在历史commit里

首先写一个首页

<title>首页</title>
<link rel="stylesheet" href="/style.css">
<h5>我的余额为<span id="amount">&&&amount&&&</span></h5>
<!-- &&&amount&&&知识前台的占位符,会将从后台读取的数据显示在这里,并替换这个我们假设的占位符 -->
<form action="/pay" method="POST">
    <input type="submit" value="付款一元">
</form>

再写服务器的代码

var http = require('http')
var fs = require('fs')
var url = require('url')

var port = process.env.PORT || 8888;

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')  // 就读取 index.html 的内容
        var amount = fs.readFileSync('./db','utf-8')//****从数据库(db文件)同步读取数据,放到amount变量里
        string = string.toString().replace('&&&amount&&&',amount)//将后台的amount替换为前台的占位符&&&amount&&&

        response.setHeader('Content-Type', 'text/html;charset=utf-8')  // 设置响应头 Content-Type
        response.write(string)   // 设置响应消息体
        response.end();
    } else if (path === '/style.css') {   // 如果用户请求的是 /style.css 路径
        var string = fs.readFileSync('./style.css')
        response.setHeader('Content-Type', 'text/css')
        response.write(string)
        response.end()
    } else if (path === '/main.js') {  // 如果用户请求的是 /main.js 路径
        var string = fs.readFileSync('./main.js')
        response.setHeader('Content-Type', 'application/javascript')
        response.write(string)
        response.end()
    } else if(path === '/pay' && method.toUpperCase() === 'POST'){//如果请求的路径是pay且方法为post
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;
        fs.writeFileSync('./db',newAmount);
        response.write('success')
        response.end()
    }else {  // 如果上面都不是用户请求的路径
        response.statusCode = 404
        response.setHeader('Content-Type', 'text/html;charset=utf-8')  // 设置响应头 Content-Type
        response.write('找不到对应的路径,你需要自行修改 index.js')
        response.end()
    }

    // 代码结束,下面不要看
    console.log(method + ' ' + request.url)
})

server.listen(port)
console.log('监听 ' + port + ' 成功,请用在空中转体720度然后用电饭煲打开 http://localhost:' + port)

最后在创建一个名为db的文本文件充当数据库

接着我们开启服务器
node index.js 8888

PU0B59.png
接着在浏览器中输入地址http://localhost:8888
当用户打开浏览器输入ip地址,就会向后台发送请求,服务器发现地址是/之后执行if elsepath==='/'分支,执行下面的代码:

if (path === '/') {  // 如果用户请求的是 / 路径
        var string = fs.readFileSync('./index.html')  // 就读取 index.html 的内容
        var amount = fs.readFileSync('./db','utf-8')//****从数据库(db文件)同步读取数据,放到amount变量里
        string = string.toString().replace('&&&amount&&&',amount)//将后台的amount替换为前台的占位符&&&amount&&&

        response.setHeader('Content-Type', 'text/html;charset=utf-8')  // 设置响应头 Content-Type
        response.write(string)   // 设置响应消息体
        response.end();
    } 

接着点击提交按钮,就会向后台发起post请求,请求路径为/pay,于是进入path === '/pay'分支,执行下面的代码,使数据库(这里数据库用一个文本文件代替)中的金额减一

else if(path === '/pay' && method.toUpperCase() === 'POST'){//如果请求的路径是pay且方法为post
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;
        fs.writeFileSync('./db',newAmount);
        response.write('success')
        response.end()
    }

然后会发现/pay页面返回了success
PU0hUH.png
返回刚才的首页,刷新后,会发现金额减少了1.

也可以模拟后台修改数据库成功或失败

 else if(path === '/pay' && method.toUpperCase() === 'POST'){//如果请求的路径是pay且方法为post
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;
        if(Math.random()>0.5){//模拟成功或失败
            fs.writeFileSync('./db',newAmount);
            response.write('success')
        }else{
            response.write('fail')
        }
        response.end()
    }

3 优化体验:用image和script发送请求

但是刚才那样体验非常不好,因为要返回,要刷新.在2005年以前,网页的前后端交互都是这样.但现在是8012年了,我们需要优化一下用户体验

不光form表单可以发送请求,a标签(需要点击),link标签,script,图片,都可以发送请求

3.1用image发送请求

那我们先试试用img发送一个请求
当浏览器发现html里面有一个img时,会自动发送请求,请求路径就是img的src.但是有一个缺点.发送的请求只能是get,没办法改成post.
使用img.onload和img.onerror方法判断请求是成功还是失败,具体操作看下方代码

复习一下返回的状态码:
2开头:成功
3开头:重定向
4开头:客户端错误
5开头:服务器错误

修改首页

<title>首页</title>
<link rel="stylesheet" href="/style.css">
<h5>我的余额为<span id="amount">&&&amount&&&</span></h5>
<!-- &&&amount&&&知识前台的占位符,会将从后台读取的数据显示在这里,并替换这个占位符 -->

<button id="button">付钱一元</button>
<script>
    let btn = document.getElementById("button");
    btn.onclick = function(){
        let img = document.createElement("img");
        img.src = "/pay";
        img.onload = function(){//如果图片请求成功(返回码以2开头)
            alert("付钱成功")
            window.location.reload();//弹框并刷新
        }
        img.onerror = function(){//返回码是4开头
            alert("付钱失败")
        }    
    }
</script>

修改/pay路径的响应

else if(path === '/pay'){//如果请求的路径是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')//设置返回文件类型为jpg
            response.statusCode = 200;//返回码为200,说明成功
            response.write(fs.readFileSync('./dog.jpg'))//必须返回一个真的图片,不然浏览器还是会认为是失败
        }else{
            response.statusCode = 400;//否则返回码为400,说明失败
            response.write('fail')
        }
        response.end()
    }

然后开启服务器,浏览器输入地址
点击付钱,成功,并刷新
PUr6vq.png
付钱失败时,弹框后浏览器就不刷新了:
PUr2rV.png

也可局部刷新

img.onload = function(){//如果图片请求成功(返回码以2开头)
            alert("付钱成功")
                        amount.innerText = amount.innerText-1;//局部刷新/因为已经知道数据库修改成功,所以无需再进行刷新,直接修改数值即可

        }

还有一个需要注意的点
在下面这段代码中

 response.setHeader('Content-Type','image/jpg')//设置返回文件类型为jpg,
            response.statusCode = 200;//返回码为200,说明成功
            response.write(fs.readFileSync('./dog.jpg'))//必须返回一个真的图片,不然浏览器还是会认为是失败

看看回复的响应
response.write(fs.readFileSync('./dog.jpg'))这句代码中fs.readFileSync('./dog.jpg')参数里只有路径,没有加别的,那么读出来的数据类型为2进制数据,然后'Content-Type','image/jpg'这句代码表示以jpg图片的形式读取这段二进制代码.

复习一下响应的四个部分:

1 协议/版本号 状态码 状态解释
2 Key1: value1
2 Key2: value2
2 Content-Length: 17931
2 Content-Type: text/html
3
4 响应的内容

response.write(fs.readFileSync('./dog.jpg'))这句代码就是响应的第四部分,即响应的内容

3.2用script发送请求

那么我们也可以用script动态的发送请求.原理和img类似,但是必须得把script标签加入到body里面,才会发送请求,而img标签只要创建,就会发送请求.
接下来看代码
首页的发送请求代码

<script>
    let btn = document.getElementById("button");
    let amount = document.getElementById("amount")
    btn.onclick = function(){
        let script = document.createElement("script");
        script.src = "/pay";
        
        document.body.appendChild(script);//这句话一定要加上,这是与使用图片请求不一样的地方

        script.onload = function(){//如果script请求成功(返回码以2开头)
            alert("付钱成功");
        }
        script.onerror = function(){//返回码是4开头
            alert("付钱失败");
        }
    }
</script>

服务器端的代码:

else if(path === '/pay'){//如果请求的路径是pay,接受script的请求
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;

        if(Math.random()>0.5){//模拟成功或失败
            fs.writeFileSync('./db',newAmount);//成功了就把数据写入数据库
            
            response.setHeader('Content-Type','applacation/javascript')//设置返回文件类型为javascript
            response.statusCode = 200;//返回码为200,说明成功
            response.write('alert("我是创建的script请求里面响应的内容")')//响应内容为创建的script标签里面的内容
        }else{
            response.statusCode = 400;//否则返回码为400,说明失败
            response.write('fail')
        }
        response.end()
    }

当我点击打钱的时候,他会动态的创建一个script,这个script会根据src发起请求

进入失败分支:
PUI8wd.png
成功:
先执行请求使用的script里面的语句
PUItYt.png

PUIaSf.png

每次请求就会创建一个新的script标签,如果响应成功了,就执行响应里面的javascript语句,这个script里面的语句就是返回的响应的第四部分的内容,执行完之后才会触发onload事件
PUI6kn.png

3.2.1优化用户体验

那么既然请求返回的响应可以执行,我们就不需要写onload了,直接把onload里面的代码写在响应的第四部分不就可以了?没错

接来下修改一下代码,删掉onload代码,将onload里面的代码写在服务器端.将前台请求成功要做的事情交给后台来做.

btn.onclick = function(){
        let script = document.createElement("script");
        script.src = "/pay";

        document.body.appendChild(script);//这句话一定要加上,这是与使用图片请求不一样的地方

        //删除onload代码,交给后台来做
        script.onerror = function(){//返回码是4开头
            alert("付钱失败");
        }
    }

后台只需要将金额数量减1即可,不需要刷新,增加用户体验

 else if(path === '/pay'){//如果请求的路径是pay,接受script的请求
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;

        if(Math.random()>0.5){//模拟成功或失败
            fs.writeFileSync('./db',newAmount);//成功了就把数据写入数据库

            response.setHeader('Content-Type','applacation/javascript')//设置返回文件类型为javascript
            response.statusCode = 200;//返回码为200,说明成功
            response.write('amount.innerText = amount.innerText-1')//响应内容为创建的script标签里面的内容,返回后会立即执行*********
        }else{
            response.statusCode = 400;//否则返回码为400,说明失败
            response.write('fail')
        }
        response.end()
    }else

最后在做一件事情
PUI6kn.png
因为每次不管成功失败,都会创建一个script标签,那么可否在请求结束后,将标签删除?
可以.
修改前台onload和onerror代码即可,不管请求成功失败,都移除script标签

script.onload = function(e){
            e.currentTarget.remove();//添加移除代码
        }
        script.onerror = function(e){
            e.currentTarget.remove();
        }
3.2.2总结 SRJ(server rrendered javascript--服务器返回的javascript)

以上方法就叫做SRJ,服务器返回的javascript,即不是在前台写的javascript
这是在ajax出现之前,后端程序员想出来的前后台无刷新,局部更新页面内容的交互方法

4 请求另个一网站的script

script请求的内容不受域名限制.当前网站的可以请求其他网站的js代码,例如jquery
PUox2V.png
可以引入jquery

所以刚才的SRJ很危险,假如只用了get方法,那么像付钱这种操作很容易被伪装成功,导致别人的恶意攻击
例如我进入一个钓鱼网站
那个网站直接请求
script.src = "http://alipay.com/pay?usname=mataotao&amount=100000"
这样只要进入网站执行这个请求,岂不是可以随意给自己的账户打钱?
所以只用get不安全.
所以很多危险操作都得用POST请求,例如打钱必须验证用户,做各种支付相关的安全防范,手机验证码之类的.

下面我们模仿一下不同的网站请求对方的后台的过程
首先修改一下本地的hosts文件,将两个域名都指向127.0.0.1,假装成两个不同的网站

hosts文件相当于一个个人的DNS,当你访问某个域名时,是先通过hosts进行解析的,没有找到才进一步通过外网DNS服务器进行解析

如果是windows系统:
windows系统hosts文件位置及操作
如果是linux或者Unix
地址为/etc/hosts

PU7mYn.png
添加代码,那么如果我在本地访问mataotao.com和jack.com,他首先会进127.0.0.1,而不是去外网解析dns
这样我们就假装成为了两个网站,实际上两个网站的内容是一样的,知识用来做实验

然后开两个服务器,使用两个不同的端口来监听(模拟两个不同的网站都开启了服务器,都在监听)
监听8001
PU7aSx.png

监听8002
PU70OO.png

这样我们在地址栏输入mataotao.com:8001就进入了第一个网站,在地址栏输入jack.com:8002
就进入了第二个网站.

这样两个个网站都可以接受请求,都可以访问,只不过内容一样而已
那么接下来我们来模仿不同网站之间的请求互动

修改前端SRJ请求代码的请求路径,改为script.src = "http://jack.com:8002/pay";
那么我们打开mataotao.com:8001就进入了第一个网站,点击按钮后,意思就是请求http://jack.com:8002/pay这个第二个网站的地址.我们来试一试

PU7zBF.png
结果如图所示

PUHS74.png这是成功的时候

可以看到,两个完全不同的网站,他们两个是可以互相调script的
PUHPhR.png

这时候的意思是:我是用第一个网站(mataotao.com)打开了第二个网站(jack.com)的付钱功能!
(由于我为了演示,这两个网站用了同样的代码,同一个数据库,我们假设第一个网站he第二个网站代码不同)

5 jsonp

这时候出现一个问题:
jack.com的后端程序员需要对mataotao.com的页面细节了解的很清楚.
这样的缺点是后台对前台需要了解得太深入了.比如说你是node或者php腾讯后端程序员,可是我竟然还要对阿里巴巴网站的一个按钮的一个细节非常了解.耦合度太高,前后端彼此之间关系太紧密了.如果后端程序员对页面细节不了解,代码就写不下去了.这样对前后端分离不友好

5.1解决前后端耦合度太高的问题

解耦(解决耦合问题)
解决方法:

后台程序员:我只把我后台应该改的数据库,然后我写一个callback函数,将成功和失败的状态传递给前台.这个函数就是我后端程序员做完这些事情成功之后要前端程序员去做的事情.然后执行这个函数,这个函数的内容交给前端去写.

前端程序员:我不管后端如何修改数据库,我事先写好成功和失败的函数,成功了就返回给我一个成功的提示,然后我根据提示执行成功的函数,如果给我的提示是失败,就执行失败的函数.
接下来写代码理解:
修改服务器端的代码:

response.write(`${query.callbackName}.call(undefined,'success')`)//先获取查询字符串里的callbackName即从前台传过来的回调函数的函数名,然后再执行他,并把函数的参数定为success

添加前台index.html的代码

<script>
    // 这三行是添加的
    window.xxx = function(result){
        alert("我是前端程序员写的函数,经过后端程序员调用才执行")
        alert(`后端返回回来的函数参数是:${result}`)
    }
    // 这三行是添加的
    let btn = document.getElementById("button");
    let amount = document.getElementById("amount")
    btn.onclick = function(){
        let script = document.createElement("script");
        //这行是修改的
        script.src = "http://jack.com:8002/pay?callbackName=xxx";//请求时带上查询参数,指定callbackName为XXX,以便给后台程序员调用
        //这行是修改的

        document.body.appendChild(script);//这句话一定要加上,这是与使用图片请求不一样的地方

        script.onload = function(e){//如果script请求成功(返回码以2开头)
            e.currentTarget.remove();//添加移除代码
        }
        script.onerror = function(e){//返回码是4开头
            alert("失败")
            e.currentTarget.remove();
        }
    }
</script>

添加的这几句代码的解释:
首先
script.src = "http://jack.com:8002/pay?callbackName=xxx";
在请求时带上查询参数,指定callbackName为xxx,以便给后台程序员调用
接着

response.write(`${query.callbackName}.call(undefined,'success')`)

后台程序员获得前太程序员的查询参数,并执行名为查询参数的函数xxx

执行xxx,并且将相应的内容作为回调函数的第一个参数传回去,这个参数为字符串'success'

window.xxx = function(result){
        alert("我是前端程序员写的函数,经过后端程序员调用才执行")
        alert(`后端返回回来的函数参数是:${result}`)
    }

执行结果:
Pa9B8O.png

Pa9rxe.png
Pa9Rat.png

也可以根据后台传过来的参数,修改前台的逻辑

window.xxx = function(result){
        if(result === "success"){
            amount.innerText = amount.innerText - 1;
        }
    }

那么如果我们返回的参数不是字符串,而是对象呢?
这也是可以的,例如我们修改一下代码

else if(path === '/pay'){//如果请求的路径是pay,接受script的请求
        var amount = fs.readFileSync('./db','utf-8')
        var newAmount = amount-1;

        if(Math.random()>0.5){//模拟成功或失败
            fs.writeFileSync('./db',newAmount);//成功了就把数据写入数据库

            response.setHeader('Content-Type','applacation/javascript')//设置返回文件类型为javascript
            response.statusCode = 200;//返回码为200,说明成功
            response.write(`${query.callbackName}.call(undefined,{
                "success":true,
                "left":${newAmount}
            })`)//把函数的参数定为一个对象
        }else{
         
            response.statusCode = 400;//否则返回码为400,说明失败
            response.write('fail')
        }
        response.end()
    }

前端:

    window.xxx = function(result){
        if(result.success === true){
            amount.innerText = amount.innerText - 1;
        }
    }

5.2jsonp的含义和理解

我请求一个script,script调用一个函数的同时,把第一个参数设置为要返回的数据,不管这个数据是对象还是字符串.然后人们把这种调用script并返回字符串或者对象的使用方法叫做是jsonp,这就是jsonp!.只是名字比较奇怪,其实真名的名字应该是:动态标签跨域请求!即利用动态标签进行跨域请求的技术

这种方法的好处是可以解决跨域的问题.例如刚才的例子,两个网站如果域名不一样,那么第一个网址的前台和另一个网址后台的服务器无法进行数据交换(由于浏览器的同源策略).但是使用script标签就可以避免这个问题.利用script请求,可以请求道第二个网站的json或字符串数据,这种方法就叫做jsonp

JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的<script> 元素是一个例外。利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。----百度百科

jsonp要解决的是两个不同域名的网站如何交流
,答案是用script标签就可以交流了,因为script标签是不受域名限制的,而Ajax是受域名限制的

5.3什么是jsonp总结

jsonp
请求方:mataotao.com的前端(浏览器),一个网站的前端
响应方:jack.com的后端(服务器)另一个网站的后端

  1. 请求方创建一个script,src指向响应方,同时传一个查询参数?callbackName=xxx
  2. 响应方根据查询参数构造形如

    • xxx.call('undefined','你要的数据')
    • xxx("你要的数据")

      这样的响应

  3. 请求方浏览器接收到响应, 就会执行xxx.call('undefined','你要的数据')
  4. 这样请求方就得到了想要的数据

这就是jsonp

5.4 行业约定

jsonp没有标准的规范,但是为了统一,有了行业约定

  1. callbackName一律用callback
  2. xxx函数一律使用形如mataotao123124234的字符加上随机数的形式(这样的好处是不用取函数名了,而且调用完就delete,不会污染全局变量)

下面用行业约定来改写代码,加*的行为修改的代码

<script>
    let btn = document.getElementById("button");
    let amount = document.getElementById("amount")
    btn.onclick = function () {
        let script = document.createElement("script");
        let functionName = 'taotao' + parseInt(Math.random() * 10000, 10);//***随机生成一个函数名
        window[functionName] = function (result) {//***必须使用window[functionName]这样的形式
            if (result.success === true) {
                amount.innerText = amount.innerText - 1;
            }
        }
        script.src = "http://jack.com:8002/pay?callback=" + functionName;//***将随机生成的函数名放到拼接到查询字符串上

        document.body.appendChild(script);//这句话一定要加上,这是与使用图片请求不一样的地方

        script.onload = function (e) {//如果script请求成功(返回码以2开头)
            e.currentTarget.remove();//添加移除代码
            delete window[functionName];//***不管成功失败,最后都移除functionName
        }
        script.onerror = function (e) {//返回码是4开头
            alert("失败")
            e.currentTarget.remove();
            delete window[functionName];//***不管成功失败,最后都移除functionName
        }
    }
</script>
 response.write(`${query.callback}.call(undefined,{
                "success":true,
                "left":${newAmount}
            })`)

PaljxJ.png
1随机生成的查询字符串
2返回的根据随机生成的函数名的数据

6 使用jQuery中的jsonp

jQuery使用jsonp非常简单

只要这样修改前台的代码.后台不用改 url不需要写callback查询参数,因为jQuery会自动给你生成

btn.onclick = function () {

        $.ajax({
            url: "http://jack.com:8002/pay",
            jsonp: "callback",
            dataType: "jsonp",
            success: function (response) {
                if (response.success) {
                    amount.innerText = amount.innerText - 1;
                }
            }
        });
        
    }

Pa1uZt.png

Pa1TQH.png

需要注意的一点是,jsonp不是ajax中的一种.不要背jquery误导

7 面试题:jsonp为什么不能用post请求

答:

  1. jsonp是通过动态创建script实现的
  2. 动态创建script的时候只能用get,没有办法用post

6 什么json

JSON 语法

6.1JSON 语法规则

在 JS 语言中,一切都是对象。因此,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。但是对象和数组是比较特殊且常用的两种类型:

  • 对象表示为键值对
  • 数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组

6.2JSON 键/值对

JSON 键值对是用来保存 JS 对象的一种方式,和 JS 对象的写法也大同小异,键/值对组合中的键名写在前面并用双引号 "" 包裹,使用冒号 : 分隔,然后紧接着值:

{"firstName": "Json"}

这很容易理解,等价于这条 JavaScript 语句:

{firstName : "Json"}

6.3 JSON 与 JS 对象的关系

很多人搞不清楚 JSON 和 Js 对象的关系,甚至连谁是谁都不清楚。其实,可以这么理解:
JSON 是 JS 对象的字符串表示法,它使用文本表示一个 JS 对象的信息,本质是一个字符串

var obj = {a: 'Hello', b: 'World'};
//这是一个对象,注意键名也是可以使用引号包裹的
var json = '{"a": "Hello", "b": "World"}'; //这是一个 JSON 字符串,本质是一个字符串

6.4JSON 和 JS 对象互转

要实现从对象转换为 JSON 字符串,使用 JSON.stringify() 方法:

var json = JSON.stringify({a: 'Hello', b: 'World'}); //结果是 '{"a": "Hello", "b": "World"}'

要实现从 JSON 转换为对象,使用 JSON.parse() 方法:

var obj = JSON.parse('{"a": "Hello", "b": "World"}'); //结果是 {a: 'Hello', b: 'World'}

风彻
1.5k 声望142 粉丝

« 上一篇
无缝轮播