JS20 - AJAX - 查询天气、地区选择、模拟待办事项功能、Promise、fetch、JSONP

292 阅读25分钟

数据交互的方式

传统请求及缺点

  • 传统请求方式:浏览器地址栏、超链接、form表单、DOM/BOM 方法等
  • 传统请求不足:页面会全部刷新,且传统请求有时间差,导致用户体验不好

传统请求方式.png

Ajax 请求

  • 定义AJAX 全称 Asynchronous JavaScript And Xml,就是异步的 JavaScript 和 XML,因此其本质不是一种新技术,而是 Jesse James Garrett 提出的新术语,用来表示集合多种技术(包含HTML、CSS、JavaScript、XML、XSLT 和 XMLHttpRequest)的新方法。
  • 作用:使用结合多种技术的 AJAX 模型,web能快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,从而更快地回应用户的操作。
  • 目的:通过“异步”特性,使用 XMLHttpRequest 对象与服务器通信,即可以在不重新刷新页面的情况下(HTML中的form表单发送数据会刷新页面)与服务器通信,交换数据,更新页面。
  • JSON 与 XML:js 对于xml解析不友好,对json数据解析更友好,但后端语言如Java对xml解析更友好。
  • 接口文档:在前后端分离的开发中,由于前后端的数据交互需要,开发者一般会约定一个统一的文件进行沟通交流开发并方便不同人员查看、维护,这个文件就是接口文档,接口规范一般包括四部分:请求方式、请求地址、请求参数说明、响应参数说明
  • 异步/同步:异步:JavaScript无需等待服务器响应,而是再等待服务器响应时执行其它脚本,当响应就绪后对响应进行处理;同步:JavaScript 会等到服务器相应就绪才继续执行,如果服务器缓慢,应用程序就会被挂起,因此对于服务器不繁忙的小型请求是可以的。
AJAX.png

AJAX Getting Started

Step 1 - 创建 ajax 对象

说明:JavaScript 向服务器发送一个 http 请求也是需要实现的功能,那么由谁来实现,就需要一个 包含必要函数功能的对象实例。这就是为什么会有 XMLHttpRequest 的原因。这是 IE 浏览器的 ActiveX 对象 XMLHTTP 的前身。随后 Mozilla,Safari 和其他浏览器也都实现了类似的方法,被称为 XMLHttpRequest,同时,微软也实现了 XMLHttpRequest 方法。

/** 1. 创建 httpRequest 
 *  XMLHttpRequest 是一个内置的构造函数,可以直接由window调用
 *  console.log(window.XMLHttpRequest); //ƒ XMLHttpRequest() { [native code] }
 */
if (window.XMLHttpRequest) { // Mozilla, Safari, IE7+ ...
    httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE 6 and older
    httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}

Step 2 - 配置请求信息

说明httpRequest.open("method","url","async") 方法配置请求参数。method 表示HTTP请求方法;url 表示请求地址的url,就是要发送给谁,由于安全原因,默认不能调用第三方 URL 域名,即默认不允许跨域;async 设置是否异步请求,true和默认都表示异步,开启之后,JS就不会在此语句阻塞,能够是用户在服务器还没有响应的情况下与页面交互。method 和 url 一般会在接口文档中给定。

跨域问题的原因:由于安全原因,默认不能调用第三方 URL 域名,即默认不允许跨域,这个源于浏览器的同源策略,这里说的同源指的是三个相同才是同源(同协议、同域名、同端口号),有一个不相同就是跨域

跨域问题的特点与解决:只有浏览器端与服务器端才会产生跨域问题,服务器端与服务器端之间没有跨域问题,因此其中一种解决跨域问题的方式就是,通过请求代理,即本地浏览器请求本地服务器,然后再由本地服务器连接到远程服务器

// 2. 配置 open(请求方式, 请求地址, 是否异步)
httpRequest.open("GET", "http://127.0.0.1:5500/Codes/1.txt", true);

Step 3 - 配置响应事件

  • 请求完成,是指本次请求发送出去,服务器收到了我们的请求,并且服务器返回的信息已经回到浏览器
  • readyState(ajax 状态码)
    • 0 - 未初始化
    • 1 - 正在请求 or 建立服务器链接
    • 2 - 请求成功 or 服务器已接受请求
    • 3 - 正在响应 or 服务器正处理请求
    • 4 - 完成响应 or 请求已完成且响应已准备好 -> 完成响应不一定代表数据传输成功了,如果没有资源,此时请求响应虽然已经完成,但资源传输是失败的
  • status(http 状态码)

    200(成功)、404(未找到)、403(禁止)、500(服务器内部错误)

  • 如何获取返回信息httpRequest.responseText httpRequest.responseXML
    • httpRequest.responseText:服务器以文本字符的形式返回。
    • httpRequest.responseXML:以 XMLDocument 对象方式返回,之后就可以使用 JavaScript 来处理。
// 3. 配置请求完成后的触发事件
httpRequest.onload = function(){
    //获取返回的信息
    document.write(httpRequest.responseText);
    //请求并相应成功,可通过http状态码判断
    if(httpRequest.status === 200){
        // Success.
    } else {
        // There was a problem with the request.
    }
}
  • 区别:load 事件 和 readystatechange 事件
// 区别:onload 和 onreadystatechange
// onload 在请求完成后(readyState === 4)才会触发,因此,整个函数只执行一次,因而,只需要判断 status 是否为 200
httpRequest.onload = function(){
    if(httpRequest.status === 200){
        console.log(httpRequest.readyState);  //4
        console.log(httpRequest.status);      //200
    }
}
// onreadystatechange 可以监听请求过程中不同的状态,不只执行一次
httpRequest.onreadystatechange = function () {
    switch (httpRequest.readyState) {
        case 0: document.write(`请求状态码: ${httpRequest.readyState} - 请求回传内容:${httpRequest.responseText} <br/>`); break
        //未执行
        case 1: document.write(`请求状态码: ${httpRequest.readyState} - 请求回传内容:${httpRequest.responseText} <br/>`); break
        //未执行
        case 2: document.write(`请求状态码: ${httpRequest.readyState} - 请求回传内容:${httpRequest.responseText} <br/>`); break
        //请求状态码: 2 - 请求回传内容:(空)-> 加载链接完成,请求已接受
        case 3: document.write(`请求状态码: ${httpRequest.readyState} - 请求回传内容:${httpRequest.responseText} <br/>`); break
        //请求状态码: 3 - 请求回传内容:Hello World -> 请求码3开始逐步处理请求,如果内容少则效果与请求码4相同
        case 4: document.write(`请求状态码: ${httpRequest.readyState} - 请求回传内容:${httpRequest.responseText} <br/>`); break
        //请求状态码: 4 - 请求回传内容:Hello World -> 请求处理全部完成
    }
}

Step 4 - 发送请求

说明send() 方法的参数可以是任何你想发送给服务器的内容

// 4. 将本次请求发送到服务器,此处也可以放在 step 2 与 open 函数一起配置;
httpRequest.send();

demo 演示

//GET 请求
if(window.XMLHttpRequest){
    var httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject){
    var httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
} //else none
httpRequest.open("GET","http://127.0.0.1:5500/Codes/1.txt");
//注意:如果设置的是同步执行,则必须将send放在最后,
//     因为使用同步的话,最等每行代码执行完毕再往下执行,
//     而此时,send已经把数据发送出去,但httpRequest并没有绑定回调函数,
//     后端返回的值就没有绑法执行回调函数的内容
//     因此,如果是同步请求只能将 send() 代码放在最后
httpRequest.send();  
httpRequest.onload = function(){
    if(httpRequest.status === 200 && httpRequest.readyState === 4){
        document.write("返回数据:"+httpRequest.responseText);
    } else {
        document.write("错误码:"+httpRequest.status);
    }
}

解析 JSON 格式字符串

JSON 文本

  • JavaScript Object Notation (JSON JavaScript对象表示法) 是一种数据交换格式
  • JSON 独立于语言和平台:JavaScript 不是 JSON,JSON 也不是 JavaScript 的一部分,因此 JSON 并不是 JavaScript 的子集。JSON 可以表示数字、布尔值、字符串、null、数组(有序序列),以及由这些值组成的对象(字符串与值的映射)。JSON 不支持复杂的数据类型(函数、正则表达式、日期等)。日期对象默认会转化为 ISO 格式的字符串,因此信息不会完全丢失。如果需要使用 JSON 来表示复杂的数据类型,要转化为字符串值。
  • JSON 是存储和交换文本信息的语法,类似 XML,比 XML 更小、更快、更易解析。
{
  "shoppingcart": [
    {
      "id": 0,
      "store": "小米旗舰店",
      "products": [
        {
          "id": 0,
          "name": "软件"
        }
      ]
    },
    {
      "id": 1,
      "store": "JU旗舰店",
      "products": [
        {
          "id": 0,
          "price": "153.99"
        }
      ]
    },
    {
      "id": 2,
      "store": "opsu旗舰店",
      "products": [
        {
          "id": 0,
          "name": "add the chosen item highlight style and remove all the default and the other items' highlight style",
          "url": "https://img.com/",
          "describe-size": "56*16*3(cm)"
        },
        {
          "id": 1,
          "name": "官方提供在线图片压缩软件实现,实现一键压缩图片大小",
          "url": "https://img.com/dc92012",
          "describe-specs": "默认"
        }
      ]
    }
  ]
}

与 XML 相同之处

  • JSON 是纯文本
  • JSON 具有"自我描述性"(人类可读)
  • JSON 具有层级结构(值中存在值)
  • JSON 可通过 JavaScript 进行解析
  • JSON 数据可使用 AJAX 进行传输

与 XML 不同之处

  • 没有结束标签
  • 更短
  • 读写的速度更快
  • 能够使用内建的 JavaScript eval() 方法进行解析
  • 使用数组
  • 不使用保留字

JSON.parse() 方法

  • 功能:方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换 (操作)。
  • 语法JSON.parse(text[, reviver])
  • 说明
    • JSON.parse() 不允许用逗号作为结尾:JSON.parse('{"foo" : 1, }'); 会报错
    • text - 要被解析成 JavaScript 值的字符串;
    • reviver 可选 - 转换器,会将解析出的 JavaScript 值(解析值)会经过一次转换后才将被最终返回(返回值)。解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用 reviver 函数,在调用过程中,当前属性所属的对象会作为 this 值,当前属性名和属性值会分别作为第一个和第二个参数传入 reviver 中。如果 reviver 返回 undefined,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。当遍历到最顶层的值(解析值)时,传入 reviver 函数的参数会是空字符串 ""(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的 this 值会是 {"": 修改过的解析值},在编写 reviver 函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历)
      JSON.parse('{"p": 5}', function (k, v) {
            if(k === '') return v;     // 如果到了最顶层,则直接返回属性值,
            return v * 2;              // 否则将属性值变为原来的 2 倍。
        });                            // { p: 10 }
      
        JSON.parse('{"1": 1, "2": 2,"3": {"4": 4, "5": {"6": 6}}}', function (k, v) {
            console.log(k); // 输出当前的属性名,从而得知遍历顺序是从内向外的,
                            // 最后一个属性名会是个空字符串。
            return v;       // 返回原始属性值,相当于没有传递 reviver 参数。
        });
      
        // 1
        // 2
        // 4
        // 6
        // 5
        // 3
        // ""
      

JSON.stringify() 方法

  • 功能:方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。注意,BigInt 对象不能 JSON 序列化
  • 语法JSON.stringify(value, replacer, space);
  • 返回:JSON 字符串
  • 说明
    • value - 待序列化成 JSON 字符串的 JavaScript 对象。
    • replacer 可选 - 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
    • space 可选 - 指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。
console.log(JSON.stringify({}));                    //"{}"
console.log(JSON.stringify(true));                  //"true"
console.log(JSON.stringify("foo"));                 //"foo"
console.log(JSON.stringify([1, "false", false]));   //"[]"
console.log(JSON.stringify({num:5}));               //"{num:5}"
console.log(JSON.stringify(new Number(10)))         //"10"
console.log(JSON.stringify(new Number(10)))         //"10"

JSON 解析后端数据

关键代码:JSON.parse(httpRequest.responseText);

//JSON 文件部分 -> 1.JSON
{"name":"James","age":28,"section":"Developer"}
if(window.XMLHttpRequest){
    var httpRequest = new XMLHttpRequest();
} else if(window.ActiveXObject){
    var httpRequest = new ActivexObject("Microsoft.XMLHTTP");
} //else none
httpRequest.open("GET","http://127.0.0.1:5500/Codes/1.JSON");
httpRequest.send();
httpRequest.onload = function(){
    if(httpRequest.status === 200){
        let result = JSON.parse(httpRequest.responseText);
        console.log(result);    //{name: 'James', age: 28, section: 'Developer'}
    } else {
        console.log("error:"+httpRequest.status);
    }
}

HTTP 请求方式

注意:参数由后端确定,前端不能随意写。

GET 请求 - 获取

  • 功能:获取,类似数据库的select操作
  • 结果:仅查询数据,不会修改数据
  • 参数:主要以查询字符串的形式提交给后端,即告诉后端我们希望获取的是什么,如果不书写,将获取所有数据
  • 参数书写位置:直接在请求地址后面加 ? ,拼接 key=value&key2=value2
  • 大小限制:2KB
<button id="get">get</button>
<script>
  get.onclick = function(){
    if(window.XMLHttpRequest){
      var xhr = new XMLHttpRequest();
    } else if(window.ActiveXObject){
      var xh = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.open("get","http://localhost:3000/shoppingcart?store=JU旗舰店");
    xhr.onload = function(){
      if(xhr.status === 200){
        console.log(JSON.parse(xhr.responseText));
      }
    }
    xhr.send();
  };
</script>

POST 请求 - 提交

  • 语义化:偏向于提交的语义化,例如登录注册页面
  • 参数:参数格式多样,但需要特殊说明;
  • 参数书写位置:在 send(key=value&key2=value2) 请求体内。
  • 大小限制:没有限制
  • 特殊说明
    • httpRequest.setRequestHeader("content-type","application/x-www-form-urlencoded"); 设置提交的格式为 form 表单格式,即 send(key=value&key2=value2)
    • httpRequest.setRequestHeader("content-type","application/json"); 设置提交的格式为 json 数据格式,即 send(JSON.stringify({key:valye,key2:value2}));
<button id="post">post</button>
<script>
  post.onclick = function(){
    if(window.XMLHttpRequest){
      var xhr = new XMLHttpRequest();
    }else if(window.ActiveXObject){
      var xh = new ActiveXObject("Microsoft.XMLHTTP");
    }    
    xhr.open("post","http://localhost:3000/shoppingcart");
    //post 请求需要设置请求头
    // xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");
    xhr.setRequestHeader("Content-Type","application/JSON");
    xhr.onload = function(){
      if(xhr.status === 200){
        console.log(JSON.parse(xhr.responseText));
      }
    }
    //方式1:(一维)表单字符串 store=NEW旗舰店       
    // 需要将请求头设置为 application/x-www-form-urlencoded 格式
    //      xhr.send("store=NEW旗舰店")

    //方式2:(二维)JSON 对象  
    //      需要将请求头设置为 application/JSON
    //      xhr.setRequestHeader("Content-Type","application/JSON");
    //      xhr.send(JSON.stringify({
    //        "store":"NEW旗舰店",
    //        "products":{
    //            "name":"手机",
    //            "price":200
    //        }
    //      }));
    xhr.send(JSON.stringify({
      "store":"NEW旗舰店",
      "products":{
          "name":"手机",
          "price":200
      }
    }));
  };
</script>

put 请求 - 全部更新(覆盖为指定内容)

<button id="put">put</button>
<script>
  put.onclick = function(){
    if(window.XMLHttpRequest){
      var xhr = new XMLHttpRequest();
    } else if(window.ActiveXObject) {
      var xh = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.open("put","http://localhost:3000/shoppingcart/2"); //需要制定id,才能明确修改的是哪个
    xhr.setRequestHeader("Content-Type","application/JSON");
    xhr.onload = function(){
      if(/^2\d{2}$/.test(xhr.status)){
        console.log(JSON.parse(xhr.responseText));
      }
    }
    xhr.send(JSON.stringify({
      "store": "opsu旗舰店",
      "products": [
        {
          "id": 0,
          "name": "new product",
          "describe-specs": "new specs",
          "describe-size": "new size",
          "price": "998.99",
          "counts": 99
        }
      ]
    }));
  };
</script>

patch 请求 - 部分更新

<button id="patch">patch</button>
<script>
  patch.onclick = function(){
    if(window.XMLHttpRequest){
      var xhr = new XMLHttpRequest();
    } else if(window.ActiveXObject) {
      var xh = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.open("PATCH","http://localhost:3000/shoppingcart/2"); //需要制定id,才能明确修改的是哪个
    xhr.setRequestHeader("Content-Type","application/JSON");
    xhr.onload = function(){
      if(/^2\d{2}/.test(xhr.status)){
        console.log(JSON.parse(xhr.responseText));
      }
    }
    xhr.send(JSON.stringify({
      "store": "opsu旗舰店"   //仅仅修改store内容
    }));
  };
</script>

delete 请求 - 删除

<button id="del">delete</button>
<script>
  del.onclick = function(){
    if(window.XMLHttpRequest){
      var xhr = new XMLHttpRequest();
    } else if(window.ActiveXObject) {
      var xh = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.open("DELETE","http://localhost:3000/shoppingcart/2"); //需要制定id,才能明确修改的是哪个
    xhr.setRequestHeader("Content-Type","application/JSON");
    xhr.onload = function(){
      if(/^2\d{2}/.test(xhr.status)){
        console.log(JSON.parse(xhr.responseText));
      }
    }
    xhr.send(); //删除数据不用传递参数,直接告诉后端删除数据的id是多少就可以了
  };
</script>

案例 - 查询天气 - 高德 API

<head>
    <meta charset="utf-8" />
    <meta name="viewpoint" content="width=device-width,initial-scale=1.0"/>
    <style>
        button,div{font-size:1rem;}
        button{display:block;margin:0.5em auto;width:100%;}
    </style>
</head>

<body>
    <button id="live">实时天气</button>
    <button id="forcast">预报天气</button>
    <div id="display" ></div>
</body>
<script>
    let live = document.getElementById("live");
    let forcast = document.getElementById("forcast");
    let playWeather = document.getElementById("display");
    // 1. 查询实时天气 - 高德地图 天气查询 API
    live.onclick = function () {
        getWeather("510100", "base");
    }
    // 2. 查询天气预报 - 高德地图 天气查询 API
    forcast.onclick = function () {
        getWeather("510100", "all");
    }
    //渲染内容
    function getWeather(adcode, arg) {
        var httpRequest;
        window.XMLHttpRequest ? httpRequest = new XMLHttpRequest() : console.error("Not support XMLHttpRequest")
        httpRequest.open("GET", "https://restapi.amap.com/v3/weather/weatherInfo?key=429951164744f19f085244582f537008&city=" + adcode + "&extensions=" + arg);
        httpRequest.send();
        httpRequest.onreadystatechange = function () {
            if (httpRequest.readyState === 4 && httpRequest.status === 200) {
                if (arg === "base") {
                    var liveWeatherData = JSON.parse(httpRequest.responseText).lives[0];
                    for (let key in liveWeatherData) {
                        // console.log(liveWeatherData.key); //undefined 注意:object.key 如果key是变量只能使用中括号调用
                        switch (key) {
                            case "province": display.innerHTML += "<div>" + "省份 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "city": display.innerHTML += "<div>" + "城市 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "weather": display.innerHTML += "<div>" + "云层 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "temperature": display.innerHTML += "<div>" + "温度 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "winddirection": display.innerHTML += "<div>" + "风向 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "humidity": display.innerHTML += "<div>" + "湿度 : " + liveWeatherData[key] + "</div>";
                                break;
                            case "reporttime": display.innerHTML += "<div>" + "预报 : " + liveWeatherData[key] + "</div>";
                                break;
                        }
                    }
                } else if (arg === "all") {
                    console.log(JSON.parse(httpRequest.responseText));
                    var forcastWeatherData = JSON.parse(httpRequest.responseText).forecasts[0];
                    display.innerHTML += "<hr/> <div> 省会天气预报:"+ forcastWeatherData["city"]+forcastWeatherData["province"] + "</div>";
                    forcastWeatherData["casts"].forEach( item => {
                        for(let key in item){
                            switch (key){
                                case "date":  display.innerHTML += "<div> 预报时间:"+ item[key] +"</div>";
                                break;
                                case "daytemp":  display.innerHTML += "<div> 日间温度:"+ item[key] +"</div>";
                                break;
                                case "dayweather":  display.innerHTML += "<div> 日间云层:"+ item[key] +"</div>";
                                break;
                                case "daywind":  display.innerHTML += "<div> 日间风向:"+ item[key] +"</div>";
                                break;
                                case "nighttemp":  display.innerHTML += "<div> 夜间温度"+ item[key] +"</div>";
                                break;
                                case "nightweather":  display.innerHTML += "<div> 夜间云层:"+ item[key] +"</div>";
                                break;
                                case "nightwind":  display.innerHTML += "<div> 夜间风向:"+ item[key] +"</div>";
                                break;
                            }
                        }
                        display.innerHTML += "<hr/>";
                    })
                } //else none
            }
        }
    }
    //适配一些界面
    document.documentElement.style.fontSize = document.documentElement.client / 375 * 100 + "px";
</script>

Ajax 查询天气 - 高德API.gif

案例 - 模拟待办事项 - 封装 ajax

<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewpoint" content="width=device-width,initial-scale=1.0" />
    <link rel="shortcut icon" href="http://www.yoyonn.com/imagesz2/air/air958200.jpg">
    <title>login</title>
    <style>
        * {margin: 0;padding: 0;}
        .container {height: 50vh;width: 30vw;border-radius: 7px;box-shadow: 2px 2px 2px 1px black;margin: 4vh auto;overflow:auto;text-align: center;
            /* font-size:0; 解决 => 因为设置文档 font-size 之后,导致的元素布局错位问题 */
            font-size: 0;
        }
        .list::-webkit-scrollbar{width:3px;height:8px;background:wihte;}
        .list::-webkit-scrollbar-thumb{background:black;}
        .thing {appearance:none;outline: none;width: 90%;height: 5%;padding: 0 2%;margin: 1% auto;border:0;border-radius:8px;font: 12px/12px TimeNewRoma;background:lightgray;}
        .add,.reload,.search {width: 28%;height: 8%;border: none;background: lightcoral;border-radius: 7px;margin: auto 1.5%;font: bolder .3rem/.3rem TimeNewRoma;vertical-align:middle;}
        .add:hover,.reload:hover,.search:hover {background: greenyellow;color:black;border:1px solid lightgray}
        .add:active,.reload:active,.search:active {background: black;color: white;}
        .list {width: 90%;height: 80%;margin: auto;margin-top: 2%;background: lightgray;overflow:auto;}
        .list-item{width:100%;color:black;margin: auto;font-size: 12px;cursor:pointer;text-align:left;text-indent:2em;padding:10px 0;}
        .list-item:hover{background: black;color:white;}
    </style>
    <script src="./ajax_encapsulation.js"></script>
</head>

<body>
    <div class="container">
        <input class="thing" type="text" placeholder="add your to do list thing..." value=""/>
        <button class="add">添加</button>
        <button class="search">查询</button>
        <button class="reload">清空</button>
        <div class="list"></div>
    </div>
</body>
<script>
    //获取DOM
    var thing, pwd, add, reload, search, list;
    Array.from(document.getElementsByClassName("container")[0].children).forEach(item => {
        switch (item.className) {
            case "thing": thing = item;
                break;
            case "pwd": pwd = item;
                break;
            case "add": add = item;
                break;
            case "reload": reload = item;
                break;
            case "search": search = item;
                break;
            case "list": list = item;
                break;
        }
    });
    //适配移动端
    document.documentElement.style.fontSize = document.documentElement.clientWidth / 375 * 16 + "px";
    //定义新增事项样式
    function listItem(txt){
        var listRender = document.createElement("div");
        listRender.classList.add("list-item");
        listRender.innerText = txt;
        list.append(listRender);
        return listItem;
    }
    //渲染页面
    function initialRender(result,status,error){
        if(/^2\d{2}$/.test(status)){
            if(Object.keys(result).length === 0 ){
                listItem("暂无事项,请添加")
            } else {
                result.forEach( i => {
                    //如果json中没有字段,那么json-server添加,将从id=1开始,如果已有字段,则从现有id序号继续往后递增
                    listItem(i.id + " - " + i.item);
                });
            }
        } else {
            listItem("加载错误 - 请刷新" + status)
        }
    };
    //参数判断 - 正则
    function check(value,addedItemsParent){
        var addedItems = addedItemsParent.children;
        var checkResult = [];
        for( let i = 0; i < addedItems.length; i++){
            if( new RegExp(value).test(addedItems[i].innerText.split(" ")[2]) ) {
                checkResult.push(addedItems[i].innerText.split(" ")[2]);
            } // else none
        }
        return checkResult
    }
    //初始化加载
    var addedItemsParent;
    
    onload = initialAjax;
    function initialAjax(){
        ajaxEncapsulation({
            method:"GET",
            url:"http://localhost:3000/list",
            async:true,
            success:function(result,status){
                initialRender(result,status);
                addedItemsParent =  document.getElementsByClassName("list")[0].cloneNode(true); // 如果页面筛选的时候,会有元素添加删除的情况,可以将元素克隆备用
            },
            error:function(error){
                initialRender(null,null,error);
            }
        });
    };
    //查询
    search.onclick = function () {
        var checkItems = Array.from(new Set(check(thing.value,addedItemsParent)));

        if( addedItemsParent.children[0].innerText !== "暂无事项,请添加" ) {
            if( thing.value.length === 0 ) {
                thing.value = prompt(`必须填写下列其中一项 (${checkItems})`, checkItems[0]);
            } else {
                list.innerHTML = "";
                for(let term of checkItems){
                    //通过正则匹配关键词获取的字段,有可能是多个,而由于ajax每次执行拿回一个相同key的值,因此,需要多次调用ajax方法
                    ajaxEncapsulation({
                        method: "GET",
                        url:"http://localhost:3000/list",
                        async:true,
                        data:{"item":`${term}`},
                        success: function (result) {
                            result.forEach( term => {
                                listItem(term["id"] + " - " + term["item"]);
                            });
                        },        
                        error:function(error){
                            initialRender(null,null,error);
                        }
                    });
                };
            };
        } else {
            return
        };
    }
    //添加
    add.onclick = function () {
        if( list.firstElementChild != undefined && list.firstElementChild.innerText === "暂无事项,请添加" ){
            list.firstElementChild.remove()
        } // else none
        thing.value.length === 0 ? thing.value = "default" : null;
        ajaxEncapsulation({
            method: "POST",
            url: "http://localhost:3000/list",
            async:true,
            data:{"item":`${thing.value}`},
            header: {"content-type": "application/JSON"},
            success:function(result,status){
                listItem(result["id"] + " - " + result["item"]);
                // location.reload();   //添加之后必须要刷新页面,否则将无法重新克隆所有添加的元素,导致查询时提示不全或不提示
                //上述问题,也可使用ajax刷新局部页面,但是ajax嵌套ajax依然还是会存在,回调地狱问题
                ajaxEncapsulation({
                    method:"GET",
                    url:"http://localhost:3000/list",
                    async:true,
                    success:function(result){
                        list.innerHTML = "";
                        initialAjax();
                    },
                    error:function(error){
                        initialRender(null,null,error);
                    }
                });
            },
            error:function(error){
                initialRender(null,null,error);
            }
        });
    }
    //清空
    reload.onclick = function(){
        ajaxEncapsulation({
            method:"GET",
            url: "http://localhost:3000/list/",
            async:true,
            data:{},
            success:function(result){
                //根据获取到的字段进行删除,此处已经涉及到ajax的回调地狱问题
                var deleteState = "delete success.";
                result.forEach( i => {
                    ajaxEncapsulation({
                        method:"DELETE",
                        url: "http://localhost:3000/list/" + i.id,
                        async:true,
                        data:{},
                        success:function(result,status){
                            if( !/^2\d{2}$/.test(status)) {
                                deleteFail = "id: " + id + " delete fail.";
                            } // else none
                        },
                        error:function(error){
                            initialRender(null,null,error);
                        }
                    });
                });
                alert("deleteState: " + deleteState);
                location.reload();
            },
            error:function(error){
                initialRender(null,null,error);
            }
        })
    }
</script>

</html>
//封装 ajax
function ajaxEncapsulation(args) {
    let defaultArgs = {
        method: "",
        url: "",
        id: "",
        data: {},
        header: {},
        asynchronous: true,
        success() { },
        error() { }
    };
    let { method, url, id, data, header, asynchronous, success, error } = {
        ...defaultArgs,
        ...args
    };
    //实例化
    if (window.XMLHttpRequest) {
        var xh = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        var xh = new ActiveXObject("Microsoft.XMLHTTP");
    };
    //参数
    function getDataForm() {
        var dataForm = "";
        if (Object.getOwnPropertyNames(data).length !== 0) {
            for (let key in data) {
                dataForm += key + "=" + data[key] + "&";
            };
        } // else none
        dataForm.slice(dataForm.length - 1);
        return dataForm;
    }
    function getDataJSONStr() {
        var dataJSONStr = "";
        if (Object.getOwnPropertyNames(data).length !== 0) {
            dataJSONStr = JSON.stringify(data);
        } // else none
        return dataJSONStr;
    }
    function setHeaderAndSendData() {
        if (Object.getOwnPropertyNames(header).length !== 0) {
            for (let key in header) {
                xh.setRequestHeader(key, header[key]);
                if(header[key] === "application/x-www-forn-urlencoded"){
                    xh.send(getDataForm());
                    console.log(123)
                } else if(header[key] === "application/JSON"){
                    xh.send(getDataJSONStr());
                }
            }
        } // else none
    }
    //配置发送
    switch (method) {
        case "GET":
            xh.open(method, url + "?" + getDataForm(), asynchronous);
            xh.send();
            break;
        case "POST":
            xh.open(method, url, asynchronous);
            setHeaderAndSendData();
            break;
        case "PUT":
            xh.open(method, url + "/" + id, asynchronous);
            setHeaderAndSendData();
            break;
        case "PATCH":
            xh.open(method, url + "/" + id, asynchronous);
            setHeaderAndSendData();
            break;
        case "DELETE":
            xh.open(method, url + "/" + id, asynchronous);
            xh.send();
            break;
    };
    //响应
    xh.onload = function(){
        try{
            if(/^2\d{2}$/.test(xh.status)){
                success(JSON.parse(xh.responseText),xh.status);
            } else {
                error(xh.status);
            };
        }catch(err){
            console.error(err);
        }
    };
}
//初始 - 开始无数据
{
  "list": []
}
//中间 - 添加/查询
{
  "list": [
    {
      "item": "default",
      "id": 1
    },
    {
      "item": "default",
      "id": 2
    },
    {
      "item": "default",
      "id": 3
    },
    {
      "item": "世界",
      "id": 4
    },
    {
      "item": "世界杯",
      "id": 5
    },
    {
      "item": "世界杯比赛",
      "id": 6
    },
    {
      "item": "球类运动",
      "id": 7
    },
    {
      "item": "运动达人",
      "id": 8
    },
    {
      "item": "开会",
      "id": 9
    },
    {
      "item": "主要会议",
      "id": 10
    },
    {
      "item": "世界",
      "id": 11
    }
  ]
}
//末尾 - 删除后
{
  "list": []
}
Ajax 复习练习.gif

Promise(ES6 语法)

  • 本质:构造函数
  • 功能:异步编程的一种解决方案,解决异步的回调地狱问题

回调地狱

  • 情景:一个回调函数嵌套另一个回调函数的嵌套结构在代码量较少时,还能维护,但如果嵌套层级过多,就会导致后期维护的巨大困难,导致回调地狱的情况出现。
ajax({
    url:"第一个请求",
    success(res){
        ajax({
            url:"第二个请求",
            data:{
                a:res.a,
                b:res.b
            },
            success(res2){
                // 依次嵌套下去,会形成回调地狱导致代码难以维护
            }
        })
    }
})

代码举例 - 回调地狱问题

<script src="./ajax_encapsulation.js"></script>
<script>
    ajaxEncapsulation({  //第一次调用ajax
        method:"GET",
        url:"http://localhost:3000/content",
        async:true,
        data:{"id":1},
        success(res){
            var title = document.createElement("div");
            title.innerText = `title: ${res[0].title}`;
            document.body.append(title);
            ajaxEncapsulation({  //第二次调用ajax
                method:"GET",
                url:"http://localhost:3000/comments",
                async:true,
                data:{"contentId":res[0].id},
                success(res){
                    res.forEach( item => {
                        var comment = document.createElement("div");
                        comment.innerText = `cooment ${item.id} : ${item.comment}`;
                        document.body.append(comment);
                    });
                },
                error(err){
                    console.log(err);
                }
            })
        },
        error(err){
            console.log(err);
        }
    });
    //页面输出:
    //  title: page title
    //  cooment 1 : very good point
    //  cooment 2 : such good point
</script>
//封装 ajax
function ajaxEncapsulation(args) {
    //考虑:携带参数缺失的情况(设置携带参数的默认值 - 解构赋值 方便快速)
    let defaultArgs = {
        method:"",
        url: "",
        async: true,
        header: {},
        data: {},
        success(){},
        error(){},
    };
    let { url, method, async, header, data, success, error } = {
        ...defaultArgs,
        ...args 
    };
    //创建 ajax对象
    if(window.XMLHttpRequest){
        var httpRequest = new XMLHttpRequest();
    } else if(window.ActiveXObject) {
        var httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
    }
    //拼接
    if( method === "GET" && Object.getOwnPropertyNames(data).length !== 0 ) {
        var getUrl = "";
        for(key in data){
            getUrl += key + "=" + data[key] + "&";
        }
        url = url + "?" + getUrl.slice(getUrl-1,-1);
    } // else none
    //配置
    httpRequest.open(method,url,async);
    //请求头
    if( method !== "GET" || method !== "DELETE" ){
        for( let key in header ){
            httpRequest.setRequestHeader(key,header[key]);
        }
    } //else none
    //响应
    httpRequest.onload = function(){
        try{
            if( /^2\d{2}$/.test(httpRequest.status)) {
                success(JSON.parse(httpRequest.responseText), httpRequest.status);
            } else {
                error(httpRequest.status);
            }
        } catch (error){
            console.error(error)
        }
    }
    //发送
    method !== "GET" || method !== "DELETE" ? httpRequest.send(JSON.stringify(data)) : httpRequest.send();
};

Promise 语法

解释:Promise 语法主要用于解决回调地狱问题,其核心思想是通过 Promise 实例化对象的异步状态执行对应函数,当 Promise 实例化之后,会进入 pending 状态,如果异步结束成功,例如调用的是 resolve() 方法,那么就会从pending 状态进入 fulfilled 状态,此时在 then 中注册的回调函数就会被执行,如果异步结束失败,例如调用的是 reject() 方法,就会从 pending 状态进入到 reject 状态,此时 catch 中注册的回调函数就会被执行了,因此,Promise 具有的三个状态只能是从 pending 到 fulfilled,或者从 pending 到 reject

console.log("正在加载......");  //Promise 语法还可以用于数据完全加载前的等待提示
var pro = new Promise(function(resolve, reject){
    //适合于异步语句,因为每一个异步事件在执行时,都会有 执行中/成功/失败 3个状态
    //当实例化 Promise 之后,Promise 也会进入这三个状态
    //  1. 执行中 -> pending 状态
    //  2. 完成   -> fulfilled 兑现承诺
    //  3. 拒绝   -> reject 拒绝承诺
    setTimeout(()=>{
        resolve("兑现承诺");  //如果兑现承诺,就执行该语句,并且该语句的参数传递给 then 
        reject("拒绝承诺");   //如果拒绝承诺,就执行该语句,并且该语句的参数传递给 catch 
        //resolve 执行 兑现承诺的函数(then)
        //reject 执行 拒绝承诺的函数(catch)
    })
});
pro.then(function(res){
    //兑现承诺,这个函数被执行
    console.log("success "+res);
}).catch(function(err){
    //拒绝承诺,这个函数被调用
    console.log("fail "+err);
})

Promise.all([pro]).then( res => {
    console.log(res);
    console.log("取消显示正在加载...")
}).catch( err => {
    console.log(err);
});
<script src="./ajax_encapsulation.js"></script>
<script>
    //回调地狱
    ajaxEncapsulation({  //第一次调用ajax
        method:"GET",
        url:"http://localhost:3000/content",
        async:true,
        data:{"id":1},
        //success
        success(res){
            var title = document.createElement("div");
            title.innerText = `title: ${res[0].title}`;
            document.body.append(title);
            ajaxEncapsulation({  //第二次调用ajax
                method:"GET",
                url:"http://localhost:3000/comments",
                async:true,
                data:{"contentId":res[0].id},
                //success
                success(res){
                    res.forEach( item => {
                        var comment = document.createElement("div");
                        comment.innerText = `cooment ${item.id} : ${item.comment}`;
                        document.body.append(comment);
                    });
                },
                error(err){
                    console.log(err);
                }
            })
        },
        error(err){
            console.log(err);
        }
    });
    //页面输出:
    //  title: page title
    //  cooment 1 : very good point
    //  cooment 2 : such good point
</script>

将上述回调地狱的示例,改写成Promise语法

<script src="./ajax_encapsulation.js"></script>
<script>
    //定义
    function pajax(params){
        return new Promise(function(resolve,reject){
            ajaxEncapsulation({
                ...params,
                success(result){
                    resolve(result);
                },
                error(err){
                    reject(err);
                }
            });
        });
    };
    //调用
    pajax({
        method:"GET",
        url:"http://localhost:3000/content",
        async:true,
        data:{id:1},
        success(result){
            resolve(result);
        },
        error(err){
            reject(err);
        }
    }).then( res => {
        var title = document.createElement("div");
        document.body.append(title);
        title.innerText = "title: " + res[0].title;
        return pajax({  //关键:必须要return,否则不能使用链式调用
            method:"GET",
            url:"http://localhost:3000/comments",
            async:true,
            data:{contentId:res[0].id},
            success(result){
                resolve(result);
            },
            error(err){
                reject(err);
            }
        });
    }).then( res => {
        console.log(res)
        res.forEach( item => {
            var comment = document.createElement("div");
            document.body.append(comment);
            comment.innerText = "comment " + item.id + " : " + item.comment;
        });
    }).catch( err => {
        console.log(err);
    });

    //页面输出:
    //  title: page title
    //  cooment 1 : very good point
    //  cooment 2 : such good point
</script>

Promise 源码模拟

const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class MyPromise {
    constructor(fn) {
        this.status = PENDING;  //默认状态
        this.value = null;  //异步完成后,要传递的值
        this.resolveArr = [];   //存放resolve之后then的回调
        this.rejectArr = [];   //存放reject之后then的回调
        fn(this.resolve, this.reject);
    };
    resolve = res => {
        setTimeout(() => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = res;
                console.log(this.status);
            } // else none
        }, 500);
    };
    reject = err => {
        setTimeout(() => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.value = err;
                console.log(this.status);
            } // else none
        }, 500);
    }
    then(resolveFn, rejectFn) {
        return new MyPromise((resolve, reject) => {
            resolveFn = typeof resolveFn === "function" ? resolveFn : () => { };
            rejectFn = typeof rejectFn === "function" ? rejectFn : () => { };
            setTimeout(() => {
                if (this.status === FULFILLED) {
                    resolveFn(this.value);
                } else if (this.status === REJECTED) {
                    rejectFn(this.value);
                }
            }, 500);
        });
    }
}
//检验
console.log(1)
// 成功 - 执行resolve()
new MyPromise((resolve, reject) => {
    console.log(2);
    resolve("result1");
}).then(res => {
    console.log(3);
    console.log(res);
});

//失败 - 执行reject()
new MyPromise((resolve, reject) => {
    reject("error1");
}).then("_", err => {
    console.log(err);
});

//链式调用 => 问题:没办法链式调用
let n = new MyPromise((resolve, reject) => {
    resolve("result2");
}).then(res => {
    console.log(res);
    return new MyPromise((resolve, reject) => {
        resolve("result3");
    }).then(res => {
        console.log(res);
    });
});
console.log(5);

fetch 语法

功能

  • 功能:Fetch API 提供了一个获取资源的接口(包括跨域请求),是 XMLHttpRequest 的优化版本(A modern replacement for XMLHttpRequest),但其方法内部还是通过实例化 XMLHttpRequest 实现的,因此可以加看作是一个语法糖
  • 区别于 jQuery.ajax()
    • resolve 和 reject:当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject,即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve(如果响应的 HTTP 状态码不在 200 - 299 的范围内,则设置 resolve 返回值的 ok 属性为 false),仅当网络故障时或请求被阻止时,才会标记为 reject。
    • fetch 不会发送跨域 cookie,除非使用了 credentials 的初始化选项。

基本语法格式

//fetch 基本语法格式 - POST 为例
const data = { username: 'example' };

fetch('https://example.com/profile', {
    method: 'POST', // or 'PUT'
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),  //data 是要携带的参数
})
    .then(response => response.json())
    .then(data => {
        console.log('Success:', data);
    })
    .catch((error) => {
        console.error('Error:', error);
    });

fetch() 返回值

  • fetch() 返回:一个包含响应结果 Promise 对象,默认 GET 请求
let f = fetch("http://localhost:3000/content");
console.log(f);
//输出结果是 pending 状态的 Promise 对象,说明可以使用then方法,进行链式调用
//Promise {<pending>}
// [[Prototype]]: Promise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: Response -> response 对象是一个包含响应结果的 promise 对象
    // body: ReadableStream -> response对象是一个 HTTP 响应,而不是真的 JSON,本质上是一个可读数据流,为了获取 JSON 的内容,我们需要使用 json() 方法(该方法返回一个将响应 body 解析成 JSON 的 promise)
    //   locked: false
    //   [[Prototype]]: ReadableStream
    // bodyUsed: false
    // headers: Headers {}
    // ok: true
    // redirected: false
    // status: 200
    // statusText: "OK"
    // type: "cors"
    // url: "http://localhost:3000/content"
    // [[Prototype]]: Response

Response.json() 和 then()

  • then() 方法:通过调用该方法,返回的是一个 Response 对象(响应结果)
  • Response.json() 方法:返回一个将响应 body 解析成 JSON 的 promise,如果调用的是text()方法,那么就是返回字符串,也就是将 then() 方法返回的 Response 对象的 body 可读数据流解析为 JSON 文件,因此,一般会使用两个 then 语句
//未解析为 JSON 文件的 Response
fetch("http://localhost:3000/content")
    .then(res => {
        console.log(res);   //res 不做任何操作 -> 说明fetch返回的是一个Response对象
        //Response {type: 'cors', url: 'http://localhost:3000/content', …}
            // body: ReadableStream
            // bodyUsed: false
            // headers: Headers {}
            // ok: true
            // redirected: false
            // status: 200
            // statusText: "OK"
            // type: "cors"
            // url: "http://localhost:3000/content"
            // [[Prototype]]: Response
    }).then(res => {
        console.log(res);   //undefined -> 因为上一层then返回的Response只是一个数据流,需要将其通过 Response.json() 方法转换为 Promise 对象才能获取到返回的 json 数据
    });

//解析为 JSON 文件的 Response
fetch("http://localhost:3000/content")
    .then(res => {
        return res.json(); //通过 Response.json() 方法将 body 数据流转换 json 数据
        //return res.text();  //如果后端传递的不是json格式,使用json()方法会造成异常,因此可以使用text()方法
    })
    .then(res => {
        console.log(res);
        //转换后能够输出具体的数据,而不只是数据流
        //(2) [{…}, {…}]
        // 0: {id: 1, title: 'page title'}
        // 1: {id: 2, title: 'article title'}
        // length: 2
        // [[Prototype]]: Array(0)
    });

Promise.reject() 和 catch()

  • 使用 fetch 方法时,调用 catch() 仅能捕获请求阻止或网络故障的问题,不会捕获请求失败(例如404、500)的问题,即使请求失败依然会调用 then,如果要解决这个问题只能在fetch语句中手动检查
  • 如何手动检查请求状态:根据fetch返回的status、ok来判断,如果请求失败,可调用reject()方法,可以将错误参数或自定义内容传递给catch(),注意如果不判断状态的话,reject()方法会直接被调用,不断是否成功
//请求只要走通了,无论成功还是失败都是走的 then 语句
fetch("http://localhost:3000/fail")  //如果地址错误
    .then(res => {
        console.log(res.ok);            //false
        console.log(res.status);        //404
        console.log(res.statusText);    //Not Found
        return res.json();  //依然可以返回,只是没有值了
    })
    .then(res => {
        console.log(res);   //{} -> 因为404错误,自然就没有值了,但是语法上是正确的
    });
    
//手动检查
fetch("http://localhost:3000/fail")  //如果地址错误
    .then(res => {
        if(res.status === true){
            return res.json();  
        }else{
            return Promise.reject({  //如果使用res.reject()
                status:res.status,
                statusText:res.statusText
            });    //如果请求失败,执行该语句
        }
    })
    .then(res => {
        console.log(res);
    })
    .catch(err => {
        console.log(err);   //{status: 404, statusText: 'Not Found'}
    });

JSONP 与同源策略问题

同源策略

  • 同源策略(Same origin policy):是一种约定的安全功能。
  • 同源:当JavaScript发送请求时,会检查 协议、域名、端口 是否与请求的url地址都相同,相同则表示同源,其中一项不相同就是跨域访问,浏览器就会阻止访问,并且在控制台报错。
  • 同源策略的解决办法
    • 后端设置 CORS(跨域资源共享):例如 json-server 模拟的后端服务器,之所以能在本地不同端口之间请求,在于设置了 Access-Control-Allow-Origin: http://127.0.0.1:5500 ,因此,如果设置 Access-Control-Allow-Origin: * 可以接受任何端口请求
    • 前后端协作 JSONP:JSONP(JSON with Padding) 是 JSON 的一种 “使用模式”,可以让网页从不同处域名处获取数据,即跨域请求,要注意的是,后端返回的格式需要是一个函数表达式,否则将无法执行。

JSONP 原理及应用

  • JSONP 原理:动态创建 script 标签,src属性指向请求地址(api接口),由于src属性的指向没有跨域限制,因此可以实现跨域请求
  • 注意事项
    • 后端接口必须是 xxx() 函数的形式;
    • jsonp 是动态创建script标签,因此需要等脚本执行完拿到数据后,删除当前的添加的script标签;
    • 只能 get 请求,不能 post put delete
<script type="text/javascript">
    //step 1 动态创建 script 标签
    let jsonpScript = document.createElement("script");
    //step 2 设置src指向api
    jsonpScript.src = "https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction";
    // -> 注意:jsoncallback=callbackFunction 的callbackFunction就是需要前后端协作的地方,后端的这个名称可以变化,但前端这里的名称需要与后面声明的函数名一致,为了更好地对接一般会设置函数名要有灵活性
    // -> 注意:不同网站 jsoncallback 的key名称不同,有些可能是cb或其它
    //steo 3 添加 script 标签
    document.body.appendChild(jsonpScript);
    //step 4 前后端协作:声明一个函数,需要与后端json返回的同名,否则后端返回的函数将因为找不大声明的函数而报错
    // -> 注意:script 不能定义为 type="module" 否则api无法正确识别script声明的函数方法
    function callbackFunction(obj) {
        console.log(obj);  // 输出 (2) ['customername1', 'customername2']
    };
    //step 5 脚本运行完后,需要删除该节点
    jsonpScript.onload = function(){
        jsonpScript.remove();
    }
</script>
//后端返回的函数名,jsonp 需要后端返回函数名才能起作用
callbackFunction(
    ["customername1", "customername2"]
)

JSONP 实例 - 百度搜索关键词联想API

<input type="text" id="inp" placeholder="跨域获取" />
<ul id="list"></ul>
<script type="text/javascript">
    //jsonp 案例 - 百度搜索框联想 api
    inp.addEventListener("input", function () {
        let inpContent = this.value;
        //不输入字段就直接返回
        if (inpContent.length < 1) {
            list.innerHTML = "";
            return
        }
        //清空之后就不必发送请求
        //JSONP
        let jsonpScript = document.createElement("script");
        jsonpScript.src = `https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=36556,37495,37841,37765,37866,37800,37760,37850,26350,37790&wd=${inpContent}&req=2&csor=14&pwd=${inpContent}&cb=jQuery110206459462907160005_1669888754740&_=1669888754754`;
        //wd -> 携带输入的value; cb -> 接口返回的函数表达式
        document.body.appendChild(jsonpScript);
        jsonpScript.remove();
    });
    //注意 -> 回调函数一定要放在全局作用域,因为服务端接收到回调函数后会返回页面中的script中去找,如果不写在全局作用域中根本找不到;
    function jQuery110206459462907160005_1669888754740(obj) {
        list.innerHTML = obj.g.map(item => `<li>${item.q}</li>`).join("");
    }
</script>

百度api关键词联想.gif

模拟后端 - 示例

json-server 模拟服务端

json-server 是什么

  • json-Server 是一个 Node 模块(可以理解为存储json数据的server),可以模拟服务端,再前端开发过程中可以指定一个 json 文件作为 api 的数据源,通俗来说,就是模拟服务端接口数据,一般用在前后端分离后,前端人员可以不依赖API开发,而在本地搭建一个JSON服务,自己产生测试数据

json-server 安装/卸载

// ------------------------------ VScode 中通过 npm 安装 json-server ------------------------------
Windows PowerShell
版权所有(C) Microsoft Corporation。保留所有权利。

安装最新的 PowerShell,了解新功能和改进!https://aka.ms/PSWindows

//还未安装 npm 
PS D:\Codes> npm -v
npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ npm -v
+ ~~~
    + CategoryInfo          : ObjectNotFound: (npm:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
    
// ------------------------------ 安装 node.js 和 npm 之后  ------------------------------
PS D:\Codes> node -v
v18.12.1
PS D:\Codes> npm -v
8.19.2
//还未安装 json-server
PS D:\Codes> json-server -v
json-server : 无法将“json-server”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ json-server -v
+ ~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (json-server:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
// ------------------------------ 如果错误安装 json-server ------------------------------
PS D:\Codes> install npm jason-server -g
install : 无法将“install”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ install npm jason-server -g
+ ~~~~~~~
    + CategoryInfo          : ObjectNotFound: (install:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

PS D:\Codes> npm install jason-server -g
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
npm WARN deprecated querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
npm WARN deprecated chokidar@1.7.0: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.
npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
npm WARN deprecated babel-preset-es2015@6.24.1: 🙌  Thanks for using Babel: we recommend using babel-preset-env now: please read https://babeljs.io/env to update!
npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.

added 797 packages, and audited 798 packages in 3m

18 packages are looking for funding
  run `npm fund` for details

16 vulnerabilities (2 low, 4 moderate, 10 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.
npm notice
npm notice New major version of npm available! 8.19.2 -> 9.1.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.1.2
npm notice Run npm install -g npm@9.1.2 to update!
npm notice
// ------------------------------ 成功安装 json-server ------------------------------
PS D:\Codes> npm install json-server -g

added 109 packages, and audited 110 packages in 13s

10 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
// ------------------------------ 安装 json-server 成功 ------------------------------
PS D:\Codes> json-server -v
0.17.1
// ------------------------------ 卸载 json-server ------------------------------ 
PS D:\Codes> npm remove json-server -g

removed 109 packages, and audited 1 package in 1s

found 0 vulnerabilities
// ------------------------------ 卸载 json-server 成功 ------------------------------ 
PS D:\Codes> json-server -v
json-server : 无法将“json-server”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ json-server -v
+ ~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (json-server:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

json-server 创建 JSON 文件

PS D:\Codes> json-server ./user_data.json --watch

  \{^_^}/ hi!

  Loading ./user_data.json
  Oops, ./user_data.json doesn't seem to exist
  Creating ./user_data.json with some default data

  Done

  Resources
  http://localhost:3000/posts
  http://localhost:3000/comments
  http://localhost:3000/profile

  Home
  http://localhost:3000 

  Type s + enter at any time to create a snapshot of the database
  Watching...
//------- 此处没有结束符号,说明已经启动了一个模拟的服务器,同时,可以看到 user_data.json 的文件已经创建了------
  • 以上操作完成后,在浏览器地址栏输入 http://localhost:3000 出现下面页面即成功运行 json-server 服务器

image.png

Live Server 和 Preview Server

  • Preview on web server:Live Server(127.0.0.1:5500) 跟 json-server(127.0.0.1:端口号)这种模拟后端联合使用的时候,Live Server 每次在前端post之后都会刷新页面,导致部分 JavaScript 代码因为被 Live Server 强制刷新而无法正常显示结果,这就导致如果有需要在VScode环境下开发js原生代码的时候,机会受到阻碍,因此,为解决这个问题,可以使用 Preview on web Server,该插件不会强制刷新页面。
xhr.onload = function(){
  console.log(xhr.status);  //201 -> Preview Server成功的状态码是201,因此建议使用正则判断状态码
  if(xhr.status === 201){
    console.log(JSON.parse(xhr.responseText));
  }
}

image.png

其它参见

blog.csdn.net/ligonglanyu…

XAMPP 软件模拟服务端 - php

image.png

示例 - 地区选择 - php 后端

<!-- html部分 -->
<html>

<head>
    <meta charset="utf-8" />
    <script src="js_ajax_XMLHttpRequest.js" type="module"></script>
</head>

<body>
    <select class="province">
        <option>-- 省 --</option>
    </select>
    <select class="city" disabled>
        <option>-- 市 --</option>
    </select>
    <select class="county" disabled>
        <option>-- 县/区 --</option>
    </select>
    <button class="btn">submit</button>
</body>
<script type="module">
    //引入模块语法
    import {renderGET, renderPOST} from "./js_ajax_XMLHttpRequest.js";

    //获取目标节点
    let province = document.querySelector(".province");
    let city = document.querySelector(".city");
    let county = document.querySelector(".county");
    let btn = document.querySelector(".btn");

    //加载页面即设置 "省"
    ~function(){ 
        renderGET(province);
    }()

    //"市" 随着 "省" 而变动 
    //由于ajax是异步请求,所以在请求的同时js代码依然在执行,为了让ajax代码执行完毕再执行下面的,可以绑定事件,待事件发生后再执行函数
    province.onchange = function () {
        renderPOST.apply(this,[this,city,"province","city.php"]);
    }

    //"县" 随着 "市" 而变动
    city.onchange = function(){
        renderPOST.apply(this,[this,county,"city","county.php"]);
    }

    //提交请求后,显示后端返回提交成功
    btn.onclick = function(){
        if(window.XMLHttpRequest){
            var httpRequest = new XMLHttpRequest();
        } else {
            console.error("Not support XMLHttpRequest");
        }
        httpRequest.open("POST","http://localhost:8000/JS_AJAX/result.php");
        httpRequest.setRequestHeader("content-type","application/x-www-form-urlencoded");
        httpRequest.send(`province=${province.value}&city=${city.value}&county=${county.value}`);
        httpRequest.onreadystatechange = function(){
            if(httpRequest.readyState === 4 && httpRequest.status === 200){
                if(province.value !== "-- 省 --" 
                        && city.value !== "-- 市 --" 
                        && county.value !== "-- 县/区 --"){
                    document.write(httpRequest.responseText);
                } //else none
            }
        }
    }
</script>

</html>
/* js部分 */
//封装请求函数
function renderGET(location) {
    var httpRequest = new XMLHttpRequest();
    // httpRequest.open("GET","http://localhost:8000/JS_AJAX/province.php"); //当放在服务器中时,如果使用全域名地址,不能再地址栏直接输入ip,否则会出现跨域报错
    httpRequest.open("GET", "province.php");
    httpRequest.send();
    httpRequest.onreadystatechange = function () {
        if (httpRequest.status === 200 && httpRequest.readyState === 4) {
            var locationArr = httpRequest.responseText.split(" ");
            locationArr.forEach(item => {
                var opt = document.createElement("option");
                location.appendChild(opt);
                opt.innerText = item;
            });

        }//else none
    }
}

//封装请求及下一级的渲染函数
function renderPOST(curLocal, targetLocal, curLocalString, url) {
    let selectEles = document.getElementsByTagName("select");
    //获取上一级地址列表集合
    let curLocals = curLocal.getElementsByTagName("option");
    //如果选中了上一级地址列表的实际地址,则下一级列表才可编辑;如果是取消最高一级的province地址,则后面全部禁用
    for (let i = 0; i < selectEles.length; i++) {
        if (this.className === selectEles[i].className) {
            if (this.value !== curLocals[0].value) {
                selectEles[i+1].removeAttribute("disabled");
                //恢复初始列表状态  
                selectEles[i+1].innerHTML = null;
                selectEles[i+1].className === "city" ? selectEles[i+1].appendChild(document.createElement("option")).innerText = "-- 市 --": null ;
                selectEles[i+1].className === "county" ? selectEles[i+1].appendChild(document.createElement("option")).innerText = "-- 县/区 --": null ;
            } else {
                let j = i + 1;
                while (j < selectEles.length) {
                    selectEles[j].setAttribute("disabled", true);
                    //恢复初始列表状态  
                    selectEles[j].innerHTML = null;
                    selectEles[j].className === "city" ? selectEles[j].appendChild(document.createElement("option")).innerText = "-- 市 --": null ;
                    selectEles[j].className === "county" ? selectEles[j].appendChild(document.createElement("option")).innerText = "-- 县/区 --": null ;
                    j++;
                }
            }
        } //else none
    }
    //ajax 获取php后端的数据
    if (window.XMLHttpRequest) {
        var httpRequest = new XMLHttpRequest();
    } else {
        console.error("Not support XMLHttpRequest");
    }
    httpRequest.open("POST", url);
    httpRequest.setRequestHeader("content-type", "application/x-www-form-urlencoded");
    httpRequest.send(`${curLocalString}=${this.value}`);
    httpRequest.onreadystatechange = function () {
        //httpRequest.readyState === 4 && httpRequest.status === 200 两者缺一不可
        // - httpRequest.readyState === 4 保证在响应完毕之后才会执行,避免状态码3的时候也会执行
        // - httpRequest.status === 200 保证资源相互成功,而不是单纯只是响应完毕
        if (httpRequest.status === 200 && httpRequest.readyState === 4) {
            httpRequest.responseText.split(" ").forEach(item => {
                let opt = document.createElement("option");
                targetLocal.appendChild(opt).innerText = item;
            });
        }
    }
}

export { renderGET, renderPOST };
<?php 
    # province.php
    echo "四川省 广东省 江西省";
?>
<?php
    # city.php
    $province = $_REQUEST["province"];
    if($province == "四川省"){
        echo "成都市 宜宾市 内江市";
    } else if ($province == "广东省"){
        echo "广州市 深圳市";
    } else if ($province == "江西省"){
        echo "南昌市";
    } else {
        echo "- 待添加 -";
    }
?>
<?php
    # county.php
    $city = $_REQUEST["city"];
    switch ($city){
        case "成都市" : echo "成华区 武侯区 锦江区 双流区";
        break;
        case "宜宾市" : echo "新城区 南溪区";
        break;
        case "内江市" : echo "东兴区";
        break;
        case "广州市" : echo "黄浦区 番禺区 花都区";
        break;
        case "深圳市" : echo "罗湖区 福田区";
        break;
        case "南昌市" : echo "东湖区 西湖区";
        break;
        default : echo "- 待添加 -";
        break;
    }
?>
<?php
    # result.php
    $province = $_REQUEST["province"];
    $city = $_REQUEST["city"];
    $county = $_REQUEST["county"];
    $result = $province.$city.$county;
    echo "你已提交成功: ".$result;
?>

Ajax-PHP 模拟地址数据提交.gif

HTTP 状态码

1XX - 临时相应

  • 表示:临时相应并需要请求者继续执行操作的状态码;
  • 100(继续): 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其他部分;
  • 101(切换协议):请求这一要求切换协议,服务器已确认并真被切换;

2XX - 成功

  • 表示:成功处理了请求的状态码;
  • 200(成功): 服务器已成功处理了请求。通常,这表示服务器提供了请求的网页。如果是对您的 robots.txt 文件显示此状态码,则表示 Googlebot 已成功检索到该文件。
  • 201(已创建) :请求成功并且服务器创建了新的资源。
  • 202(已接受); 服务器已接受请求,但尚未处理。
  • 203(非授权信息): 服务器已成功处理了请求,但返回的信息可能来自另一来源。
  • 204(无内容): 服务器成功处理了请求,但没有返回任何内容。
  • 205(重置内容): 服务器成功处理了请求,但没有返回任何内容。与 204 响应不同,此响应要求请求者重置文档视图(例如,清除表单内容以输入新内容)。
  • 206(部分内容): 服务器成功处理了部分 GET 请求。

3XX - 重定向

  • 表示:要完成请求,需要进一步操作。通常,这些状态码用来重定向。Google 建议您在每次请求中使用重定向不要超过 5 次。您可以使用网站管理员工具查看一下 Googlebot 在抓取重定向网页时是否遇到问题。诊断下的网络抓取页列出了由于重定向错误导致 Googlebot 无法抓取的网址。
  • 300(多种选择): 针对请求,服务器可执行多种操作。服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
  • 301(永久移动): 请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。您应使用此代码告诉 Googlebot 某个网页或网站已永久移动到新位置。
  • 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来响应以后的请求。此代码与响应 get 和 head 请求的301代码类似,会自动将请求者转到不同的位置,但不应使用此代码来告诉googlebot某个网页或者网站已经移动,因为googlebot会继续抓取原有位置并编制索引。
  • 303(查看其它位置): 请求者应当对不同位置使用单独的 get 请求来检索响应时,服务器返回此代码。对于出head之外的所有请求,服务器会自动转到其它位置;
  • 304(未修改): 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容;如果网页自请求者上次请求后再也没有更改过,应将服务器配置为返回此响应(称为if-modified-Since HTTP标头)。服务器可以告诉 googlebot 自从上次抓取后网页没有变更,进而节省带宽和开销。
  • 305(使用代理): 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。
  • 307(临时重定向): 服务器目前从不同位置的网页响应请求,但请求者应该继续使用原有位置来响应以后的请求,此代码与响应 get 和 head 请求的 <a href=""></a> 代码类似,会自动将请求者转到不同的位置,但不应该 告诉googlebot 某个页面或者网站已经移动,因为 googlebot 会继续抓取原有位置并编制索引。

4XX - 请求错误

  • 表示:这些状态码表示请求可能出错,妨碍了服务器的处理
  • 400(错误请求): 服务器不理解请求的语法;
  • 401(未授权) : 请求要求身份验证;对于登陆后请求的页面,服务器可能返回次响应;
  • 403(禁止) : 服务器拒绝请求。如果在googlebot 尝试抓取网站上的有效网页时看到此状态码(可以在google网站管理员工具诊断下的网络抓取页面上看到此信息),可能是服务器的主机拒绝了googlebot访问;
  • 404(未找到)
  • 405(方法禁用): 禁用请求中指定的方法;
  • 406(不接受): 无法使用请求内容特性响应请求的网页;
  • 407(需要代理授权): 此状态码与 <a href=answer.py?answer=35128>401(未授权)类似,但指定请求者应当授权使用代理。如果服务器返回此响应,还表示请求者应当使用代理;
  • 408(请求超时):服务器等候请求时发生超时;
  • 409(冲突): 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。服务器在响应与前一个请求相冲突的 PUT 请求时可能会返回此代码,以及两个请求的差异列表;
  • 410(已删除): 如果请求的资源已永久删除,服务器就会返回此响应。该代码与 404(未找到)代码类似,但在资源以前存在而现在不存在的情况下,有时会用来替代 404 代码。如果资源已永久移动,您应使用 301 指定资源的新位置;
  • 411(需要有效长度): 服务器不接受不含有效内容长度标头字段的请求;
  • 412(未满足前提条件): 服务器未满足请求者在请求中设置的其中一个前提条件;
  • 413(请求实体过大):服务器无法处理请求,因为请求实体过大,超出服务器的处理能力;
  • 414(请求的url过长): 请求的 URI(通常为网址)过长,服务器无法处理;
  • 415(不支持的媒体类型): 请求的格式不受请求页面的支持;
  • 416(请求范围不符合要求): 如果页面无法提供请求的范围,则服务器会返回此状态码;
  • 417(未满足期望值): 服务器未满足”期望”请求标头字段的要求;

5XX - 服务器错误

  • 表示:这些状态码表示服务器在处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错;
  • 500(服务器内部错误): 服务器遇到错误,无法完成请求;
  • 501(尚未实施): 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码;
  • 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应;
  • 503(服务器不可用): 服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态
  • 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求;
  • 505(http版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本