### 彪哥

爱好前端

- 初识 TypeScript

- TypeScript 常用语法

- ts-axios 项目初始化

  - 需求分析
  - 初始化项目
  - 编写基础请求代码
    - 创建入口文件
      - 编译配置文件 tsconfig.json
      - 定义 AxiosRequestConfig 接口类型
    - 利用 XMLHttpRequest 发送请求
      - 引入 xhr 模块
    - demo 编写
      - 依赖安装
      - 编写 webpack 配置文件
      - 编写 server 文件
      - 编写 demo 代码
      - 运行 demo

- ts-axios 基础功能实现

- ts-axios 异常情况处理

- ts-axios 接口扩展

- ts-axios 拦截器实现

- ts-axios 配置化实现

- ts-axios 取消功能实现

- ts-axios 更多功能实现

- ts-axios 单元测试

- ts-axios 部署与发布

- 《TypeScript 从零实现 axios》
- ts-axios 项目初始化

HuangYi
2020-01-05
目录

# 编写基础请求代码

# # 编写基础请求代码

我们这节课开始编写 `ts-axios` 库，我们的目标是实现简单的发送请求功能，即客户端通过 `XMLHttpRequest` 对象把请求发送到 server 端，server 端能收到请求并响应即可。

我们实现 `axios` 最基本的操作，通过传入一个对象发送请求，如下：

    axios({
      method: 'get',
      url: '/simple/get',
      params: {
        a: 1,
        b: 2
      }
    })

1
2
3
4
5
6
7
8

## # 创建入口文件

我们删除 `src` 目录下的文件，先创建一个 `index.ts` 文件，作为整个库的入口文件，然后我们先定义一个 `axios` 方法，并把它导出，如下：

    function axios(config) {

    }

    export default axios

1
2
3
4
5
6
7

这里 TypeScript 编译器会检查到错误，分别是 `config` 的声明上有隐含的 `any` 报错，以及代码块为空。代码块为空我们比较好理解，第一个错误的原因是因为我们给 TypeScript 编译配置的 `strict` 设置为 `true` 导致。

### # 编译配置文件 tsconfig.json

`tsconfig.json` 文件中指定了用来编译这个项目的根文件和编译选项，关于它的具体学习，我希望同学们去官网
(opens new window)系统学习一下

我们在之前讲 TypeScript 的基础时，会运行 `tsc` 命令去编译 TypeScript 文件，编译器会从当前目录开始去查找 `tsconfig.json` 文件，作为编译时的一些编译选项。

我们来看一下 tsconfig.json 文件，它包含了很多编译时的配置，其中我们把 `strict` 设置为 `true`，它相当于启用所有严格类型的检查选项。启用 `--strict` 相当于启用 `--noImplicitAny`,`--noImplicitThis`,`--alwaysStrict`，`--strictNullChecks` 和 `--strictFunctionTypes` 和 `--strictPropertyInitialization`。

我们讲 TypeScript 的基础时提到了 `--strictNullChecks`，其它的编译配置我建议同学们都去查看它的官网文档
(opens new window)，把它当做手册去查阅即可。

### # 定义 AxiosRequestConfig 接口类型

接下来，我们需要给 `config` 参数定义一种接口类型。我们创建一个 `types` 目录，在下面创建一个 `index.ts` 文件，作为我们项目中公用的类型定义文件。

接下来我们来定义 `AxiosRequestConfig` 接口类型：

    export interface AxiosRequestConfig {
      url: string
      method?: string
      data?: any
      params?: any
    }

1
2
3
4
5
6

其中，`url` 为请求的地址，必选属性；而其余属性都是可选属性。`method` 是请求的 HTTP 方法；`data` 是 `post`、`patch` 等类型请求的数据，放到 `request body` 中的；`params` 是 `get`、`head` 等类型请求的数据，拼接到 `url` 的 `query string` 中的。

为了让 `method` 只能传入合法的字符串，我们定义一种字符串字面量类型 `Method`：

    export type Method = 'get' | 'GET'
      | 'delete' | 'Delete'
      | 'head' | 'HEAD'
      | 'options' | 'OPTIONS'
      | 'post' | 'POST'
      | 'put' | 'PUT'
      | 'patch' | 'PATCH'

1
2
3
4
5
6
7

接着我们把 `AxiosRequestConfig` 中的 `method` 属性类型改成这种字符串字面量类型：

    export interface AxiosRequestConfig {
      url: string
      method?: Method
      data?: any
      params?: any
    }

1
2
3
4
5
6

然后回到 `index.ts`，我们引入 `AxiosRequestConfig` 类型，作为 `config` 的参数类型，如下：

    import { AxiosRequestConfig } from './types'

    function axios(config: AxiosRequestConfig) {
    }

    export default axios

1
2
3
4
5
6

那么接下来，我们就来实现这个函数体内部的逻辑——发送请求。

## # 利用 XMLHttpRequest 发送请求

我们并不想在 `index.ts` 中去实现发送请求的逻辑，我们利用模块化的编程思想，把这个功能拆分到一个单独的模块中。

于是我们在 `src` 目录下创建一个 `xhr.ts` 文件，我们导出一个 `xhr` 方法，它接受一个 `config` 参数，类型也是 `AxiosRequestConfig` 类型。

    import { AxiosRequestConfig } from './types'

    export default function xhr(config: AxiosRequestConfig) {
    }

1
2
3
4

接下来，我们来实现这个函数体逻辑，如下：

    export default function xhr(config: AxiosRequestConfig): void {
      const { data = null, url, method = 'get' } = config

      const request = new XMLHttpRequest()

      request.open(method.toUpperCase(), url, true)

      request.send(data)
    }

1
2
3
4
5
6
7
8
9

我们首先通过解构赋值的语法从 `config` 中拿到对应的属性值赋值给我的变量，并且还定义了一些默认值，因为在 `AxiosRequestConfig` 接口的定义中，有些属性是可选的。

接着我们实例化了一个 `XMLHttpRequest` 对象，然后调用了它的 `open` 方法，传入了对应的一些参数，最后调用 `send` 方法发送请求。

对于 `XMLHttpRequest` 的学习，我希望同学们去 mdn
(opens new window) 上系统地学习一下它的一些属性和方法，当做参考资料，因为在后续的开发中我们可能会反复查阅这些文档资料。

### # 引入 xhr 模块

编写好了 `xhr` 模块，我们就需要在 `index.ts` 中去引入这个模块，如下：

    import { AxiosRequestConfig } from './types'
    import xhr from './xhr'

    function axios(config: AxiosRequestConfig): void {
      xhr(config)
    }

    export default axios

1
2
3
4
5
6
7
8

那么至此，我们基本的发送请求代码就编写完毕了，接下来我们来写一个小 demo，来使用我们编写的 axios 库去发送请求。

## # demo 编写

我们会利用 Node.js 的 `express`
(opens new window) 库去运行我们的 demo，利用 `webpack`
(opens new window) 来作为 demo 的构建工具。

### # 依赖安装

我们先来安装一些编写 demo 需要的依赖包，如下：

    "webpack": "^4.28.4",
    "webpack-dev-middleware": "^3.5.0",
    "webpack-hot-middleware": "^2.24.3",
    "ts-loader": "^5.3.3",
    "tslint-loader": "^3.5.4",
    "express": "^4.16.4",
    "body-parser": "^1.18.3"

1
2
3
4
5
6
7

其中，`webpack` 是打包构建工具，`webpack-dev-middleware` 和 `webpack-hot-middleware` 是 2 个 `express` 的 `webpack` 中间件，`ts-loader` 和 `tslint-loader` 是 `webpack` 需要的 TypeScript 相关 loader，`express` 是 Node.js 的服务端框架，`body-parser` 是 `express` 的一个中间件，解析 `body` 数据用的。

### # 编写 webpack 配置文件

在 `examples` 目录下创建 `webpack` 配置文件 `webpack.config.js`：

    const fs = require('fs')
    const path = require('path')
    const webpack = require('webpack')

    module.exports = {
      mode: 'development',

      /**
       * 我们会在 examples 目录下建多个子目录
       * 我们会把不同章节的 demo 放到不同的子目录中
       * 每个子目录的下会创建一个 app.ts
       * app.ts 作为 webpack 构建的入口文件
       * entries 收集了多目录个入口文件，并且每个入口还引入了一个用于热更新的文件
       * entries 是一个对象，key 为目录名
       */
      entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
        const fullDir = path.join(__dirname, dir)
        const entry = path.join(fullDir, 'app.ts')
        if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
          entries[dir] = ['webpack-hot-middleware/client', entry]
        }

        return entries
      }, {}),

      /**
       * 根据不同的目录名称，打包生成目标 js，名称和目录名一致
       */
      output: {
        path: path.join(__dirname, '__build__'),
        filename: '[name].js',
        publicPath: '/__build__/'
      },

      module: {
        rules: [
          {
            test: /\.ts$/,
            enforce: 'pre',
            use: [
              {
                loader: 'tslint-loader'
              }
            ]
          },
          {
            test: /\.tsx?$/,
            use: [
              {
                loader: 'ts-loader',
                options: {
                  transpileOnly: true
                }
              }
            ]
          }
        ]
      },

      resolve: {
        extensions: ['.ts', '.tsx', '.js']
      },

      plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
      ]
    }

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

### # 编写 server 文件

在 `examples` 目录下创建 `server.js` 文件：

    const express = require('express')
    const bodyParser = require('body-parser')
    const webpack = require('webpack')
    const webpackDevMiddleware = require('webpack-dev-middleware')
    const webpackHotMiddleware = require('webpack-hot-middleware')
    const WebpackConfig = require('./webpack.config')

    const app = express()
    const compiler = webpack(WebpackConfig)

    app.use(webpackDevMiddleware(compiler, {
      publicPath: '/__build__/',
      stats: {
        colors: true,
        chunks: false
      }
    }))

    app.use(webpackHotMiddleware(compiler))

    app.use(express.static(__dirname))

    app.use(bodyParser.json())
    app.use(bodyParser.urlencoded({ extended: true }))

    const port = process.env.PORT || 8080
    module.exports = app.listen(port, () => {
      console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)
    })

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

### # 编写 demo 代码

首先在 `examples` 目录下创建 `index.html` 和 `global.css`，作为所有 `demo` 的入口文件已全局样式文件。

`index.html`：

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>ts-axios examples</title>
        <link rel="stylesheet" href="/global.css">
      </head>
      <body style="padding: 0 20px">
        <h1>ts-axios examples</h1>
        <ul>
          <li><a href="simple">Simple</a></li>
        </ul>
      </body>
    </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14

`global.css`：

    html, body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
      color: #2c3e50;
    }

    ul {
      line-height: 1.5em;
      padding-left: 1.5em;
    }

    a {
      color: #7f8c8d;
      text-decoration: none;
    }

    a:hover {
      color: #4fc08d;
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

然后在 `examples` 目录下创建 `simple` 目录，作为本章节的 demo 目录，在该目录下再创建 `index.html` 和 `app.ts` 文件

`index.html` 文件如下:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>Simple example</title>
      </head>
      <body>
        <script src="/__build__/simple.js"></script>
      </body>
    </html>

1
2
3
4
5
6
7
8
9
10

`app.ts` 文件如下：

    import axios from '../../src/index'

    axios({
      method: 'get',
      url: '/simple/get',
      params: {
        a: 1,
        b: 2
      }
    })

1
2
3
4
5
6
7
8
9
10

因为我们这里通过 `axios` 发送了请求，那么我们的 server 端要实现对应的路由接口，我们来修改 `server.js`，添加如下代码：

    const router = express.Router()

    router.get('/simple/get', function(req, res) {
      res.json({
        msg: `hello world`
      })
    })

    app.use(router)

1
2
3
4
5
6
7
8
9

### # 运行 demo

接着我们在 `package.json` 中去新增一个 `npm script`：

    "dev": "node examples/server.js"

1

然后我们去控制台执行命令

    npm run dev

1

相当于执行了 `node examples/server.js`，会开启我们的 server。

接着我们打开 chrome 浏览器，访问 `http://localhost:8080/` 即可访问我们的 demo 了，我们点到 `Simple` 目录下，通过开发者工具的 network 部分我们可以看到成功发送到了一条请求，并在 response 中看到了服务端返回的数据。

至此，我们就实现了一个简单的请求发送，并编写了相关的 demo。但是现在存在一些问题：我们传入的 `params` 数据并没有用，也没有拼接到 `url` 上；我们对 request body 的数据格式、请求头 headers 也没有做处理；另外我们虽然从网络层面收到了响应的数据，但是我们代码层面也并没有对响应的数据做处理。那么下面一章，我们就来解决这些问题。

编辑
(opens new window)
#TypeScript
上次更新: 2022/12/14, 19:36:42
初始化项目
处理请求 url 参数

← 初始化项目 处理请求 url 参数→

最近更新

  01
Git修改分支名
08-11

  02
CSS给table的tbody添加滚动条
06-29

  03
我做了一个手写春联小网页，祝大家虎年暴富 原创
01-28

更多文章>

Copyright © 2016-2022 Hesb | MIT License

- 跟随系统
- 浅色模式
- 深色模式
- 阅读模式