Ajax在前端页面的运用实例

559 阅读11分钟

登录页面之用户名验证

  • 我们需要在用户在登录页面中输入用户名的时候判断该用户名是否出现在我们的数据库中,如果判断用户名不存在时利用Ajax静默在页面上提示用户此用户名不存在请重新输入。
  • 下面是浏览器端的页面实现代码
<body>
  <div class="loginContainer">
    <h1>登录</h1>
    <form action="/checkUser" method="post">姓名:
      
      <!--
      	当用户输入用户名并失去焦点时触发ajax事件
      -->
      <input class="inputStyle" type="text" name="username" />
      
      
      <!-- 
      	提示信息默认隐藏
      	当用户输入用户名后通过ajax来实现动态提示信息
      -->
      <div class="exchange">用户名错误</div>
      
      
      <br />密码:
      <input class="inputStyle" type="password" name="pwd" /><br />
      <input class="loginStyle" type="submit" value="登录" />
    </form>
  </div>
</body>
  • 下面是基于node.js依靠koa框架实现的服务器端的页面实现基础代码
const Koa = require("koa");
//静态资源托管
const static = require("koa-static");
//动态路由搭建
const Router = require("koa-router");

//通过ctx.request.body来获取post请求提交过来的普通数据
const koaBody = require("koa-body");
const fs = require("fs");

//文本JSON格式数据(伪装数据库)-自动转为对象
const usersData = require("./data/users.json");
let app = new Koa();

//托管静态资源
app.use(static(__dirname+"/static"));


//每一个请求都用到了此中间件(有点性能浪费)
app.use(koaBody({
    multipart:true  //允许接收二进制文件
}));

//实例化Router
let router = new Router();
//测试动态路由
router.get("/",(ctx,next)=>{
    ctx.body = "hello";
})
  • 下面是服务器端登录页面实现的核心代码
//login.html的登录信息被提交到这个页面
router.get("/checkUserName",(ctx,next)=>{
    ///checkUserName?username=${this.value}
    // console.log(ctx.query.username);
    
    // usersData(数据库)里面的数据类似为
    //{"id":1, "username":"张三","pwd":123}
    let res =  usersData.find(v=>v.username==ctx.query.username);
    //ajax提交到这个页面受处理,后端返回给前端一个数据并实现静默更新HTML页面数据

    //查找后如果有记录证明数据库中有此用户名
    if(res){
        //在这里返还的数据在前端可以通过xhr.responseText获取
        //直接返回在这里会自动转为JSON.StringIfy(数据)
        ctx.body = {
            status:1,
            info:"用户名正确"
        };
    }else{
        ctx.body = {
            status:2,
            info:"用户名错误"
        };
    }
})

Ajax不同的方法请求中参数url路径的解析

  • get 可以以/checkUserName?username=${this.value}的方式发送请求
  • get 也可以以"/get/"的方式发送请求
  • post也可以通过/checkUserName?username=${this.value}的方式发送请求
  • 为了安全性post请求传递一般不会带有?name=value&sex=male这种queryString的方式来发送请求
  • post请求传递如果我们想要给后端传递数据可以通过xhr.send(data)的方式传递给后端
  • post请求传递数据时(xhr.send(data))请求头的编码格式是不能省略不写的。主要可以分为三种编码格式第一种xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),这第一种针对的是传递let data = "username=王五&age=20"这样的数据。
  • 第二种是xhr.setRequestHeader("Content-Type","multipart/form-data"),这第二种针对的是传递二进制数据(例如文件、图片等)
  • 第三种是xhr.setRequestHeader("content-type","application/json"),这第三种针对的是传递json格式的数据,例如let data = JSON.stringify({username:"王五",age:20})
  • 在客户端(浏览器)想要获取返还头信息使用console.log(xhr.getAllResponseHeaders())
  • 在客户端(浏览器)想要获取指定的响应头信息使用console.log(xhr.getResponseHeader("content-type"))
  • 当ajax的第三个参数为false的时候表示这个请求为同步代码会阻塞后续的代码的执行
  • 当ajax第三个参数为true的时候可以先输出同步代码例如console.log,因为这个请求为异步,不会阻塞后续代码的执行

  • 案例一:
<body>
    <button>点击发送ajax</button>
</body>
<script>
    // get 可以以`/checkUserName?username=${this.value}`的方式发送请求
    // get 也可以以"/get/"的方式发送请求

    //post也可以通过`/checkUserName?username=${this.value}`的方式发送请求
    document.querySelector("button").onclick = function(){
        let xhr = new XMLHttpRequest();
        //true代表着异步
        xhr.open("get","/get/",true);
        xhr.onload = function(){
            console.log(xhr.responseText);
        }
        xhr.send();
    }
</script>
  • 案例二:
<body>
    <!-- 表单的默认编码格式 -->
    <!-- <form action="" enctype="application/x-www-form-urlencoded"></form> -->
    <button>点击我发送ajax</button>
</body>
<script>
    document.querySelector("button").onclick = function () {
        let xhr = new XMLHttpRequest();
        //为了安全性post请求传递一般不会带有?name=value&sex=male这种queryString
        //但是如果我们想要给后端传递数据可以通过xhr.send(data)的方式传递给后端
        xhr.open("post", "/post", true);
        xhr.onload = function () {
            console.log(xhr.responseText); //{"status":1,"info":"post请求成功"}
            // 获取返还头信息
            // console.log(xhr.getAllResponseHeaders());

            // 获取指定的响应头信息  application/json; charset=utf-8
            console.log(xhr.getResponseHeader("content-type"));
        }

        //在ajax中的post请求中编码格式是不能省略的


        // xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        // 第一种情况:针对let data = "username=王五&age=20";
        


        // xhr.setRequestHeader("Content-Type","multipart/form-data"); //二进制编码
        // 第2种情况:针对上传文件的情况


        
        
        xhr.setRequestHeader("content-type","application/json");
        // 第三种情况:针对json格式的数据
        let data = JSON.stringify({
            username:"王五",
            age:20
        })

        // let data = "username=王五&age=20";使用xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");


        // post请求会在http正文里面传参,所以需要对正文设置编码格式
        // get请求则不需要设置编码格式
        // 从客户端到服务器端的请求设置请求头是 xhr.setRequestHeader

        
        xhr.send(data);
    }
</script>
  • 下面是服务器端post页面实现的核心代码
//针对post.html页面中的post请求
router.post("/post",(ctx,next)=>{
    console.log(ctx.request.body);
    console.log(ctx.request.files);
    //服务器端返回给客户端的数据
    ctx.body = {
        status:1,
        info:"post请求成功"
    }
})

Ajax请求中的onreadystatechange事件和onload事件

  • onreadystatechange:存有处理服务器响应的函数,每当 readyState 改变时,onreadystatechange 函数就会被执行。
  • readyState:存有服务器响应的状态信息**。0: 请求未初始化**(代理被创建,但尚未调用 open() 方法);1: 服务器连接已建立open方法已经被调用);2: 请求已接收send方法已经被调用,并且头部和状态已经可获得);3: 请求处理中(下载中,responseText 属性已经包含部分数据);4: 请求已完成,且响应已就绪(下载操作已完成)
  • xhr.onload的触发时机会在xhr.onreadystatechange之后
//客户端
<script>
document.querySelector("button").onclick = function () {
    let xhr = new XMLHttpRequest();
    xhr.open("get", "/get/4", true);
    xhr.onload = function () {
        //服务器返回json格式的数据
        console.log(xhr.responseText); //{"status":1,"info":"请求成功"}
        console.log(xhr.readyState); //4
        console.log(xhr.status); //200
    }

    // 使用xhr.onload书写可以省下不少代码

    // xhr.onreadystatechange = function () {
           //客户端ok
    //     if (xhr.readyState == 4) {
               //服务器端ok
    //         if (xhr.status == 200) {
    //             console.log(xhr.responseText);
    //         }
    //     }
    // }
    xhr.send();
}
</script>    
    
//服务器端    
//get.html模拟ajax中的get请求
router.get("/get/:id(\\d*)",(ctx,next)=>{
  console.log(ctx.params);
  ctx.body = {
      status:1,
      info:"请求成功"
  }
})
  • status常用状态码:
HTTP状态码 描述
100 继续。继续响应剩余部分,进行提交请求
200 成功
301 永久移动。请求资源永久移动到新位置
302 临时移动。请求资源零时移动到新位置
304 未修改。请求资源对比上次未被修改,响应中不包含资源内容
401 未授权,需要身份验证
403 禁止。请求被拒绝
404 未找到,服务器未找到需要资源
500 服务器内部错误。服务器遇到错误,无法完成请求
503 服务器不可用。临时服务过载,无法处理请求

Ajax针对不同返回的数据格式的不同处理

  • 返回的是json格式数据通过console.log(xhr.responseText);来获取
  • 返回的是xml数据通过 console.log(xhr.responseXML)来获取,还需要在客户端手动设置重写content-typexhr.overrideMimeType("text/xml");或者在服务器端设置ctx.set("content-type","text/xml"),两种方法取其一。
  • 如果你想得到原始数据则用console.log(xhr.response)
//客户端
<body>
<button>点击我获取xml</button>
</body>
<script>
    document.querySelector("button").onclick = function(){
        let xhr = new XMLHttpRequest();
        xhr.open("get","/xml",true);
        // 方法之二(前端设置):手动设置重写content-type;
        xhr.overrideMimeType("text/xml");
        xhr.onload = function(){

            //返回的是json格式数据通过此来获取
            // console.log(xhr.responseText);


            //返回的是xml数据通过此来获取
            console.log(xhr.responseXML);

            //如果你想得到原始数据则用
            // console.log(xhr.response)
            let name =  xhr.responseXML.getElementsByTagName("name")[1].innerHTML;
            console.log(name);
        }
        xhr.send();
    }
</script>


//服务器端
//模拟返还xml格式数据
router.get("/xml",(ctx,next)=>{
    // 方法之一(后端设置):ctx.set("content-type","text/xml");
    ctx.body = `<?xml version='1.0' encoding='utf-8' ?>
                    <books>
                        <nodejs>
                            <name>nodejs实战</name>
                            <price>56元</price>
                        </nodejs>
                        <react>
                            <name>react入门</name>
                            <price>50元</price>
                        </react>
                    </books>
                `
})

Ajax针对二进制数据的上传案例

  • 案例一:上传一个文件至服务器端保存(应上传至服务器端的指定目录下并保存信息至数据库中)
  • 案例一的知识点:new FormData()Ajax
//前端

<body>
    <input type="file" class="myfile" />
    <button>点击我上传文件</button>
</body>
<script>
    document.querySelector("button").onclick = function(){

        //目前只考虑上传一个文件的情况
        let file = document.querySelector(".myfile").files[0];

        //可能会上传多个文件
        //FileList {0: File, length: 1} 这里只上传了一个文件
        console.log(document.querySelector(".myfile").files);

        //使用FormData时会自动帮我们设置content-type
        let form = new FormData();
        
        //'img'为文件的名字(类似表单的name->value)
        form.append("img",file);
        form.append("name","张三");

        //需要一个ajax对象
        let xhr = new XMLHttpRequest();
        //上传文件一定要是post方法
        xhr.open("post","/upload",true);
        xhr.onload = function(){
            console.log(xhr.responseText);
        }
        xhr.send(form);
    }
</script>




//服务器端
//使用FormData来进行ajax的post提交请求
router.post("/upload",(ctx,next)=>{
    console.log(ctx.request.body,'ctx.request.body');
    
    console.log(ctx.request.files.img,'ctx.request.files.img');
    //console.log(ctx.request.files,'ctx.request.files');
    
    
    // 不写下面的语句不上传任何文件也可以请求成功
    //nodejs把上传的文件给存放的一个临时路径
    // let fileData =  fs.readFileSync(ctx.request.files.img.path);
    //写入路径+写入数据--意思是把上传的单张图片存放在文件夹imgs下
    // fs.writeFileSync("static/imgs/"+ctx.request.files.img.name,fileData);
    ctx.body = "请求成功";
})
  • 案例一总结:利用FormData来实现文件上传
  • 首先创建FormData对象在利用Ajax上传文件至后端并保存至对应服务器指定目录下
FormData对象 描述
upload事件 监控上传进度上传开始
xhr.upload.onloadstart 上传开始
xhr.upload.onprogress(数据传输进行中) evt.total:需要传输的总大小
evtloaded:当前上传的文件大小
xhr.upload.onload 上传成功
xhr.upload.onloadend 上传完成不论成功与否
xhr.upload.onabort 上传操作终止
xhr.onload 整个请求完成(可检测后端返回什么数据)
xhr.abort() 调取函数取消上传
  • 案例二:上传一个文件至服务器端保存,并在上传过程中显示上传的速度和进度,并提供在上传文件过程中取消上传的按钮。
  • 案例二知识点:new FormData();和文件传输上传过程中会触发的响应事件。
<body>
  <input type="file" class="myfile" />
  进度:<progress value="0" max="100"></progress> <span class="percent">0%</span>
  速度:<span class="speed">20b/s</span>
  <button>点击上传</button>
  <button>取消上传</button>
  <!-- 取消上传也需要用到ajax对象 -->
</body>
<script>
  // 单独创建一个ajax对象
  let xhr = new XMLHttpRequest();
  let btns = document.querySelectorAll("button");
  let stime; //最开始的时间
  let sloaded; //最开始上上传的文件大小是多少
  btns[0].onclick = function () {
      //底下是files对象(files[0]是上传的文件)
      let file = document.querySelector(".myfile").files[0];
      let form = new FormData();
      form.append("myfile", file);
      xhr.open("post", "/fileUpload", true);
      xhr.onload = function () {
          console.log(xhr.responseText);
      }

      //上传开始
      xhr.upload.onloadstart = function () {
          console.log("开始上传");
          //上传开始的时候记录时间
          stime = new Date().getTime();
          //上传开始的时候的文件大小
          sloaded = 0;
      }


      //数据传输进行中--当上传时(网速变慢的时候)会不断地触发该事件
      //evt.total:需要传输的 总 大小
      //evt.loaded:当前上传的文件大小
      //let percent=(evt.loaded/evt.total *100).toFixed(0)
      xhr.upload.onprogress = function (evt) {
          //改变进度条的显示进度同时也要改变span标签中的百分比值

          //速度的计算--每一时间段中上传的速度是多少?---在这段时间中文件上传的大小是多小
          //也就是说在这段时间差中上传了多大的文件
          let endTime = new Date().getTime();
          // 时间差;单位是秒(s)
          let dTime = (endTime - stime) / 1000;
          // 当前时间(在这段时间差内)内上传的文件大小(差值)
          // sloaded为刚开始上传的文件的大小
          let dloaded = evt.loaded - sloaded;
          //速度=文件大小/时间差
          let speed = dloaded / dTime;



          //文件小的时候我们让单位为bit/s
          //文件大的时候我们让单位为mb/s
          //1mb=1024kb
          //1kb=1024bit
          let unit = "b/s";



          //重新赋值stime(因为传输过程中此时间不断触发)
          //在上一段时间差的计算结束后应当重新赋值stime
          stime = new Date().getTime();
          //下一次的初始文件值大小也得重新赋值
          sloaded = evt.loaded;


          //文件小的时候我们让单位为bit/s
          //以1024为变化点  bit->kb->mb
          //文件大的时候我们让单位为mb/s
          //1mb=1024kb
          //1kb=1024bit
          if (speed / 1024 > 1) {
              unit = "kb/s";
              speed = speed / 1024;
          }
          if (speed / 1024 > 1) {
              unit = "mb/s";
              speed = speed / 1024;
          }

          //保留两位小数点的速度
          document.querySelector(".speed").innerHTML = speed.toFixed(2) + unit;
          // console.log(speed.toFixed(2));
          // console.log("正在上传");
          // 当前文件上传的大小evt.loaded
          // 需要上传文件的大小
          let percent = (evt.loaded / evt.total * 100).toFixed(0);
          //console.log(percent);
          document.querySelector("progress").value = percent;
          //span标签中的上传进度百分比
          document.querySelector(".percent").innerHTML = percent + "%";
      }



      //上传成功
      xhr.upload.onload = function () {
          console.log("上传成功");
      }

      //上传完成(不管成功与否)
      xhr.upload.onloadend = function () {
          console.log("上传完成");
      }


      //上传操作终止
      xhr.upload.onabort = function () {
          console.log("取消上传");
      }


      //提交数据
      xhr.send(form);
  }
  btns[1].onclick = function () {
      //调取函数取消上传
      xhr.abort();
  }
</script>

Ajax请求中的同源策略和跨域问题

  • 在3000端口中请求同源的资源这是可以返回数据的但返回的是在3000端口中监听的路径的资源
  • 如果跨域请求(这里表现为在3000端口中去4000端口中的数据),请求失败并且请求的数据无法返回(会报错:access control allow origin)。
//客户端
<body>
    <button>点击我请求ajax</button>
</body>
<script>
let btn = document.querySelector("button");
btn.onclick = function(){
    let xhr = new XMLHttpRequest();
    // xhr.open("get","/getAjax",true); 
    xhr.open("get","http://localhost:4000/getAjax",true);
    xhr.onload = function(){
        console.log(xhr.responseText,'1111');
    }
    xhr.send();
}


//4000端口的服务器端确实接收到请求但是由于
//跨域所以无法将数据返回给客户端
  • 常见的几种不受跨域限制的方案:第一种是img标签不会出现跨域问题例如 <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ee2170f2d8a4862a5ba8f58e0d237a8~tplv-k3u1fbpfcp-zoom-1.image" />;第二种是script标签不会出现跨域问题例如<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  • 我们受到第二种方法的启发,来模拟实现一种粗糙的跨域请求
//服务器端:http://localhost:3000/2-script.html
<script src="http://localhost:4000/getAjax?name=张三"></script>
<script>
console.log(a);
</script>

//服务器端http://localhost:4000/getAjax?name=张三

router.get("/getAjax",(ctx,next)=>{
	 //(可以打印出张三)获取了来自跨域前端的数据
	 console.log(ctx.query.name); 
     
     
      //被ajax请求到之后可以在客户端被执行(写法太粗糙会污染)
       ctx.body = "var a = 20";
}
  • 我们受到了这种粗糙的写法的启发,现在来写一个优雅的通过客户端传递回调函数至服务器端,服务器端做的是让其随着数据一同返回至客户端再执行。
//客户端
<body>
    <button>点击获取跨域资源</button>
</body>
<script>
let btn = document.querySelector("button");

//通过script解决了跨域的问题
//通过传递回调函数的方式解决了异步处理的问题
//在这里创建一个函数名方法
//目的是把这个函数放在自创建的script中请求的回调中去
//ajax请求后返回给客户端的是 bdfn(obj)即调用该函数
//其中obj是由服务器端返回的数据。
function cbfn(res){
    console.log(res);
}


btn.onclick = function(){
    let o = document.createElement("script");
    //会发生异步的
    //这里的cb要和后台保持一致 cbfn函数名字可以随便起(自定义)
    o.src = "http://localhost:4000/getAjax?cb=cbfn";
    //创建后还需要添加到页面进去
    document.querySelector("head").appendChild(o);

    // console.log(a);
    // o.onload = function(){
    //     console.log(a);
    // }
}

</script>



//服务器端
router.get("/getAjax",(ctx,next)=>{
	console.log("4000 run ");
    
    let cb = ctx.query.cb;
    console.log(cb,'cbfn');
    
    let obj = {
      a:20,
      b:20
    }
    
    
    //一旦客户端接收到了该函数执行命令就会去执行客户端的指定函数方法
    //记住下面的这一串代码并不会在服务端中执行
    //而是请求到客户端后才会执行
    //cbfn({"a":20,"b":20})
    ctx.body = `${cb}(${JSON.stringify(obj)})`;
}

jsonp的封装与运用--解决跨域方法之一

  • 封装后的运用
<body>
    <button>点击请求jsonp</button>
</body>
<script>
//封装后jsonp的调用

let btn = document.querySelector("button");
btn.onclick = function(){
    ajax({
        //http://localhost:4000/getAjax?cb=cbfn
        //其中cb=cbfn另外传递不需要在写在这里的url上
        url:"http://localhost:4000/getAjax",
        data:{
            name:"张三",
            age:20
        },
        //重要的是这里接收到了其是一个jsonp请求要做跨域处理
        dataType:"jsonp", //jsonp只支持get方式
        //默认(自指定)的回调函数
        jsonp:"cb",
        success:function(res){
            console.log(res)
        }
    })
}
</script>
  • 封装jsonp和ajax的使用
<script>
function ajax(options) {
    let opts = Object.assign({
        method: 'get',
        url: '',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        jsonp:"cb", //默认回调名为cb
        data: '',
        success: function () { }
    }, options)
    //处理jsonp请求;
    if(opts.dataType==="jsonp"){
        //jsonpFn封装了jsonp逻辑
        jsonpFn(opts.url,opts.data,opts.jsonp,opts.success);
        //不再进行普通的ajax请求
        return false;
    }
    //- - 回调函数名 回调函数
    function jsonpFn(url,data,cbName,cbFn){
        // cbName   cb/callback
        // 这里的cbFn是function(res){console.log(res)}
        // 如果直接拿这个去拼接路径会变成 ...&cb=function(res){console.log(res)}
        // 而我们想要的结果是...&cb=cbFn  拼接的是回调函数名而不是回调函数表达式

        //解决方法是用一个随机的函数名和要执行的回调函数表达式对应起来即可
        //做一个缓存的作用
        let fnName = "LTH_"+Math.random().toString().substr(2);
        window[fnName] = cbFn;

        //拼接完成的路径
        //传入的data是{name:"张三",age:20}要转为name='张三'&age=20
        //还要将回调函数名和回调函数也拼接上去  cb=cbFn(要获取的是函数名字不是表达式)
        // 这里传递的cb 要和 后端的 cb(let cb = ctx.query.cb;)对应上
        let path = url+"?"+o2u(data)+"&"+cbName+"="+fnName;
        // console.log(path);
        let o = document.createElement("script");
        o.src = path;
        document.querySelector("head").appendChild(o);
    }



    let xhr = new XMLHttpRequest();
    if (options.method == "get") {
        let data = o2u(opts.data)
        options.url = options.url + "?" + data;
    }
    xhr.open(options.method, options.url, true);
    for (let key in opts.headers) {
        //headers: {'content-type': 'application/x-www-form-urlencoded'}
        xhr.setRequestHeader(key, opts.headers[key]);
    }
    let sendData;
    switch (opts.headers['content-type']) {
        case 'application/x-www-form-urlencoded':
            sendData = o2u(opts.data);
            break;
        case 'application/json':
            sendData = JSON.stringify(opts.data);
            break;
    }


    xhr.onload = function () {
        let resData;
        //两种接收数据的方式 xml格式和其他
        if (xhr.getResponseHeader("content-type").includes("xml")) {
            resData = xhr.responseXML;
        } else {
            resData = JSON.parse(xhr.responseText);
        }
        options.success(resData);
    }


    if (options.method == "get") {
        xhr.send();
    } else {
        xhr.send(sendData);
    }
}


//ObjectToUrl
function o2u(obj) {
    let keys = Object.keys(obj);
    let values = Object.values(obj);
    return keys.map((v, k) => {
        return `${v}=${values[k]}`;
    }).join("&");
}
</script>
  • 百度搜索的地址https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su
<body>
    <h1>百度搜索</h1>
    <input type="text" class="myinput" />
    <div class="exchange"></div>
</body>
<script>
// console.log(ajax);
document.querySelector(".myinput").onblur = function(){
    ajax({
        //既可以跨域请求自己的服务器也可以跨域请求其他地方的服务器
        url:"https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su",
        dataType:"jsonp",
        data:{
            wd:this.value
        },
        //jsonp:'cb',  //这是默认的不传递也可以
        success:function(res){
            // console.log(res);

            //结果在res.s里面
            let data = res.s;
            let html = "<ul>";
            data.forEach(v=>{
                html += "<li>"+v+"</li>";
            })
            html += "</ul>";
            document.querySelector(".exchange").innerHTML = html;
        }
    })
}
</script>
  • 蘑菇街的数据请求的地址https://list.mogu.com/search
<script>
    let page = 1;
    getData();
    function getData() {
        ajax({
            url: "https://list.mogu.com/search",
            data: {
                _version: 8193,
                ratio: "3%3A4",
                cKey: 15,
                page: page,
                sort: "pop",
                ad: 0,
                fcid: 52014,
                action: "food"
            },
            dataType: "jsonp",
            jsonp: "callback",
            success: function (res) {
                console.log(res,'sssss');
                if (res.status.code == 1001) {
                    page++;
                    let data = res.result.wall.docs;
                    // console.log(data);
                    data.forEach(item => {
                        createEelement(item);
                    })
                }
            }
        })
    }

    function createEelement(item) {
        // console.log(item);
        let mydiv = document.createElement("div");
        mydiv.classList = "item";
        mydiv.innerHTML = `<img
                    src="${item.img}" />
                    <div class="bottom-describe">
                        <p class="describe">
                            ${item.title}
                        </p>
                        <div class="priceContainer">
                            <b>¥${item.price}</b>
                            <span class="oldPrice">¥${item.orgPrice}</span>
                            <span class="mystar">
                                <img src="https://s18.mogucdn.com/p2/160908/upload_27g4f1ch6akie83hacb676j622b9l_32x30.png"
                                    alt="" />
                                ${item.cfav}
                            </span>
                        </div>
                    </div>`;
                document.querySelector(".itemContainer").appendChild(mydiv);
    }
    
    
    
    //下拉加载数据;
    document.onscroll = function(){
        let windowHeight = document.documentElement.clientHeight;
        let contentHeight = document.documentElement.offsetHeight;
        //内容的高度减去可视区域的高度得到滚动的高度
        let scrollHeight = contentHeight - windowHeight;
        //当前滚动的高度
        let scrollTop = document.documentElement.scrollTop;
        if(scrollTop>=(scrollHeight-10)){
            getData();
        }

    }

</script>

CORS--解决跨域方法之二

  • 理解同源策略:如果请求资源和被请求资源都在同一个域,就叫同域请求(同源请求)。
  • 同源:协议+主机(域名、IP)+端口都相同(注意:只要有一个不一样,那么就非同源请求 - 跨域请求)
  • 同源策略具体可参考官网https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
  • 跨域读操作(Cross-origin reads)一般是不被允许的,但常可以通过内嵌资源来巧妙的进行读取访问。例如,你可以读取嵌入图片的高度和宽度,调用内嵌脚本的方法
  • 可以使用CORS(跨域资源共享)来解决跨域问题。CORS把请求分为两大类 分别是简单请求和预检请求,具体可参考官网https://developer.mozilla.org/zh-CN/docs/Glossary/CORShttps://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
  • 跨域资源共享是基于一套http头信息来实现共享跨域资源的。基本的http头信息如下:
  • 我们来模拟跨域请求的场景,构建目录如下:
  • 该server2的app.js代码为:
const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('koa-router');

const app = new Koa();

app.use(KoaStaticCache('./public', {
    prefix: '/public',
    gzip: true,
    dynamic: true
}));

const router = new KoaRouter();


router.get('/data1', async ctx => {
    ctx.body = '请求结果server2';
});

app.use(router.routes());

app.listen(9999);
  • 该server2下的public/index.html的代码为:
  • 知识点为简单请求和预检请求:
<body>
    <h1>我是 Server2 的 index.html</h1>

    <img src="https://img.kaikeba.com/70350130700202jusm.png" alt="">

    <hr />

    <!-- 请求同源不会发生跨域请求 -->
    <button>请求 server2 的 /data1</button> 

    <!-- 符合CORS的简单请求 -->
    <button>请求 server1 的 /data1</button>
    
    <!-- 符合CORS的预检请求 -->
    <button>请求 server1 的 /data2 - 非简单请求</button>



    <script>
        let btnElements = document.querySelectorAll('button');


        // 请求同源不会发生跨域请求
        btnElements[0].onclick = function () {

            let xhr = new XMLHttpRequest();

            xhr.open('get', '/data1', true);
            // xhr.open('get', 'http://localhost:9999/data1', true);

            xhr.onload = function () {
                console.log(this.responseText);
            }

            xhr.send();
        }


        //符合CORS的简单请求
        btnElements[1].onclick = function () {

            let xhr = new XMLHttpRequest();

            xhr.open('get', 'http://localhost:8888/data1', true);

            xhr.onload = function () {
                console.log(this.responseText);
            }

            xhr.send();
        }


        // 符合CORS的预检请求
        btnElements[2].onclick = function () {

            let xhr = new XMLHttpRequest();

            xhr.open('post', 'http://localhost:8888/data2', true);

            xhr.onload = function () {
                console.log(this.responseText);
            }


            // 会触发简单请求
            // xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
            // xhr.send('a=1&b=2');


            //会触发预检请求
            xhr.setRequestHeader('content-type', 'application/json');
            xhr.send('{"a": 1, "b": 2}');
        }
    </script>
</body>
  • 该server1下的app,js的代码为:
const Koa = require('koa');
const KoaRouter = require('koa-router');
const KoaBody = require('koa-body');

const app = new Koa();
const router = new KoaRouter();

router.get('/data1', async ctx => {

    // 可以准备一个白名单(whiteLists = ['http://localhost:9999', 'http://kaikeba.com'])
    // 获取到当前请求的客户端所在的源: 请求中的 Origin 头,与白名单进行比较
    // Origin: http://localhost:9999  协议主机端口
    // ctx.get('Origin') 如果在白名单内则ctx.set('Access-Control-Allow-Origin', ctx.get('Origin'));



    // 只有这个域可以跨域来访问获取我这个域的资源
    //除了 http://localhost:9999,其它外域均不能访问该资源
    // ctx.set('Access-Control-Allow-Origin', 'http://localhost:9999');
    //所有的域都可以访问这个服务器下这个目录的内容
    //该资源可以被任意外域访问

    //由服务器端设置 由客户端验证该头信息
    ctx.set('Access-Control-Allow-Origin', '*');



    console.log('有人请求了');
    ctx.body = '这个域可以让我返回数据给你';
});


//简单请求
router.post('/data2', KoaBody(), async ctx => {


    //这种设置方法是最最最简单的--不适用于很多场景
    //简单处理
    ctx.set('Access-Control-Allow-Origin', '*');

    console.log('body', ctx.request.body);

    ctx.body = '这个域可以让我返回数据给你';
});

//后端也必须去处理这个预检请求
//产生预检请求时浏览器会多发送一个请求 OPTIONS
//总共发了两个请求
//第二个请求等待着OPTIONS这个请求向他交代 后事 再带着 后事 发起请求
router.options('/data2', async ctx => {
    console.log('/data2  的预检请求');

    //我们要通过这次的预检请求返回一些特殊的头信息给客户端
    //客户端再根据返回的这些头信息
    //去处理那个 非简单的请求 是否能够正常处理

    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'content-type');

    ctx.body = '这个域可以让我返回数据给你';
})

app.use(router.routes());

app.listen(8888);

后端代理--解决跨域方法之三

  • 使用node.js的原生方法http.request
  • HTML页面中的一个按钮可以发送Ajax请求的页面代码逻辑
<button>请求 server1 的 /data1</button>

 btnElements[0].onclick = function () {
 	let xhr = new XMLHttpRequest();
    //我假装访问一个同域的地址
    xhr.open('get', '/server1-data1', true);
    
    xhr.onload = function () {
      console.log(this.responseText);
    }

    xhr.send();
 }
  • 基于node.js封装的一个后端代理方法的实现逻辑:因为用过node.js来发送http请求是不存在跨域限制的。
function myRequest(opts) {
    return new Promise((resolve) => {
        const options = Object.assign({
            protocol: 'http:',
            hostname: 'localhost',
            port: 8888,
            path: '/',
            method: 'GET'
        }, opts);
        
        const req = http.request(options, (res) => {
            // console.log(res);
        
            let str = '';
            res.setEncoding('utf8');
            res.on('data', (chunk) => {
                // console.log(`BODY: ${chunk}`);
                str += chunk.toString();
            });
            res.on('end', () => {
                console.log('数据接收完成', str);
                resolve(str);
            });
        });
        
        req.write('');
        req.end();
    })
}



//客户端假装来访问一个同域的地址
//可是我在这个地址页面做的事情确是利用node.js的http模块将
//一个跨域的地址页面信息给拿到并在这里返回给客户端
router.get('/server1/data1', async ctx => {
    let rs = await myRequest({
        path: '/data1'
    });
    ctx.body = rs;
});

  • 基于第三方库'koa-server-http-proxy'来实现后端代理,可以参考官网https://www.npmjs.com/package/koa-server-http-proxy
//前端
btnElements[1].onclick = function () {
	let xhr = new XMLHttpRequest();
    
//  我们通过第三方库koa-server-http-proxy做好一个映射地址关系
            xhr.open('get', '/server1/data1', true);
            
            xhr.onload = function () {
              console.log(this.responseText);
            }

            xhr.send();
}


//后端
const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('koa-router');
const http = require('http');
const KoaServerHttpProxy = require('koa-server-http-proxy')
const app = new Koa();

app.use(KoaStaticCache('./public', {
    prefix: '/public',
    gzip: true,
    dynamic: true
}));
const router = new KoaRouter();



app.use(KoaServerHttpProxy('/server1', {
    target: 'http://localhost:8888',
    pathRewrite: {
        '^/server1': ''
    }

    //服务器端的请求是http://localhost:9999/server1/data1
    //经过这个第三方库路径改写为http://localhost:8888/data1
}));



app.use(router.routes());
app.listen(9999);

Ajax提交用户登录信息时通过jwt鉴权的实现流程

  • 客户端的HTML代码详情:
<div id="login">
        <p>
            用户名:<input type="text" id="username">
        </p>
        <p>
            密码:<input type="password" id="password">
        </p>
        <p>
            <button id="loginBtn">登录</button>
        </p>
    </div>

    <hr />

    <button id="getDataBtn">获取数据</button>
    <div id="content">
        <!-- 通过ajax获取(会经过鉴权)再填充这里的内容 -->
        <!-- 只有鉴权成功才会显示内容为 这是用户登录鉴权后才能看到的内容 -->
    </div>

  • 前端Ajax请求、请求方式为post、请求的路径为/login的逻辑代码:
let loginBtnElement = document.querySelector('#loginBtn');
let getDataBtnElement = document.querySelector('#getDataBtn');
let contentElement = document.querySelector('#content');
let authorizationData = '';


//模拟用户输入用户信息后通过Ajax发送给后端的数据
//后端获得数据后需要通过查询数据库来判断用户登录信息的准确性
let userLoginInfo = {
  username: 'lth',
  password: '123'
}



//用户点击登录按钮后发送Ajax请求
loginBtnElement.onclick = function () {

    let xhr = new XMLHttpRequest();

    xhr.open('post', '/login', true);


    //后端返回数据结果(可能鉴权不成功可能成功)之后的操作
    xhr.onload = function () {
        //后端只有在鉴权成功的时候才会设置Authorization头并返回给前端
        //现在需要的关注的是后端是否有返回Authorization头
        authorizationData = xhr.getResponseHeader('Authorization');
        // console.log('authorizationData', authorizationData);


        if (authorizationData) {
            // 如果后端有设置这个Authorization头信息并赋返回给前端

            //为了避免因为前端页面一刷新便失去authorizationData头信息
            //前端在拿到此头信息后存储在localStorage中
            localStorage.setItem('authorizationData', authorizationData);
        }
    }

    //ajax可以自己设置头信息再通过下面的方法传递数据
    xhr.setRequestHeader('content-type', 'application/json');
    //客户端给服务器端发送json格式数据
    xhr.send(JSON.stringify(userLoginInfo));

}

  • 后端收到请求方式为post和请求的路径为/login的Ajax请求后的逻辑代码:
const Koa = require('koa');
const KoaRouter = require('koa-router');
const KoaBody = require('koa-body');
const KoaStaticCache = require('koa-static-cache');


//第三方jwt工具库
//通过签名保证数据的合法性
//算法加密在后端完成
//'koa-jwt'只是将'jsonwebtoken'进一步封装为了一个中间件
//而token的鉴权还是得通过'jsonwebtoken'来实现
const jwt = require('koa-jwt');
const jsonwebtoken = require('jsonwebtoken');

const app = new Koa();



// secret :加密秘钥
const secret = 'lth';
//unless的作用是我在访问后面这些路由的时候不用对我写的这个路由做验证了
//例如不要对/public下的路由做验证了,这些不用授权直接可以访问
//会自己去判断token是否是伪造的 因为每个请求都会来走一遍中间件
//其他未经过jwt授权的页面都是无法直接访问的
app.use(jwt({
    secret
}).unless({
    path: [/^\/public/, /^\/login/]
}));

app.use(KoaStaticCache('./public', {
    prefix: '/public',
    gzip: true,
    dynamic: true
}));

const router = new KoaRouter();



//接下来是是收到后端Ajax请求/login的逻辑
//通过这个接口来颁发一个token信息
router.post('/login', async ctx => {

    //后端收到前端提交过来的用户信息后(这里即前端发送的json数据)
    //通过查询数据库的方式验证用户信息(这里省去查询数据库)
    //验证成功后发送头Authorization信息
    //该头中包含着playload生成的token可以供后续的验证提供对应信息
    let payload = {
        uid: 1,
        username: 'lth'
    };


    // 后端受到前端发送过来的数据后
    // 客户端给服务器端发送json格式数据--下一行中是前端发过来的数据
    // xhr.send(JSON.stringify(userLoginInfo));



    // 第一种方法:直接设置头Authorization信息
    // 不可鉴权的头Authorization信息被前端存储后是可以被人随意篡改的
    // ctx.set('Authorization', JSON.stringify(payload));


    //第二种方法:使用第三方库生成了一个可以鉴权的token被前端存储后是很难被人随意篡改的
    //该token信息中包含着用户不敏感的信息
    //后端发送给前端这个头Authorization后前端会将它放在local storage里
    ctx.set('Authorization', jsonwebtoken.sign(payload, secret));

    ctx.body = '登录成功';
})
  • 前端Ajax请求、请求方式为get、请求的路径为/mySecret的逻辑代码:

//如果判断用户登录(即localStorage中存储这个keyAuthorization)这个请求会携带着头Authorization信息
//如果判断用户未登录那么这个请求不会携带者头Authorization信息


//点击获取数据的按钮
getDataBtnElement.onclick = function () {
    let xhr = new XMLHttpRequest();

    xhr.open('get', '/mySecret', true);

    xhr.onload = function () {
        contentElement.innerHTML = xhr.responseText;
    }




    //如果localStorage中没有设置key为authorizationData的值
    //那么从这取出来的值可能会是一个null值
    //说明用户并未登录--未登录没有判断头authorizationData信息--未保存至localstorage
    //所以在加一层判断即如果auth为null值则不设置请求头Authorization
    let auth = localStorage.getItem('authorizationData');
    console.log(auth,'auth');
    if (auth) {
        xhr.setRequestHeader('Authorization', 'Bearer ' + auth);
    }
    xhr.send();
}
  • 后端收到请求方式为get和请求的路径为/myScret的Ajax请求后的逻辑代码:
//通过ajax来请求这个接口xhr.open('get', '/mySecret', true);
router.get('/mySecret', async ctx => {



    // 判断用户是否有权限(是否登录)
    // 判断现在的客户端请求时有没有携带者这个头Authorization
    // 如果有这个头且这个头信息的内容是我们允许的用户id则返回对应的数据

    // 即判断前端Ajax请求中有无设置
    // xhr.setRequestHeader('Authorization', authorizationData);
    // 设置了后后端接收到该头信息后,判断有头信息的内容是否符合我们要求

    let authorizationData = ctx.get('Authorization');
    console.log('authorizationData: ', authorizationData);


    //自动解构token的数据并存到'ctx.state.user'中
    console.log('ctx.state.user', ctx.state.user);

    if (ctx.state.user) {
        //这里不限制token的内容
        //限制的写法  if(ctx.state.user.uid==1)  //token中的uid为1才呈现内容
        ctx.body = '你通过jwt鉴权了可以观看vip视频了';
    } else {
        ctx.body = '你没有权限观看视频';
    }


});


app.use(router.routes());

app.listen(8888);

  • 利用img标签的src属性携带token至服务器端得到鉴权最终成功显示在客户端给用户查看
//前端的核心实现逻辑
//浏览器设置请求头时xhr.setRequestHeader('Authorization', 'Bearer ' + auth);
//需要加上'Bearer '的前缀
//但是如果使用img的src属性来添加token信息是不用添加前缀的
xhr.onload = function() {
    // console.log(xhr.responseText);
    let data = JSON.parse(xhr.responseText);

    let img = new Image();
    img.src = '/static/upload/' + data.filename;
    contentList.appendChild(img);
}



//后端的核心实现逻辑
app.use(
    KoaJwt({
        secret: key,
        //如果自配置优先从自配置中的gettoken获取token值来鉴权
        //否则首选是从cookie中获取再者是从localStorage中获取token值来鉴权
        getToken(ctx) {
            // console.log('ctx.query.authorization', ctx.query.authorization);
            return ctx.query.authorization;
        }
    })
    .unless({
        path: [
            /^\/public/,
           
            /^\/login/,
        ]
    })
);

Ajax请求实现多文件上传的流程

  • 前端HTML页面的代码
<header>
	<button class="btn">上传</button>
	<input type="file" multiple id="attachment" style="display: none;">
</header>

<div class="content-list">
	<!-- 在这里显示上传的图片 -->
	<!-- <img src="/static/upload/upload_feb94743eab1200ce7347e2cdfe1519c.jpg" alt=""> -->
</div>

<div class="task_panel" style="display: none;">
	<div class="task_header">
		<h4>当前任务:1/3</h4>

		<span class="icon all-close"></span>
	</div>
	<ul class="task_body">
		<!-- <li>
			<span>任务一</span>
			<div class="task-progress-status">
				上传中……
			</div>
			<div class="progress"></div>
		</li> -->
	</ul>
</div>
  • 上传多个图片的逻辑代码
//上传按钮
let btnElement = document.querySelector('.btn');
//上传文件input框
let attachmentElement = document.querySelector('#attachment');

//隐藏了上传输入框的同时让用户点击上传按钮时触发上传输入框的点击事件
btnElement.onclick = function () {
    attachmentElement.click();
}


//上传input框onchange时
attachmentElement.onchange = function () {
    const xhr = new XMLHttpRequest();

    xhr.open('post', '/save', true);


    let fd = new FormData();


    //上传文件input框
    // let attachmentElement = document.querySelector('#attachment');
    //上传多个图片
    for (let i = 0; i < attachmentElement.files.length; i++) {
        fd.append('attachment', attachmentElement.files[i]);
    }

	
    

    //前端接收到后端返回的数据后根据数据生成图片数据呈现在前端页面
    xhr.onload = function () {
    console.log(xhr.responseText, '后端返回的数据是什么');

    // 把后端返回的图片地址动态添加到页面中

    //  <div class="content-list">
    // 	<!-- 在这里显示上传的图片 -->
    // </div>

    var urlArr=JSON.parse(xhr.responseText);
    //需要先获得已经存在页面(数据库里的数据)然后再累加现在上传的图片的数据
    //inner
    let Urlinner='';
    urlArr.map((item,index)=>{
        return Urlinner+=`<img src="\\${item}" alt=""></img>`
    }).join('')
    // console.log(inner.replace(/\\/g,'/'));
    Urlinner=Urlinner.replace(/\\/g,'/');
    let contentList=document.querySelector('.content-list');
    contentList.innerHTML+=Urlinner;
}
	
    
      //根据上传的进度改变上传的进度的宽度和上传的速度
  xhr.upload.onprogress = function (e) {
      // e.total : 上传的总大小 
      // e.loaded : 已经上传的数据大小
      let v = (e.loaded / e.total * 100).toFixed(2);

      taskProgressStatusElement.innerHTML = v + '%';
      progressElement.style.width = v + '%';
  }

    xhr.send(fd);
}
  • 后端接收上传的图片并存储数据库的逻辑:
router.post('/save', KoaBody({
    multipart: true,
    formidable: {
        uploadDir: './static/upload',
        keepExtensions: true
    }
}), async ctx => {
    // ctx.request.files.attachment
    // 多个文件时格式是[{},{},{}]
    // console.log(ctx.request.files.attachment,'前端上传的图片信息');
    let files= ctx.request.files.attachment;
    // console.log(files,'filesfiles');

    //现在我想要把上传的文件都存储到数据库里
    //单张图片上传我需要的信息有type,size,path
    let photos=[];
    photos.length=0;
    let urlArr=[];
    if(files.length===undefined){
        //单文件
        urlArr.push(files.path);
        //上传到数据库
        photos.push({
            type:files.type,
            size:files.size,
            path:files.path
        });
        let [rs] = await query(
            "INSERT INTO `photos` (`filename`, `type`, `size`) VALUES (?, ?, ?)",
            [photos[0].path, photos[0].type, photos[0].size]
        );
    }else{
        for(let i=0;i<[...files].length;i++){
            //多图片上传
            urlArr.push([...files][i].path);
            //上传到数据库
            photos.push({
                type:[...files][i].type,
                size:[...files][i].size,
                path:[...files][i].path
            });
            let [rs] = await query(
                "INSERT INTO `photos` (`filename`, `type`, `size`) VALUES (?, ?, ?)",
                [photos[i].path, photos[i].type, photos[i].size]
            );
        }
    }
    console.log(photos,'xxxx');
    console.log(urlArr,'12346');


    // 把上传成功后的当前这个图片的访问地址返回给 前端
    ctx.body = urlArr;
});

通过Ajax实现轮询(轮番查询)

  • 需求:通过提交表单数据(比如在2.html)发送Ajax请求修改服务器中的数据,但是往往服务器中的数据(比如在1.html)有变动时不能及时推送数据到客户端
  • 我们想要的效果是在2.html页面中(客户端)提交表单数据成功修改服务器的数据后,服务器能够及时的推送数据到客户端即1.html中让客户端页面能够显示出新内容
  • 可是实际效果是除非我们手动的刷新页面让页面重新向服务器发送一次请求才能在页面上显示新数据。说明传统的请求/响应模式在这方面的实现上有局限
  • 需求原因分析:HTTP协议 是一个数据传输协议,它主要是用来定义数据是如何进行传输的,以及数据包的格式:
  • HTTP协议采用请求/响应 模式,即首先必须由客户端发送请求,服务器再对该请求进行响应(减少服务器开销)。但是会出现服务端不能及时推送数据到客户端的问题。
  • HTTP协议是无状态 模式 ;HTTP 不保存(不持久化、不记忆)每次请求的客户端(减少服务器开销)。服务端无法得知当前客户端请求的内容是否已经处理过或是否已经验证过身份等。
轮询:
客户端定时向服务器发送Ajax请求
服务器接到请求后无论是否有响应的数据
都马上返回响应信息并关闭连接。


3.html


getData();
//轮番查询这里用setInterval来模拟实现每隔1s发送一次Ajax请求
//一直通过Ajax调用/data接口来更新页面
setInterval(getData, 1000);

function getData() {
    let xhr = new XMLHttpRequest();

    xhr.onload = function() {
        let users = JSON.parse(xhr.responseText);

        listElement.innerHTML = '';
        users.forEach( user => {
            let li = document.createElement('li');
            li.innerHTML = user.username;
            listElement.appendChild(li);
        } );
    }

    xhr.open('get', '/data', true);
    xhr.send();
}

通过Ajax实现长轮询

  • 与简单轮询相似,只是在服务端在没有新的返回数据情况下不会立即响应,而会挂起,直到有数据或即将超时才会响应数据返回给客户端。
  • 长轮询优点:实现也不复杂,同时相对轮询,节约带宽。
  • 长轮询缺点:所以还是存在占用服务端资源的问题,虽然及时性比轮询要高,但是会在没有数据的时候在服务端挂起,所以会一直占用服务端资源,处理能力变少。
  • 长轮询客户端实现
轮番查询一直通过Ajax调用/data接口来更新页面
而长轮询则在服务器端针对此来做了优化

实现思路是:
判断当前请求的数据和数据库中的数据是否有变化(是否一致)
如果有变化在返回数据
如果没有变化服务器就在休眠一会(休眠(sleep)时不返回数据)

所以我们需要创建一个变量来桥接客户端
(在客户端创建一个特殊标志来记录数据)和服务器:
通过此来判断客户端要请求的数据和服务器中的数据是否一致
(这里使用query的方法来实现)


//长轮询客户端实现


let n = 0;

getData();

function getData() {
    let xhr = new XMLHttpRequest();

    xhr.onload = function() {
        let users = JSON.parse(xhr.responseText);

        listElement.innerHTML = '';

        //通过服务器返回给客户端的数据来实时更新此标志变量
        n = users.length;


        users.forEach( user => {
            let li = document.createElement('li');
            li.innerHTML = user.username;
            listElement.appendChild(li);
        } );

        //等待服务器返回数据后在等待1s再次向服务器发送Ajax请求
        setTimeout(getData, 1000);
    }

    //请求时带上客户端标志数据的变量
    xhr.open('get', '/data?n=' + n, true);
    xhr.send();
}



  • 长轮询服务器端实现
//服务器端      
router.get('/data', async ctx => {
    let n = ctx.query.n;
    console.log('当客户端的标志数据的n和服务器的数据校验一致后(无需更改)服务器会休眠一会(挂起)不响应数据');
    console.log('服务器这里接收到的n是来自客户端的标志', n);

    // testData 检测数据是否有变化,
    // 如果没有变化 则每隔一段时间进行数据的比较
    // 直到数据有变化为止(实际应用中,最好设置不好查询太久)
    // 数据改变后会跳出await testData(n)去执行ctx.body = users来更改视图
    await testData(n);


    //如果当客户端的标志数据的n和服务器的数据校验不一致后在服务器结束之前的休眠后会立即向客户端返回(新)数据
    ctx.body = users;
});


async function testData(num) {
    // 数据和当前请求的客户端数据是一致的
    // 不仅要休眠还要判断数据是否一致
    // 加强版可以设置个超时时间或者轮询几次后结束(算了服务器返回数据吧);否则服务器判断数据不变后一直挂起(一直处于不响应不返回数据的状态)也是问题
    if (num == users.length) {
        await sleep(5000);
        await testData(num);
    }
}

function sleep(t) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, t);
    });
}

SSE(Server Send Event)服务器推送

  • 一个客户端获取新的数据通常需要发送一个请求到服务器,也就是向服务器请求的数据。使用 server-sent 事件,
  • 服务器可以在任何时刻向我们的客户端推送数据和信息。
  • 这些被推送进来的信息可以在这个客户端上作为 Events + data 的形式来处理。
  • 可以参考官网https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
  • 可以简单理解为对长轮询的一次标准化封装
  • EventSource 是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。
  • 一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。
  • 与 WebSockets,不同的是,服务端推送是单向的。数据信息被单向从服务端到客户端分发. 当不需要以消息形式将数据从客户端发送到服务器时,这使它们成为绝佳的选择。例如,对于处理社交媒体状态更新,新闻提要或将数据传递到客户端存储机制(如IndexedDB或Web存储)之类的,EventSource无疑是一个有效方案。
  • 客户端通过使用 EventSource 类接口来完成请求。
  • 问题:如果我客户端要更新怎么办?如果客户端想要实时的往服务器端推送数据怎么办?
  • sse单向推送 由客户端请求然后 服务器端 不断的推送数据给客户端
  • 实现流程为:
  • 客户端 使用 EventSource 类接口来完成请求。
https://developer.mozilla.org/en-US/docs/Web/API/EventSource

//如下
let source = new EventSource("/data");
  • 服务端需要做如下一些设置:
//设置头信息
ctx.set('content-type', 'text/event-stream');

//设置返回数据格式
ctx.body = `event: ping\ndata: {"time": "${new Date()}"}\n\n`


//可参考官网
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
  • 完整的示例如下:
客户端

/**
* 通过 EventSource 发送的请求,请求头中会包含
*  Accept: text/event-stream
* 表示,服务器的响应头content-type必须是text/event-stream
*/


let source = new EventSource("/data");

source.onmessage = function(event) {
  console.log('message:'+event.data);
}
		
		
//监听ping事件		
source.addEventListener("ping", function (event) {
  const newElement = document.createElement("li");
  const time = JSON.parse(event.data).time;
  newElement.innerHTML = "ping at " + time;
  listElement.appendChild(newElement);
});
        


//服务器端
router.get('/data', async ctx => {
    let n = ctx.query.n;
	
    
    //一定得设置这种content-type
    ctx.set('content-type', 'text/event-stream');
    
    
    // 不能再用这种返回格式:ctx.body = users;
    // {'event': 'ping', 'data': '...'}
    ctx.body = `event: ping\ndata: {"time": "${new Date()}"}\n\n`
});

WebSocket

  • WebSocket 是 HTML5 开始提供的一种在单个连接上进行全双工通讯的协议。使用协议:ws,其基于 HTTP 协议进行数据传输,但是会持久化链接和状态。
  • 基于 node.js 的 WebSocket 库:socket.io 的使用(配合koa):我们想要实现在一个实现http协议的网页中让其中的局部功能页面上使用ws协议来完成实时通信
  • 实现一个十分简易的聊天室:
  • 客户端实现的代码:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font: 13px Helvetica, Arial;
        }

        form {
            background: #000;
            padding: 3px;
            position: fixed;
            bottom: 0;
            width: 100%;
        }

        form input {
            border: 0;
            padding: 10px;
            width: 80%;
            margin-right: 0.5%;
        }

        form button {
            width: 9%;
            background: rgb(130, 224, 255);
            border: none;
            padding: 10px;
        }

        #messages {
            list-style-type: none;
            margin: 0;
            padding: 0;
        }

        #messages li {
            padding: 5px 10px;
        }

        #messages li:nth-child(odd) {
            background: #eee;
        }
    </style>
</head>

<body>
    <ul id="messages"></ul>
    <form action="">
        <button>Login</button>
        <input id="m" autocomplete="off" />
        <button>Send</button>
    </form>

    <script src="/socket.io/socket.io.js"></script>
    <script>
        let btnElements = document.querySelectorAll('button');
        let mElement = document.querySelector('#m');
        let messagesElement = document.querySelector('#messages');
        let socket;

        document.forms[0].onsubmit = function (e) {
            e.preventDefault();
        }

        btnElements[0].onclick = function () {
            //发送请求升级为ws协议
            socket = io();

            console.log(socket, '客户端和服务器都可以知道这个socket');


            // 因为链接是异步的,所以监听connect(内置事件)事件
            // button上显示出当前的socket.id
            // 第一种方式:用内置事件来监听
            socket.on('connect', () => {
                btnElements[0].innerHTML = socket.id;
            });

            // 第二种方式:用自定义事件来监听
            // 客户端收到服务器端包装后的数据后在实时更新到页面上
            socket.on('chat', data => {
                // 把数据显示在ul中
                let li = document.createElement('li');
                li.innerHTML = `
                    <strong>${data.id}</strong>
                    <span>${data.message}</span>
                    <span>当前时间为${data.chatTime}</span>
                `;

                messagesElement.appendChild(li);
            });
        }


        btnElements[1].onclick = function () {
            if (mElement.value != '') {
                //触发事件发送数据给服务器端
                socket.emit('message', {
                    message: mElement.value
                });

                // 在客户端输入消息后把数据显示在ul中
                // let li = document.createElement('li');
                // li.innerHTML = `
                //     <strong>${socket.id}</strong>
                //     <span>${mElement.value}</span>
                // `;
                // messagesElement.appendChild(li);



                //然后清空消息栏
                mElement.value = '';
            }
        }
    </script>
</body>
</html>
  • 服务器端实现的代码:
const http = require('http');
const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('koa-router');
const ioServer = require('socket.io');

//app不是http.Server对象不能直接传入const io = ioServer(httpServer);
const app = new Koa();


// 利用node内置http模块创建一个http.Server对象
// node内置的http.createServer的返回值就是一个http.Server对象
// 然后自己再httpServer.listen(8888)开启一个监听端口
const httpServer = http.createServer(app.callback());



app.use( KoaStaticCache('./public', {
    prefix: '/public',
    gzip: true,
    dynamic: true
}) );

const router = new KoaRouter();




router.get('/data', async ctx => {
    ctx.body = 'data';
});

app.use( router.routes() );

// 构建websocket服务器(必须注意传入的参数的问题)
// 传递的参数必须是一个httpServer对象(类型是http.Server对象)
const io = ioServer(httpServer);

let sockets = [];

// socket 套接字(插座)-> client&server 链接对象
// 每一个socket对象就是一个客户端和服务端的链接对象
// 服务器端可以有多个socket对象(只要你存起来)
io.on('connection', socket => {
    console.log('有人通过websocket请求了', socket.id);
    // console.log('有人通过websocket请求了', socket,'socket');

    sockets.push(socket);

    // 监听当前 socket 的对应事件(该事件是客户端发来的)
    // 注意客户端是socket.emit  服务端是socket.on
    socket.on('message', data => {
        console.log(socket.id, data); //{message:'发送过来的消息'}

        // 把某个客户端发送过来的消息转发给其它客户端
        // 把客户端发过来服务器端的消息包装后在发给客户端
        // sockets.forEach(s => {
        //     s.emit('chat', {
        //         id: socket.id,
        //         ...data
        //     });
        // });

        var formatDateTime = function (date) {
            var y = date.getFullYear();
            var m = date.getMonth() + 1;
            m = m < 10 ? ('0' + m) : m;
            var d = date.getDate();
            d = d < 10 ? ('0' + d) : d;
            var h = date.getHours();
            h=h < 10 ? ('0' + h) : h;
            var minute = date.getMinutes();
            minute = minute < 10 ? ('0' + minute) : minute;
            var second=date.getSeconds();
            second=second < 10 ? ('0' + second) : second;
            return y + '-' + m + '-' + d+' '+h+':'+minute+':'+second;
        };

        let chatData = {
            id: socket.id,
            chatTime:formatDateTime(new Date()),
            ...data
        }


        // 这个是发送给指定的socket
        // 完成了自己发自己显示更新页面的需求
        socket.emit('chat', chatData);

        // 其它客户端可以统一用广播的形式来显示更新页面
        // broadcast => 除了当前自己以外的其它socket
        // broadcast => sockets.filter(s => s != socket);
        socket.broadcast.emit('chat', chatData);
    });




});



// app.listen(8888);
httpServer.listen(8888);

-package.json

  "dependencies": {
    "koa": "^2.13.0",
    "koa-router": "^10.0.0",
    "koa-static-cache": "^5.1.4",
    "socket.io": "^3.0.3"
  },