Skip to main content

[TS] ts-axios(1) - 项目构建


Features​

  • 在浏览器端使用XMLHttpRequest对象通讯
  • 支持Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消
  • JSON数据的自动转换
  • 客户端防止XSRF

初始化项目​

创建代码仓库​

使用typescript-library-starter​

  • Usage

    git clone https://github.com/alexjoverm/typescript-library-starter.git YOURFOLDERNAME
    cd YOURFOLDERNAME

    npm install

    创建好后的代码关联远程仓库​

    git remote add origin ssh地址
    git push 代码

编写基础请求代码​

Features​

目标实现简单的发送请求功能, 即客户端通过XMLHttpRequest 对象把请求发送到server端, server端能接收到请求并响应即可.
实现axios最基本的操作, 通过传入一个对象发送请求, 如下:

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

创建入口文件​

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


function axios(config) {

}

export default axios

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

编译配置文件 tsconfig.json​

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

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

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

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

定义 AxiosRequestConfig 接口类型​

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

接下来我们来定义 AxiosRequestConfig 接口类型:

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

其中,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'

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

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

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

import { AxiosRequestConfig } from './types'

function axios(config: AxiosRequestConfig) {
}

export default axios

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

利用 XMLHttpRequest 发送请求​

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

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

import { AxiosRequestConfig } from './types'

export default function xhr(config: AxiosRequestConfig) {
}

接下来,我们来实现这个函数体逻辑,如下:

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)
}

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

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

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

引入 xhr 模块​

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

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

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

export default axios

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

demo 编写​

我们会利用 Node.js 的 express 库去运行我们的 demo,利用 webpack 来作为 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"

其中,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()
]
}

编写 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`)
})

编写 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>

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;
}

然后在 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>

app.ts 文件如下:

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

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

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

const router = express.Router()

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

app.use(router)

运行 demo​

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

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

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

npm run dev

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

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

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