AJAX 的原理与应用

AJAX的全称: Asynchronous JavaScript And XML

大纲链接 §

[toc]


0. 服务器的基本概念与初识Ajax

  • 客户端和服务器
    • 客户端和服务器基本概念
    • 客户端和服务器通信过程
  • URL地址
  • 网页打开的过程
  • 服务器对外提供的资源包括图片以及数据
  • Ajax概念和应用场景
  • 使用 Axios 代替过时的jQuery发起Ajax请求数据
  • 接口和接口文档
  • 过时的form表单与模板引擎
  • 案例
    • 图书管理接口
    • 案例-聊天机器人接口

上网的目的:通过互联网的形式来获取和消费网络资源

0.1 客户端和服务器

  • 客户端和服务器基本概念:
    • 上网过程中,负责存放和对外提供资源的计算机,叫服务器
    • 上网过程中,负责获取和消费资源的计算机,叫客户端
  • 客户端和服务器通信过程

0.2 URL地址

  • 统一资源定位符,同于标识互联网上每个资源的唯一存放位置(不可重复)
  • 客户端只有通过URL地址,才能正确定位的存放位置,从而成功访问到对应的资源

URL地址一般有三部分组成

  • 客户端与服务器的通信协议https等,在://之前
  • 存有该资源的服务器名称www.xxx.com
  • 资源在服务器上的具体存放路径

0.3 网页打开的过程

  • 即客户端和服务器通信过程,分为:请求->处理->响应
  • 在浏览器的地址栏输入网站地址,回车,向服务器发起资源请求
  • 服务器处理这次资源请求
  • 服务器将这次请求的响应发送给客户端
    • 响应可以是成功或者拒绝

浏览器Network面板简介

  • 资源筛选页签
    • XHR 表示数据请求对象XMLHttpRequest,有的浏览器显示为 Fetch/XHR
    • Doc 表示文档资源,有的浏览器显示为 HTML
  • Headers 页签
    • Request URL
    • Request Method
    • Status Code
    • Remote Address
    • Referrer Policy
  • Response 表示服务器的响应数据,格式为对应数据的格式

0.4 服务器对外提供的资源

  • 文字
  • 脚本
  • 样式
  • 图片
  • 音频
  • 视频
  • 数据

网页中的数据也是服务器对外提供的一种资源

  • 网页中 HTML控制骨架、CSS控制样式、JS控制逻辑交互行为、数据则提供主要信息,骨架、样式和交互都为数据服务
  • 常见的数据传输格式有JSONXML(前端不常用)

0.5 网页中如何请求数据

数据也是服务器对外提供的一种资源,只要是资源,必然通过 请求 -> 响应 -> 的方式获取

  • 如果要在网页中请求服务器上的数据资源,则需要用到 XMLHttpRequest 对象
  • XMLHttpRequest 对象简称xhr,是浏览器提供给JS内建的浏览器对象,属于BOM
  • 通过new XMLHttpRequest()方法构造实例对象,可以请求服务器上的数据资源
  • const xhr = new XMLHttpRequest()

0.6 资源的请求方式(请求动词)

客户端请求服务器时,请求的方式有很多种,最常见的两种方式分别为getpost请求

  • get请求用于获取服务端资源
    • 根据 URL 地址,从服务器获取 HTML、CSS、JS、图片和数据等文件资源
  • post请求用于向服务器提交数据,即发送资源
    • 例如:登录时向服务器提交的登录消息、注册时向服务器提交的注册信息、添加用户时向服务器提交的用户数据信息等给中数据提交操作

0.6 Ajax概念和应用场景

  • 在网页中利用 XMLHttpRequest 对象和服务器进行数据交互的方式就是 Ajax
  • 页面可以无刷新的请求数据
  • Ajax 能实现静态网页无法实现的和服务器之间数据交互功能
  • 用户通过和网页或应用这一数据载体交互,实现和服务器进行数据交互
  • 代替过时的 form 表单默认提交(页面强制刷新)和 jQuery 发起 Ajax 请求数据
    • 使用 Fetch API
    • 使用 Axios

Ajax的典型应用场景

  • 用户名的检测:注册用户时,通过 Ajax 的形式,动态检测用户名是否被占用,名称查重
  • 搜索提示:当输入关键字时,通过 Ajax 的形式,动态加载搜索提示列表
  • 数据分页显示:点击页码时,通过 Ajax 的形式,根据页码值动态刷新该页页面数据
  • 数据增删改查,实现数据的交互

在现代浏览器上实现一个Ajax请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const p = () => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest() // 创建XMLHttpRequest对象

// 异步ajax,监听readystatechange事件,设置回调函数
    xhr.onreadystatechange = () => { // 状态发生变化时,函数被回调
      if (xhr.readyState === 4) { // 成功完成
        // 判断响应状态码
        if (xhr.status !== 200) return reject(xhr.status) // 失败,根据响应码判断失败原因:
        return resolve(xhr.responseText) // 成功,通过responseText拿到响应的文本:
      } else {
        // HTTP请求还在继续...
      }
    }

    // 发送请求:
    xhr.open('GET', '/api/categories')
    xhr.setRequestHeader('Content-Type', 'application/json') // 设置请求头
    xhr.send() // 到这一步,请求才正式发出

  })
}

0.7 代替过时的jQuery发起Ajax请求数据

  • 使用黑马的免费教学接口API:http://www.liulongbin.top:3006/api

jQuery

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 不带参数的请求
$(() => {
  $('button1').on('click', () => {
    $.get('http://www.liulongbin.top:3006/api/getbooks', (res) => {
      console.log(res)
    })
  })
})
// 带参数的请求 // http://www.liulongbin.top:3006/api/getbooks?id=1
$(() => {
  $('button2').on('click', () => {
    $.get('http://www.liulongbin.top:3006/api/getbooks', /* 第二个参数为参数对象 */{id: 1}, (res) => {
      console.log(res)
    })
  })
})
// 提交数据的请求
// $.post 功能单一,仅发起post请求向服务器提交数据
// $post(url, [data /*可选 要提交的数据*/], [callback /* 数据提交成功的回调*/])
$(() => {
  $('button3').on('click', () => {
    $.post('http://www.liulongbin.top:3006/api/addbook',
      /* 第二个参数为提交的数据对象 */{
        bookname: '水浒传',
        author: '施耐庵',
        publisher: '上海图书出版社'
      },
      (res) => {
        console.log(res)
      })
  })
})
/*
// 综合配置
$.ajax({
    type: '', // 请求的方式
    url: '', // 请求的 URL 地址
    data: {}, // 此次请求要携带的数据
    success: (res) => {}, // 请求成功后的回调函数
})
*/
// Get
$(() => {
  $('button4').on('click', () => {
    $.ajax({
      type: 'GET',
      url: 'http://www.liulongbin.top:3006/api/getbooks',
      params: {id: 3},
      success: (res) => {
        console.log(res)
      }
    })
  })
})
// Post
$(() => {
  $('button4').on('click', () => {
    $.ajax({
      type: 'POST',
      url: 'http://www.liulongbin.top:3006/api/addbook',
      data: {
        bookname: '三国演义',
        author: '罗贯中',
        publisher: '重庆图书出版社'
      },
      success: (res) => {
        console.log(res)
      }
    })
  })
})

Axios

  • 易用、简洁且高效的http库
  • 支持node端和浏览器端
  • 基于 Promise 管理异步,是对原生XHR的封装,用Promise的实现版本,符合最新的ES规范
  • 支持HTTP拦截器等高级配置,拦截请求和响应
  • 取消请求
  • 自动转换 JSON 数据,转换请求数据和响应数据
  • 响应超时
  • 同时请求
  • 客户端支持防御 XSRF

安装

1
2
yarn add axios
# pnpm add axios

引入

1
import axios from 'axios';

基本用法

  • axios.get(url[, config])
    • 代替 jQuery 的 $.get(url: string, [data: object /*请求参数*/], [callback: function /*成功回调*/])
  • axios.post(url[, data[, config]])
    • 代替 jQuery 的 $.post(url: string, [data: object /*要提交的数据*/], [callback: function /*数据提交成功回调*/])
  • axios({...})代替 jQuery 的 $.ajax()
  • 为其他支持的请求方法提供的别名
    • axios.request(config)
    • axios.delete(url[, config])
    • axios.head(url[, config])
    • axios.options(url[, config])
    • axios.put(url[, data[, config]])
    • axios.patch(url[, data[, config]])
    • 在使用别名方法时, url、method、data 这些属性都不必在配置中指定

axios-get.js,语法:axios.get(url[, config])

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import axios from 'axios'

// 直接发起不带参数的请求
axios.get('/api/getbooks',
  {
    baseURL: 'http://www.liulongbin.top:3006/',
    timeout: 1000
  })
  .then(
    (res) => {
      const {data, status} = res
      console.log('直接发起不带参数的请求')
      console.log('data', data)
      console.log('status', status)
      console.log('-----------------')
    }
  )

// 先配置,后发起请求
const instance = axios.create({
  baseURL: 'http://www.liulongbin.top:3006/',
  timeout: 1000
})

const instance2 = axios.create({
  baseURL: 'http://www.liulongbin.top:3006/',
  timeout: 1000,
  params: {
    id: 1
  }
})

instance.get('/api/getbooks')
  .then(
    (res) => {
      const {data, status} = res
      console.log('// 先配置,后发起不带参请求')
      console.log('data', data)
      console.log('status', status)
      console.log('-----------------')
    }
  )

instance2.get('/api/getbooks')
  .then(
    (res) => {
      const {data, status} = res
      console.log('// 先配置,后发起带参请求')
      console.log('data', data)
      console.log('status', status)
      console.log('-----------------')
    }
  )

  • 成功返回的数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 {                                                                           
  "status": 200,                                                                   
  "msg": "获取图书列表成功",                                                       
  "data": [                                                                        
    { "id": 1, "bookname": "西游记", "author": "吴承恩", "publisher": "北京图书出版社"},
    { "id": 2, "bookname": "三国演义", "author": "罗贯中", "publisher": "重庆图书出版社"},
    { "id": 3, "bookname": "红楼梦", "author": "曹雪芹", "publisher": "上海图书出版社"}
  ]
}

使用 APIFox 配置好接口 直接生成代码,以 Fetch API 为例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pnpm add node-fetch // 在Node环境中使用 Fetch API 安装 pnpm add node-fetch
import fetch, {Headers} from 'node-fetch'

const myHeaders = new Headers()
myHeaders.append('User-Agent', 'apifox/1.0.0 (https://www.apifox.cn)')

const requestOptions = {
  method: 'GET',
  headers: myHeaders,
  redirect: 'follow'
}

/*
fetch('http://www.liulongbin.top:3006/api/getbooks', requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error))
*/

const response = await fetch('http://www.liulongbin.top:3006/api/getbooks', requestOptions)
const {data} = await response.json()
console.log(data)

const response2 = await fetch('https://api.github.com/users/github', requestOptions)
const data2 = await response2.json()
console.log(data2)

axios-post.js,语法:axios.post(url[, data[, config]])

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import axios from 'axios'
// 直接发起
axios.post('/api/addbook',
  {
    bookname: '金瓶梅',
    author: '兰陵笑笑生',
    publisher: '上海图书出版社'
  },
  {
    baseURL: 'http://www.liulongbin.top:3006/',
    timeout: 1000
  }
).then(res => {
  const {data} = res
  console.log(data)
})
  .catch(err => {
    console.log(err)
  })
/* 不允许重复添加 */

// 先配置,后发起请求
const instance = axios.create({
  baseURL: 'http://www.liulongbin.top:3006/',
  timeout: 1000
})

instance.post('/api/addbook',
  {
    bookname: '三国演义',
    author: '罗贯中',
    publisher: '重庆图书出版社'
  })
  .then(
    (res) => {
      const {data, status} = res
      console.log('// 先配置,后发起不带参请求')
      console.log('data', data)
      console.log('status', status)
      console.log('-----------------')
    }
  )
  .catch(err => {
    console.log(err)
  })

axios-axios.js,语法:axios(config: object)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import axios from 'axios'
// 默认Get 读取全部数据
axios('http://www.liulongbin.top:3006/api/getbooks')
  .then((res) => {
    const {data: dataObject} = res
    console.log('默认Get 读取全部数据')
    console.log('dataObject.data: ', dataObject.data)
    console.log('--------------')
  })
// 配置选项,读取全部数据
axios({
  method: 'GET',
  url: 'http://www.liulongbin.top:3006/api/getbooks'
})
  .then((res) => {
    const {data: dataObject} = res
    console.log('配置选项,读取全部数据')
    console.log('dataObject.data: ', dataObject.data)
    console.log('--------------')
  })
// 配置params选项,读取部分数据
axios({
  method: 'GET',
  url: 'http://www.liulongbin.top:3006/api/getbooks',
  params: {id: 2}
})
  .then((res) => {
    const {data: dataObject} = res
    console.log('配置params选项,读取部分数据')
    console.log('dataObject: ', dataObject)
    console.log('--------------')
  })

// 发起提交请求
axios({
  method: 'POST',
  url: 'http://www.liulongbin.top:3006/api/addbook',
  data: {
    bookname: '哈利波特',
    author: 'JK罗琳',
    publisher: '上海译文图书出版社'
  }
})
  .then((res) => {
    const {data: dataObject} = res
    console.log('发起提交请求')
    console.log('dataObject: ', dataObject)
    console.log('--------------')
  })


0.8 接口和接口文档

使用 Ajax 请求数据时,被请求的 URL 地址,就叫做后端数据接口(简称接口),同时,每个接口必须有请求方式,例如:

  • GET 请求 http://www.liulongbin.top:3006/api/getbooks 获取图书列表的接口
  • POST 请求 http://www.liulongbin.top:3006/api/addbook 添加图书的接口

分析接口的请求过程

  • GET 请求过程:用户希望获取数据 -> 浏览器网页(数据载体)通过 Ajax 发起 GET 数据请求 -> 服务器 处理请求 -> 服务器发送响应给 浏览器 Ajax -> 浏览器网页展现结果
  • POST 请求过程:用户希望提交数据 -> 浏览器网页(数据载体)通过 Ajax 发起 POST 数据请求 -> 服务器 处理请求 -> 服务器发送响应给 浏览器 Ajax -> 浏览器网页展现结果

接口测试工具

  • 为了验证接口能否被正常访问,常常需要使用接口测试工具,来对后端数据接口进行检测
  • 接口测试工具能在不写任何代码的情况下,对接口进行调用和测试
  • 一般有如下几种方式
    • 命令行curl
    • IDE比如WebStorm自带的 工具 > HTTP客户端
    • 国外 Postman
    • 国内 apifox 和 apipost

使用 Postman 和 apifox 和 apipost

  • Apifox 使用文档
  • Postman 添加标签 Tab
    • 选择 请求类型
    • 填写 URL地址
    • 在请求参数区域,填写参数和数据
      • GET 需要在 Params 中填写
      • POST 主要在 Body 中填写
        • 一般点选 x-www-form-urlencoded 后填写
    • 发送请求按钮 Send
    • 查看服务器响应结果

使用 rap2 接口平台

  • 可以自动生成文档,导入导出给中格式文档数据

0.9 案例,以vue为例

  • 案例
    • 图书管理接口(Vue + Axios)
    • 聊天机器人接口

0.9.1 图书管理接口(Vue + Axios)

BooksInfo.vue 页面组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
  <div class="books-info-demo">
    <!-- 添加图书的Panel面板 -->
    <AddBookInfo></AddBookInfo>

    <!-- 展示图书的表格 -->
    <ShowBookInfo></ShowBookInfo>
  </div>
</template>

<script lang="ts">
import AddBookInfo from '@/components/ajax-demo/book-demo/AddBookInfo.vue';
import ShowBookInfo from '@/components/ajax-demo/book-demo/ShowBookInfo.vue';
import {defineComponent, provide, reactive} from '@vue/composition-api';
import Vue from 'vue';

export default defineComponent({
  name: "BooksInfo",
  components: {
    AddBookInfo,
    ShowBookInfo
  },
  props: {},
  setup() {
    const eventbus = reactive(new Vue());
    provide<Vue>('eventbus', eventbus);
    return {
      eventbus
    }
  }
});
</script>

实现 AddBookInfo.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<template>
  <div class="add-book card">
    <div class="card-header">
      <h3 class="card-title">添加新图书</h3>
    </div>
    <div class="card-body form-inline">
      <VueInput labelName="书名"
                class="form-control"
                ref="iptBookname"
                placeholder="请输入书名"
                v-model="msgBook"/>
      <VueInput labelName="作者"
                class="form-control"
                ref="iptAuthor"
                placeholder="请输入作者"
                v-model="msgAuthor"/>
      <VueInput labelName="出版社"
                class="form-control"
                ref="iptPublisher"
                placeholder="请输入出版社"
                v-model="msgPublisher"/>
      <VueButton @click="addBookInfo"
                 ref="btnAdd">添加
      </VueButton>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import {defineComponent, ref, inject, Ref} from '@vue/composition-api';
import VueButton from '@/components/button/VueButton.vue';
import VueInput from '@/components/input/VueInput.vue';
import {addBook} from '@/components/ajax-demo/http-request/addbook';

export default defineComponent({
  name: 'AddBookInfo',
  props: {},
  components: {
    VueButton,
    VueInput
  },
  setup(props, ctx) {
    const eventbus = inject<Vue>('eventbus');

    const msgBook = ref('');
    const msgAuthor = ref('');
    const msgPublisher = ref('');

    const addBookInfo = () => {
      msgBook.value = (ctx.refs.iptBookname as unknown as Ref<string>).value.trim();
      msgAuthor.value = (ctx.refs.iptAuthor as unknown as Ref<string>).value.trim();
      msgPublisher.value = (ctx.refs.iptPublisher as unknown as Ref<string>).value.trim();
      if (msgBook.value.length * msgAuthor.value.length * msgPublisher.value.length === 0) {return alert('请填写完整的图书信息!');}

      // 发送添加书本信息请求
      addBook({
        bookname: msgBook.value,
        author: msgAuthor.value,
        publisher: msgPublisher.value
      })
        .then(res => {
          const {data} = res;
          if (data.status === 502) {return alert(data.msg);}
          alert(data.msg);
          // 通知兄弟组件 ShowBookInfo.vue 更新渲染图书列表
          eventbus?.$emit('renderBookList');
        });

    };

    return {
      msgBook,
      msgAuthor,
      msgPublisher,
      addBookInfo
    };
  }
});
</script>

<style lang="scss" scoped>
...
</style>

实现 ShowBookInfo.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<template>
  <table class="table table-bordered table-striped">
    <thead>
    <tr>
      <th scope="col">Id</th>
      <th scope="col">书名</th>
      <th scope="col">作者</th>
      <th scope="col">出版社</th>
      <th scope="col">操作</th>
    </tr>
    </thead>
    <tbody id="tb" @click="handleClick">
    <tr v-for="{id, bookname, author, publisher} in rowsData" :key="id">
      <th scope="row">{{ id }}</th>
      <td>{{ bookname }}</td>
      <td>{{ author }}</td>
      <td>{{ publisher }}</td>
      <td>
        <a href="javascript:null;"
           class="del"
           :data-id="id">删除</a>
      </td>
    </tr>
    </tbody>
  </table>

</template>

<script lang="ts">
import {defineComponent, ref, inject} from '@vue/composition-api';
import {getBookList} from '@/components/ajax-demo/http-request/getBookList';
import {delBook} from '@/components/ajax-demo/http-request/delBook';

export default defineComponent({
  name: 'ShowBookInfo',
  setup() {
    const eventbus = inject<Vue>('eventbus');

    const rowsData = ref<string[]>(['']);
    const getBookInfo = () => {
      getBookList()
        .then(res => {
          if (res.status === 200) {
            const {data} = res.data;
            rowsData.value = data;
          }
        })
        .catch(err => {
          return `获取数据失败!${err.message}`;
        });
    };
    getBookInfo();

    // 通过代理的方式为动态添加的元素绑定点击事件 删除图书
    const handleClick = (e: MouseEvent) => {
      if (Object.keys((e?.target as HTMLElement).dataset).includes('id')) {
        const bookId = (e?.target as HTMLElement).dataset.id;
        (bookId && (+bookId) <= 3)
          ? alert('请勿删除原始数据')
          : bookId && delBook(
          {id: bookId}
        )
          .then(res => {
            if (res.status === 200) {
              // const {data} = res.data;
              getBookInfo();
              return '删除图书成功!';
            }
          })
          .catch(err => {
            return `删除图书失败!${err.message}`;
          });
      }
    };

    eventbus?.$on('renderBookList', () => {
      getBookInfo();
    });

    return {
      rowsData,
      getBookInfo,
      handleClick
    };
  }
});
</script>

<style lang="scss" scoped>
...
</style>

实现 addbook.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import axios from 'axios';

export const addBook = (paramsData: Record<string, string>) => {
  // 发送添加书本信息请求
  return axios.post('/api/addbook',
    paramsData,
    {
      baseURL: 'http://www.liulongbin.top:3006/',
      timeout: 0
    }
  );
};

实现 delBook.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import axios from 'axios';

export const delBook = (paramsData: Record<string, string>) => {
  return axios.get(
    '/api/delbook',
    {
      baseURL: 'http://www.liulongbin.top:3006',
      params: paramsData
    });
};

实现 getBookList.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import axios from 'axios';

export const getBookList = () => {
  return axios.get(
    '/api/getbooks',
    {
      baseURL: 'http://www.liulongbin.top:3006'
    });
};


0.9.2 聊天机器人接口

头铁聊天AI机器人

  • 文字http://www.liulongbin.top:3006/api/robot?spoken=
  • 语音http://www.liulongbin.top:3006/api/synthesize?text=

0.10 form表单的现代用法

  • <form></form> 表单的常用属性
  • 通过 Ajax 提交表单数据
    • 阻止表单的默认提交行为
  • 案例 评论列表
  • 快速获取表单的数据
  • 使用表单库formily

表单在网页中主要负责 数据采集 的功能,即用于采集用户输入的信息,并通过 <form>标签的提交操作,将采集的信息提交对服务端进行处理

1
2
3
4
5
6
7
8
9
<form action="">
  <label for="info">Enter info: </label>
  <input id="info" name="info" type="text"/>
  <label for="password">Enter password: </label>
  <input id="password" name="password" type="text"/>
  <input id="checkbox" name="checkbox" type="text"/>
  <label for="checkbox">记住我</label>
  <button type="submit">提交</button>
</form>
  • 表单由三个基本部分组成:
    • 表单标签 <form></form>
    • 表单域可以包含以下常用元素:
      • 文本框
      • 密码框
      • 隐藏域
      • 多行文本框
      • 复选框
      • 单选框
      • 下拉选择框
    • 表单按钮 <button type="submit"></button>

0.10.1 表单标签的属性

<form></form>标签用来采集数据,<form></form>标签的属性则是用来规定 如何将采集到的数据发送到服务器

属性 描述
action URL地址 规定当提交表单时,向何处发送表单数据
method GET(/POST) 规定以何种方式将表单数据提交到 action URL
enctype *1 默认application/x-www-form-urluncoded 规定在发送表单数据之前,如何对其进行编码的格式
target *2 默认_self 规定在浏览器何处打开 action URL
  • *1 编码格式有:
    • application/x-www-form-urluncoded
    • multipart/form-data
    • text/plain
  • *2 打开方式有:
    • _blank
    • _self
    • _parent
    • _top
    • framename
action
  • action规定当提交表单时,向何处发送表单数据
  • action属性的值应该是 后端提供的 一个 URL 地址,这个 URL 地址专门负责接受表单提交过来的数据(即表单的数据最终提交到 action 属性所指向的 URL 地址来进行处理)
  • <form></form>表单在未指定 action 属性值得情况下,action 的默认值为当前页面的 URL 地址
  • 当提交表单后,页面会立即跳转到 action 属性指定的 URL 地址,同时将表单的数据以查询参数字符传的形式放在 URL 后
target
  • <a></a>标签的属性相同,规定在浏览器何处打开 action URL
  • 可选值有5个,默认情况下,target的值为_self,表示在相同的框架或窗口中打开,一般为在当前页面中打开 action URL
描述
_blank 在新标签页打开
_self 默认值,在相同的框架或窗口中打开
_parent 在父框架集中打开(很少用)
_top 在整个窗口中打开(很少用)
framename 在指定的框架中打开(很少用)
method
  • 规定以何种方式将表单数据提交到 action URL
  • 可选值只有两个,分别是 GETPOST,不支持其他方式(请求动词)
    • 大小写不敏感
  • 默认情况下,method的值为GET,表示通过 URL 的形式,把表单数据提交到 action URL
    • 数据为以 ? 开头,数据名=数据值的形式,并以&连接不同的数据,一般称为查询字符串参数 Query String Parameters
  • 发出POST请求时,提交的数据并不会显示在地址栏 URL 中

发出请求后,查看浏览器Network面板中请求相关信息

  • 请求地址 Request URL 完整地址
  • 发出GET请求时,查询字符串参数 Query String Parameters 被单独取出,并分别显示数据
  • 发出POST请求时,Request URL只包含action URL
    • 数据是通过 Form Data的形式提交
    • 数据相对更隐蔽,但不使用 https 协议的话,仍为明文传输数据,不安全

注意

  • GET的语义为获取数据,提交的数据为查询参数,目的为查询并获取服务器上的数据,在表单中较少使用
  • POST的语义为提交数据,适合用来在表单提交数据给服务器,包括文件上传
    • 登录。注册或添加数据等表单操作,都只使用POST方式来提交表单
enctype
  • 规定在发送表单数据之前,如何对其进行 编码的格式
  • 可选值有三个,默认值为 application/x-www.form-urlencoded,表示在发送前编码所有的字符
描述
application/x-www.form-urlencoded 默认值,表示在发送前编码所有的字符,一般字符串数据
mutipart/form-data 表示不对字符编码,在使用包含 文件上传空间的表单 时,必须使用该值
text/plain 三空格转换为 + 号,但不对特殊字符编码,极少用

表单提交注意

  • 在涉及 文件上传 操作时,必须将 enctype 的值设置为 mutipart/form-data
  • 不涉及 文件上传 操作时,则将 enctype 的值设置为 application/x-www.form-urlencoded

0.10.2 表单的同步提交及缺点

  • 通过 submit 按钮,触发表单提交的操作,从而使页面跳转到 action URL的行为,叫做表单的同步提交

表单同步提交的缺点

  • <form></form>表单同步提交后,整个页面会发生跳转,跳转到 action URL 所指向的地址,用户体验相对不好
  • <form></form>表单同步提交后,回退到页面之前的状态和填写的数据会丢失

0.10.3 通过 Ajax 提交表单

可以解决表单同步提交的两个缺点

  • 页面发生跳转
  • 页面状态和数据丢失

表单 只负责采集数据Ajax负责将数据提交到服务器

  • 监听表单的提交事件submit
  • 阻止表单默认提交行为 event.preventDefault()
  • 快速获取表单中的数据
  • 使用 Ajax 异步提交数据
监听表单的提交事件 submit
  • 当表单提交的时候触发submit事件
  • 注意submit事件只能作用于form元素中,不能单独作用于button或者<input type="submit">查看MDN ->
  • submit onsubmit
阻止表单默认提交行为

即阻止默认表单提交后的跳转行为

  • 当监听到表单的提交事件后,可以在回调函数中调用事件对象ee.preventDefault() 函数,来阻止表单的提交和页面的跳转

0.10.4 获取表单中的数据

在发送请求之前,需要获取用户填写表单的数据

  • 在获取表单数据时,必须为每个表单域中需要获取数据的元素添加 name 属性,而且值不能重复
  • 封装类似于JQ中的serialize(),将表单所有输入框的信息,按照 name = 用户填写的 value ,以及连接符 &,进行拼接
  • jQuery序列化表单数据 serialize()、serializeArray()及使用

参考


1. AJAX原理和应用 §

1.1 AJAX是浏览器上的功能

  • 通过JS发请求和收响应实现前后端交互
  • 浏览器可以发请求收响应
  • 浏览器在window上加一个 XMLHttpRequest构造函数
  • XMLHttpRequest 属于 BOM 浏览器模型 * MDN
  • XMLHttpRequest对象实例可以发出HTTP请求与接收HTTP响应
  • 使用浏览器内置的XMLHttpRequestfetch API(ES6), 实现在页面不刷新的情况下和服务端进行数据交互
  • 优点:交互数据时不需要刷新整个页面,只需局部刷新,页面不发生用跳转
    • 一旦拿到服务器返回的数据,AJAX 不会刷新整个网页,而是只更新网页里面的相关部分,从而不打断用户正在做的事情,体验较好
  • 注意,AJAX 只能向 同源网址 (协议、域名、端口都相同)发出 HTTP 请求,如果发出跨域请求,就会报错
  • 支持所有 HTTP 请求方法(请求动词)
1
2
typeof window.XMLHttpRequest
// function
  • 用这个构造函数(类)可以构造出一个对象
  • JS通过它实现发请求收响应

1.2 准备服务器

  • 使用 server.js 作为服务器
  • 下载或复制代码即可用 node sever.js 8888启动
  • 使用node-dev工具 ,安装
1
2
3
4
yarn global add node-dev

# 或者用
npm install -g node-dev
  • node-dev代替node,实现自动重启服务器
  • 但不能监听页面自动刷新
1
node-dev server.js 8888
  • 添加index.html / main.js两个路由
  • 路由就是if..else
  • index.html中加载路由读取的main.js
  • 加载的文件中引用到的路径,必须和路由的if..else中的路径path一致,server.js才会加载
  • response.write(fs.readFileSync('src/xxx'))中的是相对路径,和path的不同

1.3 复习

  • server.js中添加路由(条件),访问文件
  • 添加response.statusCode = 200
  • 设置Content-Type为对应的类型response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
  • 把文件读成字符串,再传入response.write
  • 结束,关闭 response.end()
1
2
const string = fs.readFileSync('src/index.html')
response.write(string)

文件实际上对于node服务器来说就是字符串,只不过把字符串写到文件里而已

【HTTP非全解】请求和响应 & Node.Js Server


1.4 如何使用JS原生XMLHttpRequest发起 Ajax 请求

1.4.1 XMLHttpRequest的基本使用

  • XMLHttpRequest(简称xhr)时浏览器提供的JS内置对象,通过它,可以 请求服务器上的数据源
    • JQ中的.ajax函数和axios是库基于xhr对象封装
  • 和它同一等级的底层接口,功能相同的ES6 API是Fetch API

以发送GET请求为例:

  • 创建对象 new XMLHttpRequest() 一般使用命名为 xhr 的变量接收
  • 配置参数 .open('GET', url) 指定 请求方式 和 URL地址
  • 绑定事件 .onreadystatechange = () => {}
    • 异步监听响应函数,判断响应状态
    • .readyState === 4 xhr 对象的请求状态
    • .status >= 200 && xhr.status < 300) || xhr.status === 304 服务器响应状态
    • 使用服务器响应回来的数据 xhr.responseText
  • 发送请求 .send()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const xhr = new XMLHttpRequest()

// console.log('readyState', xhr.readyState) // readyState 0

// xhr.open('GET', url, true)
// xhr.open('GET', '/login?username=john&password=123', true)
xhr.open('GET', url)

// console.log('readyState', xhr.readyState) // readyState 1

/* 异步代码 */
// xhr.onload = () => {} // 老方法
// xhr.addEventListener('load', () => {}) // 老方法
// 触发`load`事件表示 readyState已经是 4 了
// xhr.error = () => {} // 浏览器控制台模拟断网
// xhr.status === 304 重定向 缓存

xhr.onreadystatechange = () => {
    // console.log('readyState', xhr.readyState)
    // readyState 2
    // readyState 3
    // readyState 4
    if(xhr.readyState === 4) {
        if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304){
            console.log('xhr.status', xhr.status) // 200
            const data = xhr.responseText
            console.log(data)
        }else{
            console.log('error')
        }
            
    }
}
/* 异步代码 */

xhr.send()
  • 区别xhr.readyState加载状态和xhr.status服务器HTTP响应状态

实例,以黑马的免费教学API为例:

1
2
3
4
5
6
7
8
const xhr = XMLHttpRequest()
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.onreadystatechange = () => {
    if(xhr.readystate === 4 && xhr.status === 200) {
        console.log(xhr.responseText)
    }
}
xhr.send()

1.4.2 xhr 对象的 readyState 属性

XMLHttpRequest 对象的 readyState 属性,用来表示 当前 Ajax 请求所处的状态

每个 Ajax 请求必然处于以下5个状态中的一个

状态 描述
0 UNSENT XMLHttpRequest对象已被创建,但尚未调用 open 方法
1 OPENED open方法已被调用
2 HEADERS_RECIEVED send()方法已被调用,响应头页已被接收
3 LOADING 数据接收中,此时response属性中已经包含部分数据
4 DONE Ajax请求完成,数据传输已经完成或者失败

参考


1.4.3 使用 xhr 发起带参数的GET请求

使用 xhr 发起带参数的GET请求时,需要在调用 xhr.open()期间,为 URL 地址指定参数即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const xhr = XMLHttpRequest()

// 查询查询字符串 ?id=1
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks?id=1')


xhr.onreadystatechange = () => {
    if(xhr.readystate === 4 && xhr.status === 200) {
        console.log(xhr.responseText)
    }
}
xhr.send()
  • 在URL地址后面拼接的参数 ?id=1,称为 查询字符串
  • 查询字符串(URL参数)是指在 URL 末尾加上 用于向服务器发送信息 的字符串(变量)
  • 格式为:
    • 将英文的 ? 放在 URL 的末尾,然后再加上 参数=值
    • 加上多个参数,使用 & 符号进行分隔
  • 一般只在 GET请求中添加使用,将所需发送给服务器的数据添加到 URL 中
1
2
3
4
5
6
// 不带参数的 URL 地址
http://www.liulongbin.top:3006/api/getbooks
// 带参数的 URL 地址
http://www.liulongbin.top:3006/api/getbooks?id=1
// 带两个参数的 URL 地址
http://www.liulongbin.top:3006/api/getbooks?id=1&bookname=西游记

1.4.4 GET请求携带参数的本质

  • 发起 GET 请求时,当 URL 需要携带参数的时候,本质上,都是直接将参数以查询字符串的形式,追加到 URL 地址的后面,发送到服务器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 为给定 ID 的 user 创建请求
// 发送 GET 请求(默认的方法)
axios('/user/12345')
  .then(...);

// 等价于
axios.get('/user?ID=12345')
  .then(...);

// 等价于
axios.get('/user', {
  params: {
     ID: 12345
  }
})
  .then(...);

打开浏览器控制面板查看GET请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const getBook = (id: number, bookname?: string) => {
  return axios.get(
    '/api/getbooks',
    {
      params : {
        id: `${id}`,
        bookname: bookname
      }
    });
};

getBook(1, '西游记');

  • URL地址编码
    • 请求 URL: http://www.liulongbin.top:3006/api/getbooks?id=1&bookname=%E8%A5%BF%E6%B8%B8%E8%AE%B0

URL编码

  • URL地址中,只允许出现英文相关的字母、标点符号、数字
  • URL地址中,不允许出现中文字符
  • 如果 URL 中需要包含中文字符,则必须对中文字符进行编码(转义)
  • URL 编码原则:使用安全的字符(没有特殊用途或者意义的可打印字符)去表示不安全的字符
  • 通俗理解:使用英文字符表示非英文字符
1
2
3
http://www.liulongbin.top:3006/api/getbooks?id=1&bookname=西游记
# 经过 URL 编码之后,URL 地址变为如下格式
http://www.liulongbin.top:3006/api/getbooks?id=1&bookname=%E8%A5%BF%E6%B8%B8%E8%AE%B0

URL编码与解码 API

浏览器提供了 URL 编码与解码的 API,分别是:

1
2
3
4
encodeURI(`西游记`) // '%E8%A5%BF%E6%B8%B8%E8%AE%B0'
decodeURI(`%E8%A5%BF%E6%B8%B8%E8%AE%B0`) // '西游记'
encodeURI(`西`) // '%E8%A5%BF'
decodeURI(`%E8%A5%BF`) // '西'

浏览器自动对 URL 地址进行编码操作,无需关心 URL 地址的编码与解码操作


1.4.5 使用 xhr 发起带参数的POST请求

步骤

  1. 创建 xhr 对象
  2. 调用 xhr.open() 函数 设置请求类型和 URL
  3. 调用 xhr.setRequestHeader() 设置 Content-Type 属性(固定写法)
  4. 监听 xhr.onreadystatechange 事件
  5. 调用 xhr.send()函数,同时要在send()函数的参数中指定要发送的数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 1. 创建 `xhr` 对象
const xhr = new XMLHttpRequest()
// 2. 调用 `xhr.open` 函数
xhr.open('POST', 'http://www.liulongbin.top:3006/api/addbook')
// 3. 设置 `Content-Type` 属性(固定写法)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
// 4. 监听 `xhr.onreadystatechange` 事件
xhr.onreadystatechange(() => {
  if(xhr.readyState === 4 && xhr.status ===200) {
    const {responseText} = xhr
    console.log(responseText)
  }
})
// 5. 调用 `xhr.send()`函数
// 同时要在`send()`函数的参数中指定要发送的数据,以查询字符串的形式,无需以 ? 开头
xhr.send('bookname=水浒传&author=施耐庵&publisher=天津出版社')

1.4.6 数据交换格式

数据交换格式就是 服务器端与客户端 之间进行数传输与交换的格式

  • 在涉及网络传输方面,XML 格式已经过时了
    • 区别于 HTML (网页内容的载体),XML 作为数据的载体用来传输和存储数据
    • XML 格式臃肿,和数据无关的代码多,传输效率低
    • XML 牺牲了体积,增加冗余大小来实现数据语义化
    • 需要额外的解析器
  • JSON 是主流的交换格式
JavaScript Object Notation
  • 中文翻译为 对象表示法
  • JSON 就是 JavaScript 对象和数组字符串表示法
  • JSON 使用文本表示一个 JS 对象和数组的信息
  • JSON 的本质可以理解为字符串数据,即用字符串来表示 JS 对象数据或数组数据
  • JSON 是一种轻量级的文本数据交换格式。
  • 用于在计算机和网络之间存储和传输数据
  • 相较于 XML 优点为 体积更小、更快、更易解析
  • 2.6 回顾复习JSON §
JSON 的两种结构

JSON是用字符串来表示 JavaScript 的对象和数组

所以,JSON中包含 对象数组 两种结构,通过这两种结构的互相嵌套,可以表示复杂的数据结构

JSON 的对象结构

对象结构在 JSON 中表现为花括号 {} 括起来的内容

  • 数据结构形式为 {key: value, key: value, ...}的键值对结构
  • 其中,key必须是使用 英文的双括号包裹 的字符串
  • value 的数据类型只可以是 字符串数字布尔值数组对象null 6种

错误范例:

1
2
3
4
5
6
7
8
9
{
  name: "zd",
  'age': 20,
  "gender": 'female',
  "address": undefined,
  "hobby": ["美食", "睡觉", '追剧'],
  // 错误的示范
  "say": function() {},
}
  • 属性名必须由双引号包裹
  • 值为字符串类型时,必须由双引号包裹而不是由单引号或反引号
    • JSON 中不允许由单引号或反引号表示字符串
  • 表示空值时,必须使用null而不是undefined
  • 所有含有字符串的值时,必须加双引号而不是单引号或反引号
  • 不允许函数作为值或属性
  • 不允许在末尾数据项后加逗号
  • JSON 中不可以写注释
  • JSON 的最外层必须是对象或数组格式
  • JSON 不允许使用undefinedfunction() {}函数作为 JSON的属性或值

正确范例:

1
2
3
4
5
6
7
8
{
  "name": "zd",
  "age": 20,
  "gender": "female",
  "address": null,
  "hobby": ["美食", "睡觉", "追剧"],
  "isMarried": false
}
JSON 的数组结构

JSON 的数组结构在 JSON 中表现为以 [] 括起来的内容

  • 数据结构为 ["js", 30, true, {}, [], null]
  • 数组中数据类型可以是 字符串数字布尔值数组对象null 6种

合法的 数组形式的 JSON 数据

1
["js", "ts", "go"]
1
[100, 200, 300.5]
1
[{"name": "zs", "age": 20}, {"name": "ls", "age": 30}]
1
[["苹果", "榴莲", "椰子"], [4, 10, 5]]

注意 JSON 中不支持 bigInt 类型的数据

JSON 和 JS 对象的关系
  • JSON 是 JS 对象的字符串表示法,他使用文本表示一个 JS 对象的信息,本质是一个字符串
1
2
3
4
// 一个对象
const obj = {a: 'Hello', b: 'World'}
// 一个 JSON 字符串,本质是一个字符串,遵循了 JSON 的语法规则
const json = '{"a": "Hello", "b": "World"}'
JSON 和 JS 对象的互转

要实现从 JSON 字符串转换为 JS 对象,使用 JSON.parse() 方法

1
2
 const obj = JSON.parse('{"a": "Hello", "b": "World"}')
 // {a: 'Hello', b: 'World'}

要实现从 JS 对象串转换为 JSON 字符,使用 JSON.stringify() 方法

1
2
 const obj = JSON.stringify({a: 'Hello', b: 'World'})
 // '{"a": "Hello", "b": "World"}'

演示 JSON.parse() 在 发送 Ajax 请求时的应用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText)
    console.log(typeof xhr.responseText)
    const result = JSON.parse(xhr.responseText)
    console.log(typeof result) // object
  }
}
xhr.send()
  • JSON.parse()转换为结构化的对象从而在 JS 代码中方便处理数据
  • JSON.stringify()转换为JSON字符串用来传输数据
JSON 的序列化和反序列化

把 数据对象 转换为 字符串 的过程,叫序列化

  • 调用 JSON.stringify(JSONObject) 方法的操作,叫 JSON 序列化

把 字符串 转换为 数据对象 的过程,叫反序列化

  • 调用 JSON.parse(JSONString) 方法的操作,叫 JSON 反序列化

1.5 封装 Ajax 函数

导入自己封装的 Ajax 函数

  • 可以实现 调用自定义 myAjax 函数,发起 Ajax 请求
  • 接受一个配置对象作为参数,配置对象中可以配置如下属性:
    • method: 请求类型
    • url: 请求地址
    • data: 请求参数对象
    • successCb: 请求成功后的回调函数
1
2
3
4
5
6
7
import myAjax from 'myAjax.js'
myAjax({
  method: '', // 请求类型
  url: '', // 请求地址
  data: {}, // 请求参数对象
  successCb: () => {}, // 请求成功后的回调函数
})

处理 data 参数

  • 用户提交数据时,以对象形式提交参数
  • 需要把 data 对象,转化成查询字符串的格式,提交给服务器
  • 因此需提前定义 resolveData 函数如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
* 处理 data 参数
* @param {Record<string, string>} data需要发送到服务器的数据
* @returns {string} 返回拼接好的查询字符串
*/
function resolveData(data) {
  const arr = []
  for(let k in data) {
    arr.push(`${k}=${data.k}`)
  }
  return arr.join('&')
}

定义 myAjax 函数

  • 需要创建 xhr 对象,并监听 onreadystatechange 事件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function myAjax(options) {
  const {data, successCb} = options
  const xhr = new XMLHttpRequest()
  // 拼接查询字符串
  const qs = resolveData(data)
  // 监听请求状态改变事件
  xhr.onreadystatechange = () => {
    if(xhr.readyState === 4 && xhr.status === 200) {
      const res_JSONObj = JSON.parse(xhr.responseText /* xhr.responseText JSONString*/)
      successCb(res_JSONObj)
    }
  }
}

判断请求类型

  • 不同的请求类型,对应 xhr 对象的不同操作。因此需要对请求类型进行条件分支的判断
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...
const reqMap = {
  'GET': () => {
    xhr.open(method, `${url}?${qs}`)
    xhr.send()
  },
  'POST': () => {
    xhr.open(method, url)
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
    xhr.send(qs)
  },
  'PUT': () => {},
  'DELETE': () => {},
  'HEAD': () => {},
  'OPTION': () => {},
}

reqMap[`${method.toUpperCase()}`]()
...

完整的 myAjax 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// node 环境 // import {XMLHttpRequest} from 'xmlhttprequest'
function resolveData(data) {
  const arr = []
  for(let k in data) {
    arr.push(`${k}=${data[k]}`) // data[k] 注意 变量名属性
  }
  return arr.join('&')
}

const reqMap = {
  'GET': () => {
    xhr.open(method, `${url}?${qs}`)
    xhr.send()
  },
  'POST': () => {
    xhr.open(method, url)
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
    xhr.send(qs)
  },
  'PUT': () => {},
  'DELETE': () => {},
  'HEAD': () => {},
  'OPTION': () => {},
}
  
export function myAjax(options) {
  const {method, url, data, successCb} = options
  const xhr = new XMLHttpRequest()
  // 拼接查询字符串
  const qs = resolveData(data)
  
  // 监听请求状态改变事件
  xhr.onreadystatechange = () => {
    if(xhr.readyState === 4 && xhr.status === 200) {
      const res_JSONObj = JSON.parse(xhr.responseText /* xhr.responseText JSONString*/)
      successCb(res_JSONObj)
    }
  }

  reqMap[`${method.toUpperCase()}`]()

}

发送请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {myAjax} from './myAjax.js'

myAjax({
  method: 'GET',
  url: 'http://www.liulongbin.top:3006/api/getbooks',
  data: {
    id: 1
  },
  successCb: (res) => {
    console.log(res)
  }
})

myAjax({
  method: 'POST',
  url: 'http://www.liulongbin.top:3006/api/addbook',
  data: {
    bookname: '红宝书',
    author: '红宝书',
    publisher: '红宝书',
  },
  successCb: (res) => {
    console.log(res)
  }
})

1.6 如何使用XMLHttpRequest Level2中提供的新特性

XMLHttpRequest Level1的缺点

  • 只支持文本数据的传送,无法用来读取和上传二进制文件。
  • 传送和接收数据时,没有进度信息,只能提示有没有完成。
  • 受到"同域限制"(Same Origin Policy),只能向同一域名的服务器请求数据

XMLHttpRequest Level2的优点

  • 可以设置 HTTP 请求的时限
  • 可以使用 FormData 对象管理表单数据
  • 可以上传文件
  • 可以请求不同域名下的数据(跨域请求)
  • 可以获取服务器端的二进制数据
  • XMLHttpRequest Level2 可以获得数据传输的进度信息,能实现 fetch API 目前无法做到的事情,例如跟踪上传进度

1.6.1 使用XMLHttpRequest Level2设置 HTTP 请求的时限

有时, Ajax 操作很费时,而且无法预知要花多少时间。如果网速慢(模拟slow 3G)用户等待时间久

  • XMLHttpRequest Level2timeout 属性实现可设置 HTTP 请求的时限
  • 超过设置的时限,自动停止 HTTP 请求
  • 监听 timeout 事件 xhr.ontimeout,用来指定时间超限时的回调函数
1
2
3
4
5
const xhr = new XMLHttpRequest()
xhr.timeout = 3000 // 类型为number 单位为毫秒
xhr.ontimeout = () => {
  console.log('请求超时')
}
  • 在 node 环境下的实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// yarn add -D xmlhttprequest-ts
import {XMLHttpRequest} from 'xmlhttprequest-ts' // XMLHttpRequest level2

// yarn add -D xhr2
// import XMLHttpRequest from 'xhr2' // XMLHttpRequest level2

const xhr = new XMLHttpRequest();

// timeout
xhr.timeout = 10
xhr.ontimeout = () => {
  console.log('请求超时了')
}

xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log("-> xhr.responseText", xhr.responseText);
  }
}

xhr.send()

1.6.2 使用XMLHttpRequest Level2使用 FormData 对象管理表单数据

Ajax 操作往往用来提交表单数据,为了方便表单处理,HTML5增加了一个 FormData 内置对象,可以模拟表单操作

  • 新建 FormData 对象实例 fd
  • 可多次调用FormData实例fd.append(key: string, value: unknown) 方法,为 FormData 添加表单项
  • 创建 xhr 实例对象
  • 调用 xhr.open(method, url) 指定请求类型与地址,method一般为POST
  • 监听 onreadystatechange 事件,指定回调函数
  • 调用 xhr.send(fd),将实例fd作为参数发送请求,直接提交
  • 无需设置 xhr.setHeader 的数据类型,内部自动设置 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 新建 FormData 对象实例 fd
const fd = new FormData()
// 可多次为 FormData 添加表单项
fd.append('uname', 'zz')
fd.append('upwd', '123456')
// 创建 xhr 实例对象
const xhr = new XMLHttpRequest()
// 指定请求类型与地址
xhr.open('POST', 'http://www.liulongbin.top:3006/api/formdata')
// 直接提交 FormData 对象实例,将实例fd作为参数发送请求
// 与提交网页表单的效果一样

xhr.onreadystatechange = () => {
  if(xhr.readyState === 4 && xhr.status >= 200 &&  xhr.status < 300) {
    console.log(JSON.parse(xhr.responseText))
  }
}
xhr.send(fd)
// 无需设置 xhr.setHeader 的数据类型

FormData 对象也可以用来获取网页表单的值

  • 获取网页中的 <form> 标签元素作为参数传入 new FormData() 实例对象中,发送请求
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 使用 Ajax 的形式提交表单

// 获取表单元素
const form = document.querySelector('#form1')
// 监听表单元素的 submit 事件
form.addEventListener('submit', (e) => {
  e.preventDefault() // 使用 Ajax 的形式提交表单避险阻止默认提交行为
  // 根据 form 表单创建 FormData 对象,会自动将表单数据填充到 FormData 对象中,无需手动 调用 .append 方法
  const fd = new FormData(form)
  // 无需设置 `xhr.setHeader` 的数据类型
  const xhr = new XMLHttpRequest()
  xhr.open('POST', 'http://www.liulongbin.top:3006/api/formdata')
  xhr.onreadystatechange = () => {}
  xhr.send(fd)
})

index.html

1
2
3
4
5
<form id="form1">
  <input type="text" name="uname" autocomplete="off"/>
  <input type="password" name="upwd" autocomplete="off"/>
  <button type="submit">提交</button>
</form>
  • 设置输入框标签的autocomplete="off",关闭自动填充

参考

JS原生实现FormData获取表单数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
<body>
  <form id="form1">
    <input type="text" name="uname" autocomplete="off" />
    <input type="password" name="upwd" />
    <button type="submit">提交</button>
  </form>

  <script>
    // 1. 通过 DOM 操作,获取到 form 表单元素
    const form = document.querySelector('#form1')

    form.addEventListener('submit', function (e) {
      // 阻止表单的默认提交行为
      e.preventDefault()

      // 创建 FormData,快速获取到 form 表单中的数据
      const fd = new FormData(form)

      const xhr = new XMLHttpRequest()
      xhr.open('POST', 'http://www.liulongbin.top:3006/api/formdata')
      xhr.send(fd)

      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log(JSON.parse(xhr.responseText))
        }
      }
    })
  </script>

</body>
...

简单封装FormData.vue 仓库地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
  <div>
    <form ref="form1">
      <VueInput type="text"
                labelName="uname"
                titleName="注册名:"
                autocomplete="off"/>
      <VueInput type="password"
                labelName="upwd"
                titleName="密码:"
                autocomplete="off"/>
      <VueButton @click="onSubmit">提交</VueButton>
      {{ data }}
    </form>
  </div>
</template>

<script lang="ts">
import {defineComponent, onMounted, ref, reactive} from '@vue/composition-api';
import VueInput from '@/components/input/VueInput.vue';
import VueButton from '@/components/button/VueButton.vue';

export default defineComponent({
  name: 'FormData',
  components: {VueButton, VueInput},
  props: {},
  setup(props, ctx) {
    const resData = ref<Record<string, unknown>>({});
    const data = ref<Record<string, unknown>>({});
    let form: HTMLFormElement = ctx.refs.form1 as HTMLFormElement;
    // 根据 form 表单创建 FormData 对象,会自动将表单数据填充到 FormData 对象中
    // 无需手动 调用 .append 方法
    let formData: FormData = reactive(new FormData(form));

    const onSubmit = (/*, e: Event*/) => {
      // e.preventDefault(); // 使用 Ajax 的形式提交表单 避险阻止默认提交行为
      formData = new FormData(form);

      // 无需设置 `xhr.setHeader` 的数据类型
      const xhr = new XMLHttpRequest();
      xhr.open('POST', 'http://www.liulongbin.top:3006/api/formdata');

      xhr.onreadystatechange = () => {
        if (xhr.readyState == 4 && xhr.status == 200) {
          resData.value = JSON.parse(xhr.responseText);
          (data.value as unknown) = resData.value.data;
        }
      };

      xhr.send(formData);

    };

    onMounted(() => {
      form = ctx.refs.form1 as HTMLFormElement;
      // 根据 form 表单创建 FormData 对象,会自动将表单数据填充到 FormData 对象中
      // 无需手动 调用 .append 方法
      formData = new FormData(form);
    });

    return {
      resData,
      onSubmit,
      formData,
      form,
      data
    };
  }
});
</script>

1.6.3 使用XMLHttpRequest Level2上传文件

  • 上传图片API
    • 接口:http://www.liulongbin.top:3006/api/upload/avatar
    • 请求类型:POST
    • 请求参数:{'avatar': fileObj}
    • 返回值

实现步骤

  1. 定义UI结构
  2. 验证是否选择了文件
  3. FormData 中追加文件
  4. 使用 xhr 发起上传文件的请求
  5. 监听 onreadystatechange 事件
  1. 定义UI结构
1
2
3
4
5
6
<!-- 1. 文件选择框 -->
<input type="file" id="file1"/>
<!-- 2. 上传按钮 -->
<button id="btnUpload">上传文件<button/>
<!-- 3. 向服务器发起请求图片,并显示-->
<img src="" alt="" ref="img" width="800">
  1. 验证是否选择了文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

// 1. 获取上传文件按钮
const btnUpload = document.querySelector('#btnUpload')

// 2. 为按钮添加点击事件监听
btnUpload.addEventListener('click',() => {
// 3. 获取选择的文件列表
  const files = document.querySelector('#file1').files
  if(files.length <= 0) {return alert('请选择要上传的文件!')}
// 上传后续的业务逻辑
})

  1. 获取上传文件按钮
  2. 为按钮添加点击事件监听
  3. 获取选择的文件列表 使用<input type="file"/>元素的API .files
  1. FormData 中追加文件
1
2
3
4
// 1. 创建 FormData 对象
const fd = new FormData()
// 2. 向 FormData 中追加文件
fd.append('avatar', files[0])
  1. 创建 FormData 对象
  2. 向 FormData 中追加文件 files[0]
  1. 使用 xhr 发起上传文件的请求
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 1. 创建 xhr 对象
const xhr = new XMLHttpRequest()
// 2. 调用 open 函数,指定请求类型与 URL 地址,上传文件的请求类型必须为 POST
xhr.open('POST', 'http://www.lilongbin.top:3006/api/upload.avtar')
// 3. 监听 `onreadystatechange` 事件
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4 && xhr.status === 200) {
    const data = JSON.parse(xhr.responseText)
    data.status === 200
      ? document.querySelector('#img').src = `http://www.lilongbin.top:3006${data.url}`
      : console.log(data.message);
  }
}
// 4. 发起请求
xhr.send(fd)
  1. 创建 xhr 对象
  2. 调用 open 函数,指定请求类型与 URL 地址,上传文件的请求类型必须为 POST
  3. 监听 onreadystatechange 事件
  4. 发起请求

封装 Upload.vue 组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
  <div>
    <VueInput type="file" ref="file1"/>
    <VueButton ref="btnUpload"
               @click="uploadFile">上传图片
    </VueButton>
    <br/>
    <img src="" alt="" ref="img" width="800">
  </div>
</template>

<script lang="ts">
import VueButton from '@/components/button/VueButton.vue';
import VueInput from '@/components/input/VueInput.vue';
import {defineComponent} from '@vue/composition-api';
import Vue, {VNode} from 'vue';

export default defineComponent({
  name: 'Upload',
  components: {VueInput, VueButton},
  props: {},
  setup(props, ctx) {
    const uploadFile = () => {
      const files = (ctx.refs.file1 as Vue).$el.querySelector('input')?.files;
      if (files && files.length <= 0) {return alert('请选择要上传的文件');}
    };

    return {
      uploadFile
    };
  }
});
</script>

1.6.4 使用XMLHttpRequest Level2获得数据传输的进度信息

XMLHttpRequest 2.0对象中,可以通过监听 xhr.upload.onprogress 事件,来获取到文件上传的进度

  • 监听 xhr.upload 的 onprogress 事件,设置回调函数
  • 事件形参 e
  • e.lengthComputable 当前上传的资源是否具有可计算的长度
  • e.loaded 文件已传输的字节
  • e.total 需传输文件的总字节
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 创建 xhr 对象
const xhr = new XMLHttpRequest()
// 监听 xhr.upload 的 onprogress 事件
xhr.upload.onprogress = (e) => {
  // e.lengthComputable 是一个布尔值,表示当前上传的资源是否具有可计算的长度
  if(e.lengthComputable) {
    // e.loaded 已传输的字节
    // e.total 需传输的总字节
    const percentComplete = Math.ceil((e.loaded / e.total) * 100)
  }
}

可以设置浏览器Network面板中slow 3G 查看上传过程

1
2
3
-> percentComplete 39
-> percentComplete 78
-> percentComplete 100

动态设置进度条

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<template>
  <div>
    <VueInput type="file" ref="file1"/>
    <VueButton ref="btnUpload"
               @click="uploadFile">上传图片
    </VueButton>
    <div style="">
      <label for="file">文件上传进度:
        <progress id="file" max="100" :value="`${percentComplete}`"
                  style="vertical-align: text-bottom;width: 30%;margin: 0 10px;"></progress>
        <span>{{ progressTips }}</span>
      </label>
    </div>
    <br/>
    <img src="" alt="" ref="img" width="800">
  </div>
</template>

<script lang="ts">
import VueButton from '@/components/button/VueButton.vue';
import VueInput from '@/components/input/VueInput.vue';
import {computed, defineComponent, inject, ref} from '@vue/composition-api';
import Vue from 'vue';

export default defineComponent({
  name: 'Upload',
  components: {VueInput, VueButton},
  props: {},
  setup: function (props, ctx) {
    // 动态进度条数值
    const percentComplete = ref<number>(0);
    // 进度条提示文字
    const progressTips = computed(() => {
      return percentComplete.value === 100
        ? '上传完成'
        : `${percentComplete.value}%`;
    });

    const toast = inject<Function>('toast');
    const uploadFile = () => {
      // 获取用户选择的文件列表
      const fileList = (ctx.refs.file1 as Vue).$el.querySelector('input')?.files;
      if (fileList && fileList.length <= 0) {return toast && toast('请选择要上传的文件!');}
      const fd = new FormData();
      // 将用户选择的文件添加到 FormData 中
      fileList && fd.append('avatar', fileList[0]);

      // 创建 xhr 对象
      const xhr = new XMLHttpRequest();

      // 监听文件上传的进度
      xhr.upload.onprogress = (e) => {
        if (e.lengthComputable) {
          // 计算出上传的进度 动态设置进度条
          percentComplete.value = Math.ceil((e.loaded / e.total) * 100);
        }
      };

      // 调用 open 函数,指定请求类型与 URL 地址,上传文件的请求类型必须为 POST
      xhr.open('POST', 'http://www.liulongbin.top:3006/api/upload/avatar');

      // 监听 `onreadystatechange` 事件
      xhr.onreadystatechange = () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
          const data = JSON.parse(xhr.responseText);
          data.status === 200
            ? (ctx.refs.img as HTMLImageElement).src = `http://www.liulongbin.top:3006${data.url}`
            : toast && toast(data.message);
        }
      };

      // 发起请求
      xhr.send(fd);
    };

    return {
      uploadFile,
      percentComplete,
      progressTips
    };
  }
});
</script>

1.6.5 使用XMLHttpRequest Level2请求不同域名下的数据(跨域请求)

设置严格检查<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">


参考


1.7 复习使用axios发起 Ajax 请求

1.8 实现文件上传与loading效果


2 AJAX应用

2.1 AJAX加载CSS §

以前使用<link rel="stylesheet" href="/style.css">引入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover">
    <link rel="stylesheet" href="/style.css">
    <title>AJAX demo</title>
</head>
<body>
  <h1>AJAX demo</h1>
</body>
</html>
  • 这里--<link rel="stylesheet" href="/style.css">中的/style.css"是路由path的请求路径

server.js

1
2
3
4
5
6
7
8
9
// ...
if (path === '/index.html') {
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    const string = fs.readFileSync('src/index.html')
    response.write(string)
    response.end()
  }
// ...

style.css

1
2
3
4
body {
  color: red;
  background-color: #eaeaea;
}

使用 AJAX 加载 CSS 的四个步骤

server.js里写路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ...
if (path === '/index.html') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/html;charset=utf-8')
const string = fs.readFileSync('src/index.html')
response.write(string)
response.end()
} else if (path === '/main.js') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
response.write(fs.readFileSync('src/main.js'))
response.end()
} else if (path === '/style.css') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/css;charset=utf-8')
response.write(fs.readFileSync('src/style.css'))
response.end()
}
// ...
  • 区别路由path的请求路径和fs.readFileSync('src/xxx')的查找文件目录路径

index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover">
  <!-- <link rel="stylesheet" href="/style.css"> -->
  <title>AJAX demo 2</title>
</head>

<body>
  <h1>AJAX demo CSS</h1>
  <p>
    <button id="getCSS">请求CSS</button>
  </p>
  <script src="/main.js"> </script>
</body>
</html>
  • index.html里写的所有路径(/style.css/main.js),必须和路由的path路径一致,才能生效
  • 创建一个 button 测试

main.js 里写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
getCSS.onclick = () => {
  const request = new XMLHttpRequest()
  request.open('GET', '/style.css');
  request.onload = () => {
    // console.log('成功了')
    console.log('request.response')
    console.log(request.response)
    // 之后将响应的内容,用对应的标签包裹,插入html页面相应位置
  }
  request.onerror = () => {
    console.log('失败了')
  }
  request.send()
}
  1. 创建 HttpRequest 对象 (全称是 XMLHttpRequest
  2. 调用对象的 open 方法
  3. 监听对象的 onloadonerror 事件
  4. 调用对象的 send 方法(发送请求)
  5. 获取资源用request.open('GET', '/xxx');,只用两个参数,mdn 中后面的参数不用XMLHttpRequest.open() MDN

request.response就是响应得到的文件内容

  • 要使这段内容生效,就在外包裹一层对应的标签
1
2
3
4
// 将响应的内容,用对应的标签包裹,插入html页面相应位置
const style = document.createElement('style') // 创建 style 标签
style.innerHTML = request.response // 填写 style 内容
document.head.appendChild(style) // 插入head标签里

触发onerror事件

1
2
3
request.onerror = () => {
  console.log('失败了')
}
  • server.js路径写错:你输入的路径不存在对应的内容
1
2
3
4
5
6
7
8
//...
else {
    response.statusCode = 404
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    response.write(`你输入的路径不存在对应的内容`)
    response.end()
}
//...
  • 只会渲染路径错误时的路由,并不触发onerror 事件
  • onerror对AJAX的支持不好

在第3步中,AJAX一般改用 onreadystatechange 事件,不用onloadonerror

  • 用对象的status属性,即状态码来判断,响应是否成功
  • 注意onreadystatechange全小写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
getCSS.onclick = () => {
  const request = new XMLHttpRequest()
  request.open('GET', '/style.css');
  request.onreadystatechange = () => {
    // 监听对象的 readyState属性
    // console.log(request.readyState) // 2 3 4
    if (request.readyState === 4) {
      // console.log('下载完成')
      // 下载完成,但不知道下载成功还是失败

      if (request.status >= 200 && request.status < 300) {
        // 将响应的内容,用对应的标签包裹,插入html页面相应位置
        const style = document.createElement('style') // 创建 style 标签
        style.innerHTML = request.response // 填写 style 内容
        document.head.appendChild(style) // 插入head标签里
      } else {
        console.log('加载CSS失败')
      }
    }
  }
  request.send()
}

2.2 AJAX加载JS §

以前使用<script src="main.js"></script>引入

使用 AJAX 加载 JS 的四个步骤

server.js

1
2
3
4
5
6
7
8
// ...
else if (path === '/getJS.js') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
response.write(fs.readFileSync('src/getJS.js')) // 在目录中创建一个测试的getJS.js
response.end()
}
//...

index.html

1
2
3
4
<p>
<button id="getCSS">请求CSS</button>
<button id="getJS">请求JS</button>
</p>
  1. 创建 HttpRequest 对象 (全称是 XMLHttpRequest
  2. 调用 open 方法
  3. 监听对象的 onreadystatechange 事件
  4. 调用对象的 send 方法(发送请求)

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//...
// AJAX 加载 JS
getJS.onclick = () => {
    const request = new XMLHttpRequest
    request.open('GET', '/getJS.js')
    request.onreadystatechange = () => {
        if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
            // 创建script标签
            const script = document.createElement('script')
            // 填写 script内容
            script.innerHTML = request.response
            // 插到body里
            document.body.appendChild(script)
            }
        }
  request.send()
}
//...
  • 在事件处理函数里操作 JS 文件内容
  • 将响应的内容,用对应的标签包裹,插入html页面相应位置
  • AJAX下载并执行响应的内容

2.3 AJAX加载HTML §

以前没有加载过 HTML

  • 在目录src/下创建测试用indexOld.html,不写全,只写个标签
1
2
<h2>AJAX demo 2</h2>
<div style="background-color: bisque;border: 1px solid orange;width:300px;height:300px;">动态内容</div>

配置路由server.js

1
2
3
4
5
6
7
8
//...
else if (path === '/indexOld.html') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.write(fs.readFileSync('src/indexOld.html'))
response.end()
}
//...

index.html 加测试按钮

1
2
3
4
5
<p>
    <button id="getCSS">请求CSS</button>
    <button id="getJS">请求JS</button>
    <button id="getHTML">请求HTML</button>
</p>

使用 AJAX 加载 HTML 的四个步骤

  1. 创建 HttpRequest 对象
  2. 调用对象的 open 方法
  3. 监听对象的 onreadystatechange 事件
  4. 调用对象的 send 方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//...
// AJAX 加载 HTML
getHTML.onclick = () => {
    const request = new XMLHttpRequest
    request.open('GET', '/indexOld.html')
    request.onreadyStatechange = () => {
        if (request.readyState === 4 && request.status >= 200 && request < 300) {
            // 创建 div 标签
            const div = document.createElement('div')
            // 填写 div 内容
            div.innerHTML = request.response
            // 插到body里
            document.body.appendChild(div)
        }
     }
  request.send()
}
//...
  • 在事件处理函数里操作 HTML 文件内容
  • 动态展示index.html
  • 控制台 Network 查看请求 点Preview 预览请求的页面
  • Response 看源代码
  • 把页面拆成不同的部分,在用户点击时,请求这个部分
  • 可以请求一小块 HTML,而 iframe 是请求整个网页,更加臃肿
  • AJAX 可以做到轻量级的请求,任意 HTTP 支持的文件类型

2.4 AJAX加载XML §

使用 AJAX 加载 XML 的四个步骤

server.js

1
2
3
4
5
6
7
8
//...
else if (path === '/getXML.xml') {
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/xml;charset=utf-8')
    response.write(fs.readFileSync('src/getXML.xml'))
    response.end()
  }
//...

index.html

1
2
3
4
5
6
<p>
    <button id="getCSS">请求CSS</button>
    <button id="getJS">请求JS</button>
    <button id="getHTML">请求HTML</button>
    <button id="getXML">请求XML</button>
</p>

getXML.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<message>
    <warning>
         Hello World
    </warning>
</message>
  1. 创建 HttpRequest 对象(全称是 XMLHttpRequest
  2. 调用对象的 open 方法
  3. 监听对象的 onreadystatechange 事件
  4. 调用对象的 send 方法(发送请求)

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// AJAX 加载 XML
getXML.onclick = () => {
  const request = new XMLHttpRequest()
  request.open('GET', '/getXML.xml')
  request.onreadystatechange = () => {
    if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
      // console.log(request.responseXML)
      const dom = request.responseXML
      const text = dom.getElementsByTagName('warning')[0].textContent.trim();
      console.log(text)
      console.log(request.responseXML)
    }
  }
  request.send()
}
  • 在事件处理函数里操作 responseXML
  • request.responseXML是 DOM 对象

2.5 AJAX应用小结 §

HTTP 可以请求的类型

  • 可以请求 HTML CSS JS
  • 设置正确的 Content-Type
  • 响应的内容,用对应的标签包裹,插入html页面相应位置
  • 知道怎么解析,就可以使用

解析方法

  • 得到 CSS 后生成 style
  • 得到 JS 后生成 script
  • 得到 HTML 使用 innerHTMLDOM API
  • 得到 XML 后使用 responseXMLDOM API
  • 不同类型的数据有不同的解析办法

2.6 回顾复习JSON §

JavaScript Object Notation

  • JSON 是 轻量级的数据交换格式语言
  • 和HTML CSS JS一样是门独立的语言
  • 和HTML XML Markdown 是 标记语言,用来展示数据
  • 中文官网
  • 一页纸
  • 铁轨图

支持的类型

  • string 只支持双引号,不支持单引号、反引号和无引号
  • number 支持科学技术法
  • bool truefalse
  • null 没有 undefined
  • object
  • array

区别于 JS 的类型

  • 不支持函数与变量(即不支持引用)

和JS语法的区别:

  • JSON 的字符串必须用双引号。
  • JSON 无法表示 undefined,只能表示 “undefined”
  • JSON 无法表示函数
  • JSON 的对象语法不能有引用

2.7 AJAX加载JSON §

server.js

1
2
3
4
5
6
else if (path === '/getJSON.json') {
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/json;charset=utf-8') // 'application/json;charset=utf-8'也可
    response.write(fs.readFileSync('src/getJSON.json'))
    response.end()
}

getJSON.json

1
2
3
4
5
6
{
  "name": "frank",
  "gender": "male",
  "age": 18,
  "xxx": null
}

index.html

1
2
3
4
5
6
7
8
<p>
    <button id="getCSS">请求CSS</button>
    <button id="getJS">请求JS</button>
    <button id="getHTML">请求HTML</button>
    <button id="getXML">请求XML</button>
    <button id="getJSON">请求JSON</button>
</p>
<h2>Hello <span id="myName"></span></h2>

使用 AJAX 加载 JSON 的四个步骤

  1. 创建 HttpRequest 对象(全称是 XMLHttpRequest
  2. 调用对象的 open 方法
  3. 监听对象的 onreadystatechange 事件
  4. 调用对象的 send 方法(发送请求)

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// AJAX 加载 JSON
getJSON.onclick = () => {
  const request = new XMLHttpRequest()
  request.open('GET', '/getJSON.json')
  request.onreadystatechange = () => {
    if (request.readyState === 4 && request.status === 200) {
      console.log(request.response)
      const object = JSON.parse(request.response)
      console.log(object)
      myName.textContent = object.name
    }
  }
  request.send()
}
  • 在事件函数里使用 JSON.parse
  • JSON.parse 把符合 JSON 语法的字符串变成对应的对象或者是其他东西

2.8 JS内置对象 window.JSON §

JSON.parse('string')

  • 解析字符串
  • 将符合 JSON 语法的字符串转换成 JS 对应类型的数据
  • 返回值 Object 类型, 对应给定 JSON 文本的对象/值
  • JSON字符串 -> JS 数据
  • 转换成的不一定是对象,可以是 JSON 值类型的其他类型( typeof )
  • 由于 JSON 只有六种类型,所以转成的数据也只有6种
  • 如果不符合 JSON 语法,则直接抛出一个 Error对象
  • 一般用 try...catch捕获错误
  • 异常 若传入的字符串不符合 JSON 规范,则会抛出 SyntaxError 异常
1
2
3
4
5
6
7
8
9
let object
try{
    object = JSON.parse(`{'name':'frank'}`)
}catch(error){
    console.log('Error!')
    console.log(error)
    object = {"name":"no name"}
}
console.log(object)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const json = '{"result":true, "count":42}'
const obj = JSON.parse(json)

console.log(obj.count)
// expected output: 42

console.log(obj.result)
// expected output: true

JSON.parse('{}');              // {}
JSON.parse('true');            // true
JSON.parse('"foo"');           // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse('null');            // null

JSON.parse() 不允许用逗号作为结尾
// both will throw a SyntaxError
JSON.parse("[1, 2, 3, 4, ]");
JSON.parse('{"foo" : 1, }');

JSON.stringify(value) 序列化

  • JSON.parse()的逆运算
  • JS 数据 -> JSON字符串
  • 即序列化成 一个 JSON 字符串的值
  • 将值转换为相应的JSON格式
  • 由于 JS 的数据类型比 JSON 多,所以不一定成功
  • 如果失败,则直接抛出一个 Error对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const obj = {"hi": "Ho", "fn": () => {}}
JSON.stringify(obj) // 不报错 直接忽略不合法的类型

JSON.stringify({});                        // "{}"
JSON.stringify(true);                      // "true"
JSON.stringify("foo");                     // ""foo""
JSON.stringify([1, "false", false]);       // "[1,"false",false]"
JSON.stringify({ x: 5 });                  // "{"x":5}"

JSON.stringify({x: 5, y: 6});              
// "{"x":5,"y":6}"

JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); 
// '[1,"false",false]'

JSON.stringify({x: undefined, y: Object, z: Symbol("")}); 
// '{}'

JSON.stringify([undefined, Object, Symbol("")]);          
// '[null,null,null]' 

JSON.stringify({[Symbol("foo")]: "foo"});                 
// '{}'

JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
// '{}'

JSON.stringify(
    {[Symbol.for("foo")]: "foo"}, 
    function (k, v) {
        if (typeof k === "symbol"){
            return "a symbol";
        }
    }
);


// undefined 

// 不可枚举的属性默认会被忽略:
JSON.stringify( 
    Object.create(
        null, 
        { 
            x: { value: 'x', enumerable: false }, 
            y: { value: 'y', enumerable: true } 
        }
    )
);

// "{"y":"y"}"

2.9 AJAX综合应用:加载分页 §

2.9.1 加载列表

  • 用户打开页面,开到第一页数据
  • 用户点击下一页,看到第二页数据
  • 用户点击下一页,看到第三页数据
  • 用户点击下一页,提示没有更多了

/db/page1.json page2.json page3.json

  • 前后端不分离的渲染

index.html

1
2
3
4
5
6
7
8
<body>
<!-- ... -->
  <h2>Hello <span id="myName"></span></h2>
  <div>
    {{page1}}
  </div>
<!-- ... -->
</body>

server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if (path === '/index.html') {
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    let string = fs.readFileSync('src/index.html').toString() // 调用toString()前 是buffer

    // AJAX分页
    const page1 = fs.readFileSync('db/page1.json')
    // li
    const array = JSON.parse(page1) // [{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }, { "id": 5 }, { "id": 6 }, { "id": 7 }, { "id": 8 }, { "id": 9 }, { "id": 10 }]
    const result = array.map(item => {
      return `<li>${item.id}</li>`
    }).join('')
    string = string.replace('{{page1}}', `<ul>${result}</ul>`)
    response.write(string)
    response.end()
}
  • 从数据库中拿出,拼接字符串

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let pageCount = 1
// AJAX 加载 分页
getPage.onclick = () => {
  const request = new XMLHttpRequest()
  request.open('GET', `/page${pageCount + 1}`)
  request.onreadystatechange = () => {
    if (request.readyState === 4 && request.status === 200) {
      const array = JSON.parse(request.response)
      array.forEach(item => {
        const li = document.createElement('li')
        li.textContent = item.id
        xxx.appendChild(li)
      });

      // console.log(request.response)
      const object = JSON.parse(request.response)
      // console.log(object)
      myName.textContent = object.name

      pageCount += 1
    }
  }
  request.send()
}

2.9.2 优化点

  • 在点击第三页的时候就禁用下一页按钮

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//...
let page = [1, 2, 3]
let currentPage = page[0]
// AJAX 加载 分页
getPage.onclick = () => {
  if (currentPage < 3) {
    const request = new XMLHttpRequest()
    request.open('GET', `/page${currentPage + 1}`)
    request.onreadystatechange = () => {
      if (request.readyState === 4 && request.status === 200) {
        const array = JSON.parse(request.response)
        array.forEach(item => {
          const li = document.createElement('li')
          li.textContent = item.id
          xxx.appendChild(li)
        });
        // console.log(request.response)
        const object = JSON.parse(request.response)
        // console.log(object)
        myName.textContent = object.name
        currentPage += 1
      }
    }
    request.send()
  } else {
    // console.log("Ban")
    getPage.disabled = true
  }
}
//...

2.9.3 请求天气数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// mock 接口 GET 方法 查询参数直接跟url在后面
let url = 'http://rap2api.taobao.org/mock/244238/weather?city=北京'
let xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = () => {
    // xhr.DONE === 4
    if(xhr.readyState === 4){
        if( (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304){
        
        // 将xhr.responseText的JSON格式转换为对象
            console.log(JSON.parse(xhr.responseText))
            
        // xhr.responseType = 'json' // 兼容性注意
            console.log(xhr.response)
        } else {
            console.log('服务器异常')
        }
    }
}

xhr.responseType = 'json' // 兼容性注意
xhr.onerror = () => {
    console.log('服务器异常')
}
xhr.send()

2.9.4 发送POST请求

应用场景:涉及到登录、注册、用户填写信息(调查问卷)需提交,而不是查询的场合时,要用POST请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
let url = 'http://rap2api.taobao.org/app/mock/24238/login'
let xhr = new XMLHttpRequest()

// 设置xhr请求超时时间
xhr.timeout = 3000
xhr.open('POST', url)

// 设置请求头 告诉服务器请求参数的编码方式
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') // post 方法 用xhr.send()接受发送的数据,区别于get方法中写在url里

// 封装 url
function makeUrl(obj) {
    let arr = []
    for(let key in obj){
        arr.push(`${key} = ${obj[key]}`)
    }
    return arr.join('&')
}
const testUser = makeUrl({
    username: 'John',
    password: '123456'
})

// xhr.send('username=John&password=123456')
// 将参数变成 `key1=value1&key2=value2` 的形式,传给xhr.send()
xhr.send(testUser)

// 如果要上传文件(图片)
// xhr.setRequestHeader('Content-type', 'multipart/form-data') 
//

xhr.onload = () => {
    if( (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 ){
        console.log(JSON.parse(xhr.responseText))
    } else {
        console.log('响应异常')
    }
}
xhr.ontimeout = () => {
    console.log('请求超时')
}
xhr.onerror = () => {
    console.log('服务器异常')
}

2.9.3 注册登录发送POST请求

默认的Content-type,是multipart/form-data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let formData = new FormData()
// 添加参数
formData.append('username', 'jzm')
formData.append('password', '132456')

let url = 'http://rap2api.taobao.org/app/mock/244238/register'

let xhr =  new XMLHttpRequest()

xhr.open('POST', url, true)
xhr.onload = () => {
    if(xhr.status ===200 || xhr.status === 304) {
        console.log(JSON.parse(xhr.responseText))
        console.log('登陆成功')
    } else {
        console.log('接口异常')
    }
}
xhr.send(formData)

2.9.4 POST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<body>
  <form>
    <input type="text" name="username" placeholder="用户名">
    <input type="password" name="password" placeholder="密码">
    <input type="file" name="file">
    <input type="submit">
    
    <p id="msg"></p>
  </form>
</body>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const $ = string => {return document.querySelector(string)}
const $submit = $('[type = submit]')
const $form = $('#form')
const $msg = $('#msg')

let url = 'http://rap2api.taobao.org.app/mock/244238/register'
$form.onsubmit = (e) => {
    e.preventDefault()
    
    // 校验开始
    let formData = new FormData($form)

    let xhr = new XMLHttpRequest()
    xhr.open('POST', url, true)
    xhr.onload = () => {
        if(xhr.status === 200  || xhr.status === 304) {
          let data = JSON.parse(xhr.responseText)
          $msg.innerText = data.msg || data.errMsg 
        } else {
          $msg.innerText = '接口异常'
        }
      }
    xhr.send(formData)
}

3. 手写AJAX §

使用原生 JS 写出一个 AJAX 请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
myButton.onclick = () => {
    let request = new XMLHttpRequest()
    request.open('GET','/xxx')
    request.onreadystatechange = () => {
        if(request.readystate === 4 && request.status === 200) {
            const string = request.responseText
            const stringObj = window.JSON.parse(string)
        }
    }
    request.send()
}

后台的 server.js ,处理请求

1
2
3
4
5
6
if(path === '/xxx') {
    response.statusCode = 200
    response.setHeader('Content-Type','text/json;charset=utf-8')
    response.write(`{"status":"ok"}`)
    response.end()
}

4. ajax 的简陋封装 §

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const request = (url, cityParams, onsuccess, onerror) => {
    url = url + '?' + Object.entries(cityParams).map(arr => arr[0] + '=' + arr[1]).join('&')
    let xhr = new XMLHttpRequest()
    xhr.open('GET', url, true)
    xhr.onload = () => {
        if(xhr.status === 200 || xhr.status ===304){
            onsuccess(JSON.parse(xhr.responseText))
        } else {
            onerror()
        }
    }
    xhr.onerror = onerror
    xhr.send()
}

request('http://rap2api.taobao.org/app/mock/244238/weather',{city:'内蒙古'}, data => {
    console.log('请求成功')
    console.log(data)
}, () => {
    console.log('接口异常')
})

5. fetch代替JS原生AJAX §

AJAX的API设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。

  • Fetch API 是一种ES6规范,用来取代XMLHttpRequest对象
  • 两个特点,一是接口合理化,Ajax 将所有不同性质的接口都放在 XHR 对象上,而 Fetch 将它们分散在几个不同的对象上,设计更合理;
  • 二是 Fetch 操作返回Promise对象,避免了嵌套的回调函数。
1
2
3
4
5
6
let url = 'http://api.jirengu.com/getWeather.php?city=北京'

fetch(url).then(response => {return response.json()})
    .then(data => {
        document.body.innerText = data.results[0].weather_data[0].weather
    })

POST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let url = 'http://api.jirengu.com/getWeather.php?city=北京'
let data = {username: 'jirengu', password: '13456'}

fetch(url,{
    method: 'POST',
    body: Object.entries(data).map(arr => { return arr[0] + '=' + arr[1]}).join('&'),
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
}).then(res => {return res.json()})
    .then(response => console.log('Success:', response))
    .catch(error => {console.log('Error:', error)})

使用githubpagesmock数据 §

搭建服务器

同步开发,约定格式,模拟数据,前后端联调


使用easymockmock数据 §

1
# curl http://....

轮询与长轮询Comet聊天室

双工通信:每隔固定时间发一次请求

客户端

  • 发请求,等待响应(pending)
  • 当响应时,再次发一个请求

服务器端

  • 请求到来,如果没新数据,则不发响应
  • 当有新数据,需要通知客户端,再响应



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名