携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
前言
Bug在手,天下我有(bushi)
⭐hello大家好呀,最近在系统学习TS,作为程序员只有理论知识的积累是不够的,苦于TS一堆枯燥乏味的知识点,想找个项目练练手锻炼一下实践能力?马上安排!
熟练使用axios在前端开发过程中已经是必备技能了,但是很多开发者对于axios的原理还不是很理解,接下来我将分几篇文章来带你用TypeScript从零构建一个axios,既能巩固自己的TS基本功,又能从源码层面剖析axios的实现,再也不用害怕面试被问axios的源码啦~
该项目TS知识点会涉及如下👇
axios库的功能架构如下👇
🆕你会收货什么?
- 学会TS开发实际项目
- 学会造轮子,并学会写单元测试
- 学会使用先进的前端工具辅助开发
- 完全掌握axios的实现原理
需求分析
重构之前,我们需要简单地做一些需求分析,看一下我们这次重构需要支持哪些 feature。
- 在浏览器端使用 XMLHttpRequest 对象通讯
- 支持 Promise API
- 支持请求和响应的拦截器
- 支持请求数据和响应数据的转换
- 支持请求的取消
- JSON 数据的自动转换
- 客户端防止 CSRF
此外,我们还会支持一些 axios 库支持的一些其它的 feature。这里要注意的,我们这次重构不包括 axios 在 Node 中的实现,因为这部分我们在平时项目中应用的很少,还涉及到很多 Node.js 的知识,如果都讲的话,一是比较占用时间,另一个可能会喧宾夺主了。
那么接下来我们就开始初始化项目吧!
初始化项目
创建代码仓库
接下来,我们开始初始化项目,首先我们先去 GitHub 上创建一个仓库,填好仓库名称,以及写一下 README,对项目先做个简单的描述。
通常我们初始化一个项目,需要配置一大堆东西,比如 package.json
、.editorconfig
、.gitignore
等;还包括一些构建工具如 rollup
、webpack
以及它们的配置。
当我们使用 TypeScript 去写一个项目的时候,还需要配置 TypeScript 的编译配置文件 tsconfig.json
以及 tslint.json
文件。
这些茫茫多的配置往往会让一个想从零开始写项目的同学望而却步,如果有一个脚手架工具帮我们生成好这些初始化文件该多好。好在确实有这样的工具,接下来我们的主角 TypeScript library starter
隆重登场。
TypeScript library starter
它是一个开源的 TypeScript 开发基础库的脚手架工具,可以帮助我们快速初始化一个 TypeScript 项目,我们可以去它的官网地址学习和使用它。
使用方式
git clone https://github.com/alexjoverm/typescript-library-starter.git ts-axios
cd ts-axios
npm install
先通过 git clone
把项目代码拉下来到我们的 ts-axios
目录,然后运行 npm install
安装依赖,并且给项目命名,我们仍然使用 ts-axios
。
安装好依赖后,我们先来预览一下这个项目的目录结构。
目录文件介绍
TypeScript library starter
生成的目录结构如下:
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── code-of-conduct.md
├── node_modules
├── package-lock.json
├── package.json
├── rollup.config.ts // rollup 配置文件
├── src // 源码目录
├── test // 测试目录
├── tools // 发布到 GitHub pages 以及 发布到 npm 的一些配置脚本工具
├── tsconfig.json // TypeScript 编译配置文件
└── tslint.json // TypeScript lint 文件
优秀工具集成
使用 TypeScript library starter
创建的项目集成了很多优秀的开源工具:
- 使用 RollupJS 帮助我们打包。
- 使用 Prettier 和 TSLint 帮助我们格式化代码以及保证代码风格一致性。
- 使用 TypeDoc 帮助我们自动生成文档并部署到 GitHub pages。
- 使用 Jest帮助我们做单元测试。
- 使用 Commitizen帮助我们生成规范化的提交注释。
- 使用 Semantic release帮助我们管理版本和发布。
- 使用 husky帮助我们更简单地使用 git hooks。
- 使用 Conventional changelog帮助我们通过代码提交信息自动生成 change log。
这里我们列举了很多工具,感兴趣的同学们可以点开他们的链接对这些工具做进一步学习。
NPM Scripts
TypeScript library starter
同样在 package.json
中帮我们配置了一些 npm scripts
,接下来我们先列举一下我们开发中常用的 npm scripts
,剩余的我们在之后学习中遇到的时候再来介绍。
npm run lint
: 使用 TSLint 工具检查src
和test
目录下 TypeScript 代码的可读性、可维护性和功能性错误。npm start
: 观察者模式运行rollup
工具打包代码。npm test
: 运行jest
工具跑单元测试。npm run commit
: 运行commitizen
工具提交格式化的git commit
注释。npm run build
: 运行rollup
编译打包 TypeScript 代码,并运行typedoc
工具生成文档。
然后就是关联本地仓库。至此,我们项目已经初始化完毕,接下来我们就开始编写源码实现 axios 了。
编写基础请求代码
我们这节开始编写 ts-axios
库,我们的目标是实现简单的发送请求功能,即客户端通过 XMLHttpRequest
对象把请求发送到 server 端,server 端能收到请求并响应即可。
我们实现 axios
最基本的操作,通过传入一个对象发送请求,如下:
axios({
method: 'get',
url: '/simple/get',
params: {
a: 1,
b: 2
}
})
创建入口文件
我们删除 src
目录下的文件,先创建一个 index.ts
文件,作为整个库的入口文件,然后我们先定义一个 axios
方法,并把它导出,如下:
function axios(config) {
}
export default axios
这里 TypeScript 编译器会检查到错误,分别是 config
的声明上有隐含的 any
报错,以及代码块为空。代码块为空我们比较好理解,第一个错误的原因是因为我们给 TypeScript 编译配置的 strict
设置为 true
导致。
编译配置文件tsconfig.json
tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项,关于它的具体学习,可以去官网系统学习一下。
运行 tsc
命令去编译 TypeScript 文件,编译器会从当前目录开始去查找 tsconfig.json
文件,作为编译时的一些编译选项。
我们来看一下 tsconfig.json 文件,它包含了很多编译时的配置,其中我们把 strict
设置为 true
,它相当于启用所有严格类型的检查选项。启用 --strict
相当于启用 --noImplicitAny
,--noImplicitThis
,--alwaysStrict
,--strictNullChecks
和 --strictFunctionTypes
和 --strictPropertyInitialization
。
定义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 也没有做处理;另外我们虽然从网络层面收到了响应的数据,但是我们代码层面也并没有对响应的数据做处理。那么下面一章,我们就来解决这些问题。