JSONP技术栈

简单的前后端交互

前面学习了这么多,都是在和页面打交道,不管是HTML、CSS、JavaScript,DOM,都没有跑出浏览器,那今天来学习下和后台交互。

当我点击付款按钮时,页面的数值会减小;当我刷新页面时,内容不会改变,该怎么实现呢?

先写一个简单的node.js脚本,让页面能够正常运行

  1. 创建一个文件夹里面创建两个文件
  2. 新建一个脚本文件
  3. 新建一个index.html文件
  4. 打开服务器,就可以看到自己的页面了

脚本文件

  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 parsedUrl = url.parse(request.url, true)
  var pathWithQuery = request.url 
  var queryString = ''
  if(pathWithQuery.indexOf('?') >= 0){ queryString = pathWithQuery.substring(pathWithQuery.indexOf('?')) }
  var path = parsedUrl.pathname
  var query = parsedUrl.query
  var method = request.method


  /******** 从这里开始看,上面不要看 ************/

  if(path === '/'){
    var string = fs.readFileSync('./index.html','utf8')
    response.setHeader('Content-Type','text/html;charset=utf-8')
    response.write(string)
    response.end()
  }else if(path === '/css/style.css'){
    var string = fs.readFileSync('./css/style.css','utf8')
    response.setHeader('Content-Type','text/css')
    response.write(string)
    response.end()
  }else if(path === '/js/main.js'){
    var string = fs.readFileSync('./js/main.js','utf8')
    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.end('找不到对应的路径,你需要自行修改 index.js')
  }
  
  /******** 代码结束,下面不要看 ************/
})

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

HTML 文件

<!DOCTYPE html>
<html>
<head>
    <title>首页</title>
</head>
<body>
    <h5>你的余额是<span id="amount">100</span></h5>
    <button id="button">付款</button>
    <script>
        let button = document.getElementById('button')
        let amount = document.getElementById('amount')
        
        button.addEventListener('click',function(){
            amount.textContent = +amount.textContent - 1
            console.log(1)
        })
    </script>
</body>
</html>

当我们点付款的时候,100会往下减小,但是当我们刷新页面的时候,又变成了100。

这里我们只是在浏览器上面操作页面内容,数据没有长久的存储在数据库里面,所以当我们刷新页面时,它又回到初始状态,这显然不是我们要的效果。

我们该怎么解决这个问题呢?

用占位符替换页面数据

我们在刚才的当前文件夹中新建一个文件,作为我们的数据库,现在我们把100元写在数据库里。

数据库简单的讲就是一个能长久存储数据的地方,文件是数据库最简单的形式。

touch db
echo '100' > db

index.html中的100用一个占位符替代,这个占位符要和页面中其他变量不重复,实际上页面上的数据,前端是不需要知道的,用一个占位符站位即可,发送请求,后端程序员去读数据库里的内容,返回给前端。

<span id="amount">&&&amount&&&</span>

在脚本中if(path === '/index.html')里加一个替换占位符语句。

var amount = fs.readFileSync('./index','utf8')      //文件中的数据类型是String
string = string.replace('&&&amount&&&',amount)

重启服务器后,刷新页面后我们看到的100是数据库里的数据,当我们点击付款时,变化的是数据库里面的数据,和前端没有关系,但是当我们刷新页面后,依旧回到初始状态,没有保存最新的数据。

我点付款的时候应该发送一个请求告诉服务器,请把数据库里的100变成99,然后刷新页面;我不对页面做操作改变它的数据了,那我点付款的时候,发起一个请求,应该怎么做呢?

可以发请求的标签有imglinkscriptform表单,当我们点击按钮应该发送POST请求,因为是更新数据库内容,所以这里只有form表单能发送POST请求。

form表单发请求

<form action="pay" method="post">
    <input type="submit" value="付款">
</form> 

在服务器里面增加一个/pay请求路径。

if(path === '/pay' && method.toUpperCase() === 'POST'){
    var amount = fs.readFileSync('./db','utf8')
    var newAmount = amount - 1
    fs.writeFileSync('./db',newAmount)
    response.write('success')       //成功后给用户返回
    response.end()
}

当我点付款的时候,会看当前页面会跳转到/pay路径下的页面,表示成功了。

点击浏览器的返回上一页,刷新下当前页面,之前的100变成了99,无论怎么刷新或者重新打开,数据都是之前的操作过结束后的数据。

这里前端要写的就是form表单,后端如果发现是某个路径并且是POST请求,就去操作数据库。

这是旧时代的操作,form表单一旦提交了都会刷新当前页面,给用户造成了不好的体验。有个程序员想出了用iframe解决页面刷新的问题,操作成功后在iframe打卡跳转页面。

iframe 表单刷新页面

<form action="pay" method="post" target="success">
    <button id="button">付款</button>
</form>
<iframe name="success" src="about:blank" frameborder="0"></iframe>

有个程序员觉得页面中多出一个东西总是怪怪的,绞尽脑汁又想出了动态创建img标签的方法

动态创建img标签发送请求

有洁癖的程序员总是能想出更好的解决方法,接着来看下动态创建img标签的发送请求的方法。

脚本中增加/pay路径下应该这样写

 if(path === '/pay'){
    var string = fs.readFileSync('./db','utf8')
    var newAmount = string - 1
    if(Math.random() > 0.5){
        fs.writeFileSync('./db',newAmount)
        response.setHeader('Content-type','image/jpeg')
        response.statusCode = 200
        response.write(fs.readFileSync('./1.jpeg'))
    }else{
        response.statusCode = 400
        response.write('alert("fail")')
    }
    response.end()
  }

因为img标签,只能发送GET请求,所以这里就不做method判断了。

JS 文件

button.addEventListener('click',function(e){
    let image = document.createElement('img')
    image.src = '/pay'
    img.onload = function(){
        alert('success')
        window.location.reload()
    }
    img.onerror = function(){
        alert('fail')
    }
})

onloadonerror是提示用户成功了还是失败,在onload里面加上一个window.location.reload()成功后会自动刷新页面。

动态创建img标签的方法,必须要返回真实的图片,浏览器才能知道操作成功了,不然onload一直不会成功,虽然数据库已经修改成功了,但浏览器只要没接收到图片,就会执行onerror,这也是它的局限所在——必须要返回真是图片。

同时刷新页面会造成浏览器重新渲染,所以当浏览器接收到响应时,前端应该在页面上自动减1,用户并不会知道这中间发生了什么。

动态创建script标签——SRJ方案

a标签发送请求太浪费资源了,这时又有人想出了用script标签发请求,这就是优秀程序员和普通程序员之间的差距啊。

下面来看看是怎么实现的:

button.addEventListener('click',function(){
    var script = document.createElement('script')
    script.src = '/pay'
    document.body.appendChild(script)   //必须要将创建出来的Script放在页面中才可以
    script.onload = function(){
        alert('sucess')
    }
})

/pay路径下的代码

if(path === '/pay'){  
    var string = fs.readFileSync('./db','utf8')
    var newAmount = string - 1
    fs.writeFileSync('./db',newAmount)
    response.setHeader('Content-type','application/js')
    response.statusCode = 200
    response.write('alert("success1")')
    response.end()
  }

当我点付款时,成功后首先执行脚本里面的script,执行完了之后才执行main.js内的onload

因为脚本中的script先执行,所以main.js里面就不需要提示用户了,直接后台给提示内容就可以了。

如下:

response.write(`alert("success")
amount.innerText = amount.innerText -1`)    //ES6字符串方法

到这里聪明的你应该也发现了一个问题,当我点付款时,不管成功与否都会创建一个script,对于程序员来说是无法接受的,所以要用onloadonerror去监听,不管成功与否都将它删除掉。这里虽然删掉了但它还是在内存中。

script.onload = function(e){
    e.currentTarget.remove()
}
script.onerror = function(e){
    e.currentTarget.remove()
}

动态创建script方法发送请求叫做——SRJ方案(全称 Server rendered javascript),在 ajax 出来以前,无刷新局部更新页面内容的最好的方案。

请求另一个网站的script

在页面中引入一个script时,一定要在当前域名吗?

NO!!!我们在页面中引入的各种库,不都是引入别人的网站的script

那这样的话,是不是可以操作别人网站的/pay,所以GET请求太不安全,太容易伪造了,所以大部分的/pay都用POST请求去做。

PORT=8002 node index.js 可以开多个端口

SRJ方案前后端耦合太紧密了,需要后端对页面了解太清楚。

其实前端提供一个xxx() API就可以了。

response.write(`
xxx.call(undefined,'success')
`)

前端提供 API 的方法,其实解耦还没有解的很干净,我们在设置scriptsrc时可以直接设置请求参数,脚本只需要取这个参数就可以了,至于具体叫什么名字不重要

script.src = 'http://baidu.com:8002/pay?callbackName=xxx'
response.write(`
    ${query.callbackName}.call(undefined,'success')
`)

到这里已经是很好的方案了,但是有一个问题是,调用函数传递的参数,前端怎么知道呢?如果不确定,到时候出了问题就要各自扯皮了,这时候JSON应运而出。

JSONP方案

JSONPJSON的格式进行参数传递,解决了两个网站之间的交流。至于为什么叫JSONP,应该是大括号左边的叫做左padding,右边的叫做右padding,连接起来就叫做JSONP

${query.callbackName}.call(undefined,{
    "success": true
    "left": ${newAmount}
})

用文字叙述 JSONP

请求方:qq.com的前端程序员(浏览器)
响应方:baidu.com的后端程序员(服务器)

  1. 请求方创建script ,src指向相应方同事传一个查询参数 ?callbackName=xxx
  2. 响应方根据查询参数callbackName,构造形如xxx.call(undefined,'你要的数据')这样的响应
  3. 浏览器接收到响应,xxx.call(undefined,'你要的数据')
  4. 那么请求方就知道了它要的数据
    这就是JSONP

约定:

  1. callbackName -> callback
  2. xxx -> 随机数

按照约定写一下

button.addEventListener('click',funcion(e){
    let script = document.createElement('script')
    let functionName = parseInt(Math.random()*1000000)      //这个函数名是随机数
    window[functionName] = function(result){    //result是服务器返回的结果
        if(result === 'success'){
            amount.innerText = amount.innerText - 1
        }
    }

    script.src = 'http://baidu.com:8002?callback=' + functionName   //写在参数里面
    document.body.appendChild(script)
    script.onload = function(e){
        e.currentTarget.remove()
        delete window[functionName] //如果成功了要干掉这个函数
    }
    script.onerror = function(e){
        alert("false")
        e.currentTarget.remove()
        delete window[functionName] //如果失败了也要干掉这个函数

    }
})

jQuery实现

用jQuery就能非常方便的使用

button.addEventListener('click',function(){
    $.ajax({
        url: "http://baidu.com:8002/pay"
        dataType: "JSONP"
        success:function(response){
            console.log(response)
        }
    })
})

JSONP为什么不支持POST请求

  1. 因为JSONP是通过动态创建script实现的
  2. 动态创建script只有GET请求没有POST请求
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容