创建代码仓库
我们开始初始化项目,首先我们先去 GitHub 上创建一个 repo,填好 repo 名称,以及写一下 README,对项目先做个简单的描述。
通常我们初始化一个项目,需要配置一大堆东西,比如 package.json、.editorconfig、.gitignore 等;还包括一些构建工具如 rollup、webpack 以及它们的配置。
当我们使用 TypeScript 去写一个项目的时候,还需要配置 TypeScript 的编译配置文件 tsconfig.json 以及
tslint.json 文件。
这些茫茫多的配置往往会让人望而却步,如果有一个脚手架工具帮我们生成好这些初始化文件该多好。好在确实有这样的工具,接下来我们的主角 TypeScript library starter 隆重登场。
TypeScript library starter
它是一个开源的 TypeScript 开发基础库的脚手架工具,可以帮助我们快速初始化一个 TypeScript 项目,我们可以去它的官网地址学习和使用它。
使用方式
代码语言:javascript复制1git clone https://github.com/alexjoverm/typescript-library-starter.git ts-axios
2cd ts-axios
3
4npm install
先通过 git clone 把项目代码拉下来到我们的 ts-axios 目录,然后运行 npm install 安装依赖,并且给项目命名,我们仍然使用 ts-axios。
安装好依赖后,我们先来预览一下这个项目的目录结构。
目录文件介绍
TypeScript library starter 生成的目录结构如下:
代码语言:javascript复制 1├── CONTRIBUTING.md
2├── LICENSE
3├── README.md
4├── code-of-conduct.md
5├── node_modules
6├── package-lock.json
7├── package.json
8├── rollup.config.ts // rollup 配置文件
9├── src // 源码目录
10├── test // 测试目录
11├── tools // 发布到 GitHup pages 以及 发布到 npm 的一些配置脚本工具
12├── tsconfig.json // TypeScript 编译配置文件
13└── tslint.json // TypeScript lint 文件
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 工具生成文档。
关联远程分支
代码已经初始化好,接下来我们要把当前代码仓库关联我们的远程仓库,首先在命令行中运行命令查看远程分支:
1git remote -v
这里我们不会得到任何输出,因为我们还没有关联远程分支,我们先去 GitHub 上找到我们仓库的地址,在命令行运行:
1git remote add origin 仓库地址
关联后,远程库的名字就是 origin,这是 Git 默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。
接着你就可以继续运行 git remote -v 查看关联结果了。
拉取代码
运行如下命令从远程仓库拉取 master 分支代码并合并:
1git pull origin master
这个时候会报错:
代码语言:javascript复制1error: The following untracked working tree files would be overwritten by merge:
2 README.md
3Please move or remove them before you merge.
4Aborting
因为我们在使用 typescript library starter 初始化代码的时候也创建了 README.md,和远程仓库的 README.md 冲突了。我们把 README.md 文件删除,再次运行:
1git pull origin master
这次代码就拉取成功了,并且在本地也创建了一个 master 分支。
提交代码
最后我们来提交代码,首先运行:
1git add .
把提交的代码从工作区添加到暂存区,然后运行 npm run commit 这个 npm 脚本来提交代码,运行后它会依次询问你几个问题,比如你这次修改的范围包括哪些、提交的描述、是否有 break change、影响了哪些 issue 等等。
填写完毕,工具会帮我们运行 git commit 并且自动把我们提交的信息合成一条提交注释。接着运行命令把代码推送到远程 git 仓库中:
1git push origin master
接着我们去 GitHub 仓库中就可以看到刚才这条提交记录了。
至此,我们项目已经初始化完毕,接下来我们就开始编写源码实现 axios 了。
编写基本请求代码
我们这节课开始编写 ts-axios 库,我们的目标是实现简单的发送请求功能,即客户端通过 XMLHttpRequest 对象把请求发送到 server 端,server 端能收到请求并响应即可。
我们实现 axios 最基本的操作,通过传入一个对象发送请求,如下:
代码语言:javascript复制1axios({
2 method: 'get',
3 url: '/simple/get',
4 params: {
5 a: 1,
6 b: 2
7 }
8})
创建入口文件
我们删除 src 目录下的文件,先创建一个 index.ts 文件,作为整个库的入口文件,然后我们先定义一个 axios 方法,并把它导出,如下:
代码语言:javascript复制1function axios(config) {
2
3}
4
5export 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。
定义 AxiosRequestConfig 接口类型
接下来,我们需要给 config 参数定义一种接口类型。我们创建一个 types 目录,在下面创建一个 index.ts 文件,作为我们项目中公用的类型定义文件。
接下来我们来定义 AxiosRequestConfig 接口类型:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 url: string
3 method?: string
4 data?: any
5 params?: any
6}
其中,url 为请求的地址,必选属性;而其余属性都是可选属性。method 是请求的 HTTP 方法;data 是 post、patch 等类型请求的数据,放到 request body 中的;params 是 get、head 等类型请求的数据,拼接到 url 的 query string 中的。
为了让 method 只能传入合法的字符串,我们定义一种字符串字面量类型 Method:
代码语言:javascript复制1export type Method = 'get' | 'GET'
2 | 'delete' | 'Delete'
3 | 'head' | 'HEAD'
4 | 'options' | 'OPTIONS'
5 | 'post' | 'POST'
6 | 'put' | 'PUT'
7 | 'patch' | 'PATCH'
8
接着我们把 AxiosRequestConfig 中的 method 属性类型改成这种字符串字面量类型:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 url: string
3 method?: Method
4 data?: any
5 params?: any
6}
然后回到 index.ts,我们引入 AxiosRequestConfig 类型,作为 config 的参数类型,如下:
代码语言:javascript复制1import { AxiosRequestConfig } from './types'
2
3function axios(config: AxiosRequestConfig) {
4}
5
6export default axios
那么接下来,我们就来实现这个函数体内部的逻辑——发送请求。
利用 XMLHttpRequest 发送请求
我们并不想在 index.ts 中去实现发送请求的逻辑,我们利用模块化的编程思想,把这个功能拆分到一个单独的模块中。
于是我们在 src 目录下创建一个 xhr.ts 文件,我们导出一个 xhr 方法,它接受一个 config 参数,类型也是 AxiosRequestConfig 类型。
代码语言:javascript复制1import { AxiosRequestConfig } from './types'
2
3export default function xhr(config: AxiosRequestConfig) {
4}
接下来,我们来实现这个函数体逻辑,如下:
代码语言:javascript复制1export default function xhr(config: AxiosRequestConfig): void {
2 const { data = null, url, method = 'get' } = config
3
4 const request = new XMLHttpRequest()
5
6 request.open(method.toUpperCase(), url, true)
7
8 request.send(data)
9}
我们首先通过解构赋值的语法从 config 中拿到对应的属性值赋值给我的变量,并且还定义了一些默认值,因为在 AxiosRequestConfig 接口的定义中,有些属性是可选的。
接着我们实例化了一个 XMLHttpRequest 对象,然后调用了它的 open 方法,传入了对应的一些参数,最后调用 send 方法发送请求。
对于 XMLHttpRequest 的学习,我希望同学们去 mdn 上系统地学习一下它的一些属性和方法,当做参考资料,因为在后续的开发中我们可能会反复查阅这些文档资料。
引入 xhr 模块
编写好了 xhr 模块,我们就需要在 index.ts 中去引入这个模块,如下:
代码语言:javascript复制1import { AxiosRequestConfig } from './types'
2import xhr from './xhr'
3
4function axios(config: AxiosRequestConfig): void {
5 xhr(config)
6}
7
8export default axios
那么至此,我们基本的发送请求代码就编写完毕了,接下来我们来写一个小 demo,来使用我们编写的 axios 库去发送请求。
demo 编写
我们会利用 Node.js 的 express 库去运行我们的 demo,利用 webpack 来作为 demo 的构建工具。
依赖安装
我们先来安装一些编写 demo 需要的依赖包,如下:
代码语言:javascript复制1"webpack": "^4.28.4",
2"webpack-dev-middleware": "^3.5.0",
3"webpack-hot-middleware": "^2.24.3",
4"ts-loader": "^5.3.3",
5"tslint-loader": "^3.5.4",
6"express": "^4.16.4",
7"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:
代码语言:javascript复制const fs = require('fs')
2const path = require('path')
3const webpack = require('webpack')
4
5module.exports = {
6 mode: 'development',
7
8 /**
9 * 我们会在 examples 目录下建多个子目录
10 * 我们会把不同章节的 demo 放到不同的子目录中
11 * 每个子目录的下会创建一个 app.ts
12 * app.ts 作为 webpack 构建的入口文件
13 * entries 收集了多目录个入口文件,并且每个入口还引入了一个用于热更新的文件
14 * entries 是一个对象,key 为目录名
15 */
16 entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
17 const fullDir = path.join(__dirname, dir)
18 const entry = path.join(fullDir, 'app.ts')
19 if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
20 entries[dir] = ['webpack-hot-middleware/client', entry]
21 }
22
23 return entries
24 }, {}),
25
26 /**
27 * 根据不同的目录名称,打包生成目标 js,名称和目录名一致
28 */
29 output: {
30 path: path.join(__dirname, '__build__'),
31 filename: '[name].js',
32 publicPath: '/__build__/'
33 },
34
35 module: {
36 rules: [
37 {
38 test: /.ts$/,
39 enforce: 'pre',
40 use: [
41 {
42 loader: 'tslint-loader'
43 }
44 ]
45 },
46 {
47 test: /.tsx?$/,
48 use: [
49 {
50 loader: 'ts-loader',
51 options: {
52 transpileOnly: true
53 }
54 }
55 ]
56 }
57 ]
58 },
59
60 resolve: {
61 extensions: ['.ts', '.tsx', '.js']
62 },
63
64 plugins: [
65 new webpack.HotModuleReplacementPlugin(),
66 new webpack.NoEmitOnErrorsPlugin()
67 ]
68}
编写 server 文件
在 examples 目录下创建 server.js 文件:
代码语言:javascript复制 1const express = require('express')
2const bodyParser = require('body-parser')
3const webpack = require('webpack')
4const webpackDevMiddleware = require('webpack-dev-middleware')
5const webpackHotMiddleware = require('webpack-hot-middleware')
6const WebpackConfig = require('./webpack.config')
7
8const app = express()
9const compiler = webpack(WebpackConfig)
10
11app.use(webpackDevMiddleware(compiler, {
12 publicPath: '/__build__/',
13 stats: {
14 colors: true,
15 chunks: false
16 }
17}))
18
19app.use(webpackHotMiddleware(compiler))
20
21app.use(express.static(__dirname))
22
23app.use(bodyParser.json())
24app.use(bodyParser.urlencoded({ extended: true }))
25
26const port = process.env.PORT || 8080
27module.exports = app.listen(port, () => {
28 console.log(`Server listening on http://localhost:${port}, Ctrl C to stop`)
29})
编写 demo 代码
首先在 examples 目录下创建 index.html 和 global.css,作为所有 demo 的入口文件已全局样式文件。
index.html:
代码语言:javascript复制 1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <title>ts-axios examples</title>
6 <link rel="stylesheet" href="/global.css">
7 </head>
8 <body style="padding: 0 20px">
9 <h1>ts-axios examples</h1>
10 <ul>
11 <li><a href="simple">Simple</a></li>
12 </ul>
13 </body>
14</html>
global.css:
代码语言:javascript复制 1html, body {
2 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
3 color: #2c3e50;
4}
5
6ul {
7 line-height: 1.5em;
8 padding-left: 1.5em;
9}
10
11a {
12 color: #7f8c8d;
13 text-decoration: none;
14}
15
16a:hover {
17 color: #4fc08d;
18}
然后在 examples 目录下创建 simple 目录,作为本章节的 demo 目录,在该目录下再创建 index.html 和 app.ts 文件
index.html 文件如下:
代码语言:javascript复制 1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <title>Simple example</title>
6 </head>
7 <body>
8 <script src="/__build__/simple.js"></script>
9 </body>
10</html>
app.ts 文件如下:
代码语言:javascript复制 1import axios from '../../src/index'
2
3axios({
4 method: 'get',
5 url: '/simple/get',
6 params: {
7 a: 1,
8 b: 2
9 }
10})
因为我们这里通过 axios 发送了请求,那么我们的 server 端要实现对应的路由接口,我们来修改 server.js,添加如下代码:
代码语言:javascript复制1const router = express.Router()
2
3router.get('/simple/get', function(req, res) {
4 res.json({
5 msg: `hello world`
6 })
7})
8
9app.use(router)
运行 demo
接着我们在 package.json 中去新增一个 npm script:
1"dev": "node examples/server.js"
然后我们去控制台执行命令
1npm run dev
相当于执行了 node examples/server.js,会开启我们的 server。
接着我们打开 chrome 浏览器,访问 http://localhost:8080/ 即可访问我们的 demo 了,我们点到 Simple 目录下,通过开发者工具的 network 部分我们可以看到成功发送到了一条请求,并在 response 中看到了服务端返回的数据。
至此,我们就实现了一个简单的请求发送,并编写了相关的 demo。但是现在存在一些问题:我们传入的 params 数据并没有用,也没有拼接到 url 上;我们对 request body 的数据格式、请求头 headers 也没有做处理;另外我们虽然从网络层面收到了响应的数据,但是我们代码层面也并没有对响应的数据做处理。那么下面一章,我们就来解决这些问题。