请求、异步、回调、Promise、解构赋值

简介

这是一个前端新人的关于请求与响应的笔记。

github 演示

效果预览图

基础准备

做一个服务器 server.js

server.js

var http = require('http')
var fs = require('fs')
var url = require('url')
var port = process.argv[2]

if (!port) {
  console.log('请指定端口号。\n例如:node server.js 8080')
  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

  /******** 路由代码开始 ********/

  console.log('含查询字符串的路径\n' + pathWithQuery)

  if (path === '/' || path === '/index.html') {
    // 1.请求 /index.html 或 /。返回 index.html 文件的字符串
    var string = fs.readFileSync('./index.html')
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    response.write(string)
    response.end()
  } else {
    // 0.请求失败。返回 '请求失败' 字符串。
    response.statusCode = 404
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    response.write('请求失败')
    response.end()
  }

  /******** 路由代码结束 ********/
})

server.listen(port)
console.log('监听 ' + port + ' 成功\n请用浏览器打开 http://localhost:' + port)

运行 server.js

node server.js 8080

请求测试

在地址栏输入 http://localhost:8080,请求的是 / ,返回的是 index.html 文件转成的字符串。
在地址栏输入 http://localhost:8080/hehe,请求的是 /hehe,返回的是 '请求失败' 字符串。

数据库是什么鬼

文件系统是一种数据库

MySQL 是一种数据库

只要能长久地存数据,就是数据库

用文件做一个数据库 money.db

创建 money.db 文件作为数据库

echo "99">money.db
// 创建一个文件money.db,内容为99,作为数据库。

money.db 的内容

99

index.html 用 &&&amount&&& 占位,等待数据替换。

<h5>余额<span id="amount">&&&amount&&&</span></h5>

服务器接受请求修改数据库并返回响应

server.js 当请求 /index.html 时,读取 money.db 替换到 index.html 的 &&&amount&&&

if (path === '/' || path === '/index.html') {
  // 请求 /index.html。返回 index.html 文件的字符串。
  var string = fs.readFileSync('./index.html', 'utf8')
  var amount = fs.readFileSync('./money.db', 'utf8')
  string = string.replace('&&&amount&&&', amount)

  response.setHeader('Content-Type', 'text/html;charset=utf-8')
  response.write(string)
  response.statusCode = 200
  response.end()
}

各种发请求

1.用 form 发请求

用 form 可以发 get 请求,但是会刷新页面或新开页面。

index.html

<h5>余额<span id="amount">&&&amount&&&</span></h5>
<form action="/formpay" method="POST">
    <input type="submit" value="付款">
</form>

server.js

if (path === '/formpay' && method.toUpperCase() === 'POST') {
  // 请求 /formpay 且类型是 POST。操作数据库,并返回 'success' 字符串。 
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  response.write('formpay success')
  response.statusCode = 200
  response.end()
}

浏览器收到 ‘pay success’ 的字符串,整个页面渲染成只有 ‘pay success’ 字符串的新页面。此时,后退到 index.html 并刷新,会请求 index.html 读取到新的余额。

用 iframe 接收 form 得到的响应页面

form 默认会在新页面展示接收到的 ‘pay success’ 字符串。

我们用 iframe 来展示,就不用跳转页面了。(注意:firefox可以,但chrome还是会跳转)

<h5>余额<span id="amount">&&&amount&&&</span></h5>
<form action="/pay" method="POST" target="result">
    <input type="submit" value="付款">
</form>
<iframe id="result" src="about:blank" frameborder="0"></iframe>

但是余额上的数字还是没法更新,因为数据库变了,但是当前页面没有请求并接收到新的数据库。

2.用 a 发请求

用 a 可以发 get 请求,但是会刷新页面或新开页面。

index.html

<a href="/apay">用a发请求</a>

server.js

if (path === '/apay') {
  // 请求 /apay。
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  response.write('apay success')
  response.statusCode = 200
  response.end()
}

3.用 img 发 get 请求

用 img 可以发 get 请求,可以局部刷新,但是只能以图片的形式展示。

index.html

//点击按钮,生成一个 img,img 的 src 发起 get 请求。
imgButton.addEventListener('click', (e) => {
    let image = document.createElement('img')
    image.src = '/imgpay'
    image.onload = function () {
        alert('img成功')
        // window.location.reload() // 1.成功后帮用户刷新页面
        amount.innerText = amount.innerText - 1 // 2.直接局部修改余额
    }
    image.onerror = function () {
        alert('img失败')
    }
})

server.js

//接收请求,操作数据库,返回图片、状态码、header。
if (path === '/imgpay') {
  // 请求 /imgpay。操作数据库,并返回图片。 
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  response.setHeader('Content-Type', 'images/jpg')
  response.write(fs.readFileSync('./success.jpg'))
  response.statusCode = 200
  response.end()
}

用 link 可以发 get 请求,但是只能以 CSS、favicon 的形式展示。

index.html

linkpay.onclick = function () {
    var link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = '/linkpay'
    document.head.appendChild(link)
}

server.js

if (path === '/linkpay') {
  // 请求 /linkpay。
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  response.write('apay success')
  response.statusCode = 200
  response.end()
}

5.用 script 发 get 请求

用 script 可以发 get 请求,局部刷新,只能 get 不能 post,但是只能以脚本的形式运行。

index.html

scriptButton.addEventListener('click', (e) => {
    let script = document.createElement('script')
    script.src = '/scriptpay'
    document.body.appendChild(script)
    script.onload = function (e) { // 状态码是 200~299 则表示成功      
        alert('scriptpay success')
        e.currentTarget.remove() // 把服务器返回的 JS 标签移除。
    }
    script.onerror = function (e) { // 状态码大于等于 400 则表示失败
        alert('scriptpay fail')
        e.currentTarget.remove()
    }
})

server.js

if (path === '/scriptpay') {
  // 请求 /scriptpay。操作数据库,并返回 script 语法的字符串。
  var amount = fs.readFileSync('./money.db', 'utf8')
  amount -= 1
  fs.writeFileSync('./money.db', amount)

  response.setHeader('Content-Type', 'application/javascript')
  response.write(`
    amount.innerText = amount.innerText - 1
  `)
  response.statusCode = 200
  response.end()
} 

浏览器收到复符合 JavaScript 语法的字符串,会执行它。

amount.innerText = amount.innerText - 1 // 假设之前是99,付款后数据库是98。

跨域 SRJ

这种技术叫做 SRJ - Server Rendered JavaScript,服务器返回的 JavaScript。

JavaScript 是可以跨域的

例如:在我们的网页引用 CDN 的 jQuery,我们的域名肯定跟 cdn.bootcss.com 这个域名不同啊!

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>

6.JSONP 跨域发请求

index.html

JSONPButton.addEventListener('click', (e) => {
    // 1.创建script
    var script = document.createElement('script')
    // 2.创建随机字符串
    var functionName = 'jaylan' + parseInt(Math.random() * 10000000, 10)
    // 3.创建一个函数
    window[functionName] = function (JSONStr) {
        amount.innerText = amount.innerText - 1
        alert(JSONStr)
    }
    // 4.用 script 发起请求。请求路径是 /JSONPpay,请求的查询参数是 callback=functionName。
    script.src = '/JSONPpay?callback=' + functionName
    // 5.把 script 插入 body,使得 script 生效执行。
    document.body.appendChild(script)

    script.onload = function (e) { // 状态码是 200~299 则表示成功 
        e.currentTarget.remove()
        delete window[functionName] // 请求完了就干掉这个随机函数
    }
    script.onerror = function (e) { // 状态码大于等于 400 则表示失败
        e.currentTarget.remove()
        delete window[functionName] // 请求完了就干掉这个随机函数
    }
})

server.js

if (path === '/jQueryJSONPpay') {
  // 请求 /jQueryJSONPpay。
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  let callback = query.callback
  response.setHeader('Content-Type', 'application/json')
  response.write(`
    ${callback}.call(undefined, {
      "success":true,
      "left":${newAmount}
    })
  `)

  response.statusCode = 200
  response.end()

JSONP 到底干了什么

浏览器做了一个 functionName 的函数, 用 script 发起 /JSONPpay?callback=functionName 的请求。

服务器收到这个请求,得到了 functionName 的函数名。服务器把 functionName.call(undefined, JSON) 这样的符合 JavaSCript 语法的字符串返回给浏览器。

浏览器收到返回的字符串,就执行 JavaScript,于是把 functionName 给 call 调用了。

JSONP的意义

JSONP 实现了:

前端做了一个函数,这个函数需要一个数据。

后端把需要的数据做成 JSON对象,并且做成调用这个函数的语句,返回给前端。

前端拿到语句,调用了函数。

JSONP为什么不能用POST

因为 JSONP 是通过动态创建 script 实现的,而 script 只能发 get 不能发 post。

通过jQuery来使用JSONP更方便

// 使用JSONP。jQuery把JSONP归类在ajax,不用在意。
$.ajax({
    url: "http://localhost:8888/jQueryJSONPpay",
    jsonp: "callback",
    dataType: "jsonp",

    // 请求的 JSON 内容
    // data: {
    //     q: "select title,abstract,url from search.news where query=\"cat\"",
    //     format: "json"
    // },

    //  请求成功后执行的函数
    success: function (response) {
        if (response.success) {
            amount.innerText = amount.innerText - 1
        }
    }
})

7.用XMLHttpRequest发请求 XML响应

用 XMLHttpRequest 发请求,响应回 XML 语法的字符串,解析 XML 当作 DOM 来用。

index.html

XMLpay.addEventListener('click', (e) => {
    // 1.构造 XMLHttpRequest 请求
    let request = new XMLHttpRequest() 
    // 2.监控请求的状态变化
    request.onreadystatechange = () => { 
        if (request.readyState === 4) {
            // 状态4===响应完毕
            if (request.status >= 200 && request.status < 300) {
                console.log('请求成功')
                // 把xml转化为dom
                let parser = new DOMParser()
                let xmlDoc = parser.parseFromString(request.responseText, "text/html")
                // 用dom一样来操作xml
                let heading = xmlDoc.getElementsByTagName('heading')[0].textContent
                console.log('打印响应xml的heading内容:' + heading)
            } else if (request.status >= 400) {
                console.log('请求失败')
            }
        }
    }
    // 3.配置请求
    request.open('GET', './XMLpay') 
    // 4.发送请求
    request.send() 
})

sercer.js

if (path === '/XMLpay') {
  // 请求 /XMLpay。
  var amount = fs.readFileSync('./money.db', 'utf8')
  var newAmount = amount - 1
  fs.writeFileSync('./money.db', newAmount)

  response.statusCode = 200
  response.setHeader('Content-Type', 'text/xml;charset=utf-8')
  // XML语法的字符串
  response.write(`
      <note>
        <to>sakura</to>
        <form>jaylan</form>
        <heading>welcome</heading>
        <body>bengbengbeng</body>
      </note>
    `)
  response.end()
}

8.用XMLHttpRequest发请求 JSON响应

响应 XML 太麻烦了,不好用,于是用 JSON 代替 XML,JSON 是轻量级的数据交换语言。

index.html

// 1.构造 XMLHttpRequest 请求
let request = new XMLHttpRequest()
// 2.监控请求的状态变化
request.onreadystatechange = () => {
    if (request.readyState === 4) {
        // 状态4===响应完毕
        if (request.status >= 200 && request.status < 300) {
            console.log('请求成功')
            let string = request.responseText
            // 把符合 JSON 语法的字符串,转换成 JS 对应的值。
            let object = window.JSON.parse(string)
            // JSON.parse 是浏览器提供的
            console.log(typeof object)
            console.log(object)
        } else if (request.status >= 400) {
            console.log('请求失败')
        }
    }
}
// 3.配置请求
request.open('GET', './JSONpay')
// 4.发送请求
request.send()

readyState请求的5种状态

状态 描述
0 UNSENT 代理被创建,但尚未调用 open() 方法。
1 OPENED open() 方法已经被调用。
2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
3 LOADING 响应下载中; responseText 属性已经包含部分数据。
4 DONE 响应下载操作已完成。

server.js

if (path === '/JSONpay') {
    // 请求 /JSONpay。
    var amount = fs.readFileSync('./money.db', 'utf8')
    var newAmount = amount - 1
    fs.writeFileSync('./money.db', newAmount)

    response.statusCode = 200
    response.setHeader('Content-Type', 'text/json;charset=utf-8')
    // response.setHeader('Access-Control-Allow-Origin', 'http://localhost:8888/')
    // JSON语法的字符串
    response.write(`{
      "note":{
        "to":"八重樱",
        "from":"飞鱼丸",
        "heading":"嘤嘤嘤",
        "body":"大姐你回来啦"
      }
    }`)
    response.end()
  }

JavaScript 与 JSON

  1. JSON 没有 function 和 undefined。

  2. JSON 的字符串首尾必须是双引号”。

JavaScript JSON
undefined 没有
null null
[‘a’,‘b’] [“a”,“b”]
function fn(){} 没有
{name:‘sakura’} {“name”:“sakura”}
‘sakura’ “sakura”
var a = {} a.self = a 没有变量
{__proto__} 没有原型链

JSON官网

9.用AJAX发请求

index.html

AJAXpay.addEventListener('click', (e) => {
    let request = new XMLHttpRequest()
    request.onreadystatechange = () => {
        if (request.readyState === 4) {
            if (request.status >= 200 && request.status < 300) {
                successFn.call(undefined, arg)
            } else if (request.status >= 400) {
                failFn.call(undefined.arg)
            }
        }
    }
    request.open('GET', './AJAXpay')
    request.send()
})

什么是AJAX

AJAX不是JavaScript的规范,它只是一个哥们Jesse James Garrett“发明”的缩写:

Asynchronous JavaScript and XML

异步的 JavaScript 和 XML

意思就是用JavaScript执行异步网络请求。

用JavaScript发请求,用JavaScript处理响应。

  1. 使用XMLHttpRequest发请求。

  2. 服务器返回符合XML格式的字符串。

  3. JS解析XML,并更新局部页面。

  4. 因为XML不好用,后来人们用JSON替代了XML。

金句:

  1. 你才返回对象,你全家都返回对象
  2. JS 是一门语言,JSON 是另一门语言,JSON 这门语言抄袭了 JS这门语言
  3. AJAX 就是用 JS 发请求
  4. 响应的第四部分是字符串,可以用 JSON 语法表示一个对象,也可以用 JSON 语法表示一个数组,还可以用 XML 语法,还可以用 HTML 语法,还可以用 CSS 语法,还可以用 JS 语法,还可以用我自创的语法

只有 AJAX 要同源策略,其他 a\img\form\link\script 都可以跨源。

只有 协议+端口+域名 一模一样才允许发 AJAX 请求

一模一样一模一样一模一样一模一样一模一样一模一样一模一样一模一样

  1. http://baidu.com 可以向 http://www.baidu.com 发 AJAX 请求吗 no
  2. http://baidu.com:80 可以向 http://baidu.com:81 发 AJAX 请求吗 no

浏览器必须保证 只有 协议+端口+域名 一模一样才允许发 AJAX 请求 CORS 可以告诉浏览器,我俩一家的,别阻止他

突破同源策略 === 跨域

CORS 跨域

Cross-Origin Resource Sharing

server.js

// CORS 跨源共享给http://frank.com:8001
response.setHeader('Access-Control-Allow-Origin', 'http://frank.com:8001')

AJAX 的所有功能

  • 客户端的JS发起请求(浏览器上的)
  • 服务端的JS发送响应(Node.js上的)

JS 可以设置任意请求 header

第一部分 request.open('get', '/xxx')

第二部分 request.setHeader('content-type','x-www-form-urlencoded')

第四部分 request.send('a=1&b=2')

JS 可以获取任意响应 header

第一部分 request.status / request.statusText

第二部分 request.getResponseHeader() / request.getAllResponseHeaders()

第四部分 request.responseText

10.自制 window.jQuery.ajax

window.jQuery.ajax = function (options) {
    // 代码
}

AJAX 的回调

window.myQuery.ajax = function ({url,method,body,successFn,failFn,}) {
    let request = new XMLHttpRequest()
    request.open(method, url)
    request.onreadystatechange = () => {
        if (request.readyState === 4) {
            if (request.status >= 200 && request.status < 300) {
                // request.onreadystatechange 触发时,call successFn
                successFn.call(undefined, request.responseText)
            } else if (request.status >= 400) {
                failFn.call(undefined, request)
            }
        }
    }
    request.send(body)
}
// 使用方代码
myQAJAX.addEventListener('click', (e) => {
    window.myQuery.ajax({
        url: '/xxx',
        method: 'post',
        body: 'a=1&b=2',
        // 使用方写的函数,使用方不call
        successFn: (responseText) => {
            console.log(1)
        },
        failFn: (request) => {
            console.log(2)
        }
    })
})
// myQAJAX 按钮按下,调用 ajax(),写了一个 successFn 函数,但是它没调用。
// request.onreadystatechange 监听变化,触发 successFn.call。
// successFn:() =>{} 这个函数就是回调函数。

同步、异步、回调

同步:一定要等任务执行完了,得到结果,才执行下一个任务。

function taskSync() {
    return '同步任务的返回值'
}

var result = taskSync() // 那么 result 就是同步任务的结果
otherTask() // 然后执行下一个任务

异步:不等任务执行完,直接执行下一个任务。

function taskAsync() {
    var result = setTimeout(function () {
        console.log('异步任务的结果')
    }, 3000)
    return result // 返回 setTimeout 的 TimerID
}

var result = taskAsync() // result 不是异步任务的结果,而是一个 timer id
otherTask() // 立即执行其他任务,不等异步任务结束

聪明的你可能会发现,我们拿到的 result 不是异步执行的结果,而是一个 timer id,那么要怎么拿到异步任务的结果呢?

用回调。

改下代码如下:

function taskAsync(callback) {
    var result = setTimeout(function () {
        callback('异步任务的结果')
    }, 3000)
    return result
}

function callback(result) {
    console.log(result) // 三秒钟后,这个 callback 函数会被执行
}

taskAsync.call(callback) // taskAsync 来调用
otherTask() // 立即执行其他任务,不等异步任务结束

所以「回调」经常用于获取「异步任务」的结果。

回调的问题

问题是每个程序员的回调名不一样

Promise 解决了这个问题

请背下这个代码

function xxx() {
    return new Promise((f1, f2) => {
        doSomething()
        setTimeout(() => {
            // 成功就调用 f1,失败就调用 f2
        }, 3000)
    })
}
xxx().then(success, fail)

// 链式操作
xxx().then(success, fail).then(success, fail)

ES6的解构赋值

解构赋值语法是一种 Javascript 表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

let a = obj.a
let b = obj.b
let c = obj.c
// 等价于下面的这行代码。ES6 解构赋值。
let{a,b,c}= obj
var a = 'a';
var b = 'b';
[a, b] = [b, a] // 交换变量 a b 的值。