Ajax原理理解与封装

191 阅读5分钟

定义:

​它是浏览器提供的一套方法,可以实现页面无刷新更新数据,提高用户浏览网站应用的体验(异步,局部更新)

应用场景

  1. 页面上拉加载更多数据
  2. 列表数据无刷新分页
  3. 提交表单之前验证数据合法性
  4. 搜索框提示文字下来列表

ajax的运行环境

​ 运行在网站环境中才能生效(需要响应的服务器)

ajax运行原理及实现

image.png ​ ajax相当于浏览器发送请求与接受响应的代理人,以实现不影响用户感受的局部刷新页面

ajax的使用步骤

get
// 1.创建ajax对象 
let xhr = new XMLHttpRequest();
// 2.告诉ajax请求地址和请求方式
xhr.open('get', 'http://localhost:8090');
// 3.发送请求数据
xhr.send();
// 4.获得服务器端的响应数据
xhr.onload = function () {
    console.log(xhr.responseText);
}
post请求
// 1.创建ajax对象 
let xhr = new XMLHttpRequest();
// 2.告诉ajax请求地址和请求方式
xhr.open('post', 'http://localhost:8090/post');
let params = {'name':'lijian'}
xhr.setRquestHeader('Conten-Type','application/json')
// 3.发送请求数据
xhr.send(JSON.stringfy(params));
// 4.获得服务器端的响应数据
xhr.onload = function () {
    console.log(xhr.responseText);
}

ajax状态码

​ 0: 请求为初始化(还没调用open())

​ 1: 请求已经建立,但是还没发送(还没有调用send())

​ 2: 请求已经发送

​ 3.请求正在处理中,通常响应中已经有了部分数据

​ 4: 响应已经完成,可以获取并使用服务器的响应了

xhr.readyState//获取ajax状态码

当状态改变后系统回自动触发onreadystatechange事件

ajax的错误处理

  1. 网络通畅,服务器端可以接收到请求,服务器段返回的结果不是预期结果,如此可以判断服务器端返回的状态码,分别进行处理。xhr.status获取http状态码

  2. 网络通畅,服务器端没有接收到请求,返回404状态码

    检查请求地址是否错误

  3. 网络通畅,服务器端能接收到请求,返回500状态码

    服务器代端程序错误

  4. 网络中断,请求无法发送到服务器

    会触发xhr对象中的onerror事件,在实践中对错误进行处理

  5. ajax在ie低版本中会存在缓存问题

ajax封装

function ajax(options) {
  // 默认参数
  let defaults = {
    type: 'get',
    url: '',
    data: {},
    header: {
      'Content-Type': 'application/json'
    },
    success: () => { },
    error: () => { }
  }
  // 使用options对象覆盖默认对象,assign方法改变原对象,不用再次赋值
  Object.assign(defaults, options)
  // 创建ajax对象
  let xhr = new XMLHttpRequest()
  let params = ''
  // url拼接参数
  for (key in defaults.data) {
    params += `${key}=${defaults.data[key]}&`
  }
  // 去除最后的&
  if (params.length > 0) {
    params = params.substr(0, params.length - 1)
  }
  // 判断请求请求方法为get,则直接在url中加入?
  if (defaults.type === 'get') {
    defaults.url += '?' + params
  }
  // 设置请求方法和url
  xhr.open(defaults.type, defaults.url)

  if (defaults.type === 'get') {
    xhr.send()
  } else {
    // 设置请求参数格式
    let contentType = defaults.header['Content-Type']
    xhr.setRequestHeader('Content-Type', contentType)
    // 判断post请求的参数格式
    if (contentType === 'application/json') {
      // json
      xhr.send(JSON.stringify(defaults.data))
    } else {
      // 非json
      xhr.send(defaults.data)
    }
  }
  // 请求完成
  xhr.onload = function () {
    // 获取响应头中的数据
    let resContentType = xhr.getResponseHeader('Content-Type')
    let resText = xhr.responseText
    if (resContentType.includes('application/json')) {
      resText = JSON.parse(resText)
    }
    // 当http状态码为200时,调用成功处理函数
    if (xhr.status == 200) {
      defaults.success(resText, xhr)
    } else {
      // 当http状态码为200时,调用成功处理函数 
      defaults.error(resText, xhr)
    }

  }
}

FormData

​ FromData构造方法可以将一个表单的dom对像,转换成fromData数据对象。表单中的name属性里面的值为属性名,输入的值为属性值

普通数据处理方法

  1. get():依据属性名获得属性值

  2. set(): 依据属性名设置属性值,当没有该属性名时则添加后在设置值

  3. delete(): 输出表单属性中的值,(比如:注册时删除两个密码中的再次输入密码属性)

  4. append(): 向表单对象中追加属性值

    注意:set方法与append方法的区别是,在属性名已经存在的时候,set是直接覆盖,而append是保留两个属性值

<form id="form">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="button" value="提交" id="submit">
</form>
 <script>
    let submit = document.getElementById("submit")
    let form = document.getElementById("form")

    submit.onclick = function () {
      // 将普通的html表单转化为表单对象
      let fromData = new FormData(form)

      // FormData.get()依据属性名获取表单属性值
      // FormData.set()设置表单属性值
      let username = fromData.get('username')
      fromData.set('username', username + new Date())
      fromData.set('sex', '男')

      let xhr = new XMLHttpRequest()
      xhr.open('post', 'http://localhost:8090/formData')
      xhr.send(fromData)
      xhr.onload = function () {
        if (xhr.status == 200) {
          console.log('xhr.responseText', JSON.parse(xhr.responseText));
        }
      }
    }
  </script> 
  <script>
    let submit = document.getElementById("submit")
    let form = document.getElementById("form")

    submit.onclick = function () {
      // 将普通的html表单转化为表单对象
      let fromData = new FormData(form)

      // FormData.get()依据属性名获取表单属性值
      // FormData.set()设置表单属性值
      let username = fromData.get('username')
      fromData.set('username', username + new Date())
      fromData.set('sex', '男')

      let xhr = new XMLHttpRequest()
      xhr.open('post', 'http://localhost:8090/formData')
      xhr.send(fromData)
      xhr.onload = function () {
        if (xhr.status == 200) {
          console.log('xhr.responseText', JSON.parse(xhr.responseText));
        }
      }
    }
  </script>

文件上传

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .progress {
      position: relative;
      width: 300px;
      height: 20px;
      text-align: center;
      background-color: gainsboro;
    }

    .progress-text {
      position: absolute;
      left: 49%;
    }

    .progress-bar {
      width: 0;
      height: 20px;
      background-color: greenyellow;
    }
  </style>
</head>

<body>
  <form id="form">
    <label for="file">选择文件
      <input type="file" name="file" id="file">
    </label>
    <div class="view" id="view_img">
    </div>
    <div class="progress">
      <span class="progress-text" id="progress_text"></span>
      <div class="progress-bar" id="progress_bar"></div>
    </div>
  </form>
  <script>
    // 获取文件元素
    let file = document.getElementById('file')
    // 文件选择空间,在用户选择文件时触发
    file.onchange = function () {
      let formData = new FormData()
      // 将用户选择的文件追加在fromDatta表单中
      formData.append('attrName', this.files[0])
      // 创建ajax
      let xhr = new XMLHttpRequest()
      xhr.open('post', 'http://localhost:8090/upload')
      // 在文件上传的过程中持续触发
      xhr.upload.onprogress = function (ev) {
        // ev.loaded:文件已经上传了多少
        // ev.total:上传文件的总大小
        let bar = document.getElementById('progress_bar')
        // 计算出当前进度
        let progress = ((ev.loaded / ev.total) * 100).toFixed(2) + '%'
        // 设置进度条宽度
        bar.style.width = progress
        // 将百分比显示在进度条中
        document.getElementById('progress_text').innerHTML = progress
      }
      xhr.send(formData)
      // 监听服务器的响应事件,处理响应数据
      xhr.onload = function () {
        // 如果返回的时成功状态
        if (xhr.status == 200) {
          let result = xhr.responseText

          //创建一个图片元素进行文件预览 
          let img = document.createElement('img')
          img.setAttribute('src', result)
          img.onload = function () {
            document.getElementById('view_img').appendChild(img)
          }
        } else {
          console.log(xhr.responseText);
        }
      }
    }
  </script>
</body>

</html>

JSONP解决跨域问题

同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介

跨域请求

​ 当前发起请求的域与该请求指向的资源所在的域不一样

jsonp解决跨域

服务器代码:server.js

const express = require('express')
const path = require('path')
const app = express()
//app.use(express.static(path.join(__dirname, 'public')))
app.get('/jsonp', (req, res) => {
  let obj = {
    name: '小明',
    age: '99'
  }
  console.log(req.query);
  let result = `${req.query.callback}(${JSON.stringify(obj)})`
  res.send(result)

  //express框架中的jsonp方法实现了以上过程
  // res.jsonp({name: '小明',age: '99'})
})
app.listen(9999)
console.log('9999端口开始监听');
客户端代码:
function jsonp(options) {
  // 动态创建script标签
  let script = document.createElement('script')
  let url = options.url
  // 处理参数
  let params = ''
  for (let value in options.data) {
    params += `&${value}=${options.data[value]}`
  }
  let callback = options.callback
  // 将匿名的callback函数的作用域提升到全局,并赋予一个具体变量名(随机生成)
  // 为什么不能写死变量名呢?因为写死后,则后面的求情回覆盖前面的请求(window.callback会被覆盖)
  let funcName = 'jsonpCallBack' + Math.random().toString().replace('.', '')
  window[funcName] = callback
  // 设置src属性值
  script.setAttribute('src', `${url}?callback=${funcName}${params}`)
  document.body.appendChild(script)
  script.onload = function () {
    document.body.removeChild(script)
  }
}

jsonp({
        url: 'http://localhost:9999/jsonp',
        data: {
          name: '张三',
          age: 101
        },
        callback: function (data) {
          console.log(data);
        }
      })

jsonp可能遇到的问题

  1. 问题:当使用document.body.append添加script标签时,会导致多次点击,文档中添加过多script标签

    解决:script元素加载完后移除掉

  2. 问题:回调函数必须在全局定义域中定义,与请求调用者联系不密切

    解决:将回调函数挂载在window全局对象上

  3. 问题:回调函数挂载在window全局变量上面后,会出现后来调用者的回调函数覆盖前面的回调函数

    解决:随机生成函数名后再挂载到window对象上