第九章 RESTful 接口交互

  • Writing asynchronous code
  • Using fetch
  • Using axios with class components
  • Using axios with function components

Writing asynchronous code

默认下TypeScript的代码是同步执行的。既是一行一行代码执行。TypeScript也可以实现异步功能。调用REST API就是一个异步实现的例子。

Callbacks

回调指的是将函数作为参数,传入到一个异步函数中调用,当该异步函数完成时执行该传参函数的内容。

Callback execution

下面以一个例子阐述在TypeScript环境下的异步回调实现:

1
2
3
4
5
6
let firstName: string;
setTimeout(() => {
firstName = "Fred";
console.log("firstName in callback", firstName);
},1000);
console.log("firstName aftr setTimeout", firstName);

代码中调用了JavaScript的异步函数setTimeout。第一个参数是一个回调函数,第二个参数是执行等待的时间。

这里的回调函数,形式上使用() =>{} 表述,回调函数会将firstName变量更改为Fred,并输出到控制台。

执行代码,可以看到回调函数并没有执行,而是等待1000毫秒后再触发控制台打印信息。

异步函数的执行不会等待函数内部的完成。这种方式一方面不便于阅读,另一方面容易造成回调地狱(callback hell)。因为开发者容易在回调中内嵌更复杂的回调或异步函数实现。那么我们如何处理这种异步回调的错误?

Handling callback erros

本小节将探索如何处理异步代码的错误信息。

  1. 首先有如下代码:
1
2
3
4
5
6
7
try {
setTimeout(() => {
throw new Error("Someting went wrong");
}, 1000);
} catch (ex) {
console.log("An error has occurred", ex);
}

这次使用了try / catch方式来处理异步出现的错误信息。

  1. 错误信息必须被处理。更改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IResult {
success: boolean;
error?: any;
}
let result: IResult = { success: true };
setTimeout(() => {
try {
throw new Error("Something went wrong");
} catch (ex) {
result.success = false;
result.error = ex;
}
}, 1000);
console.log(result);

这里将try / catch放置在回调函数的内部。以及使用了变量result来表述执行的成功或失败。

至此,回调引发的错误已经被处理了。幸运的是,有更好的方式来处理这种挑战。

Promises

promise是JavaScript里面的对象。它表述了一个异步操作的最终结果(成功或失败).

Consuming a promised-based function

下面是一个promised-based的API

1
2
3
4
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data))
.catch(json => console.log("error", json));
  • 这里的函数fetch是javascript本地函数,用于处理RESTful API。
  • 入参是一个URL
  • then方法处理返回
  • catch方法处理错误信息

相比来说代码更易于阅读和理解。我们不需要在then方法中处理错误信息。

Creating a promised based function

本小节会创建一个wait函数来处理异步等待信息。

  1. 首先实现一个简单的回调:
1
2
3
4
5
6
7
8
9
10
const wait = (ms: number) => {
return new Promise((resolve, reject) => {
if (ms > 1000) {
reject("Too long");
}
setTimeout(() => {
resolve("Successfully waited");
}, ms);
});
};
  • 该函数返回一个Promise对象,该对象包含有一个需要异步执行的构造器入参。
  • promise构造入参resolve是一个函数,表示当函数执行完成后要处理的动作。
  • promise构造入参reject是一个函数,表示出现错误后需要处理的动作。
  • 函数体内部则使用了setTimeout以及一个回调处理等待动作。
  1. 消费这个promised-based函数:
1
2
3
wait(500)
.then(result => console.log("then >", result))
.then(error => console.log("catch >", error));

等待500毫秒后,函数将输出正确或失败信息。

  1. 将等待时间延长,大于1000,catch方法被调用。
1
2
3
wait(1500)
.then(result => console.log("then >", result))
.then(error => console.log("catch >", error));

Promise对于异步代码有一个很好的处理机制。致辞,还有另一种异步处理的实现方式。

async and awit

asyncawait是JavaScript的关键字。

  1. 首先从wait函数的例子开始:
1
2
3
4
5
6
7
8
9
const someWork = async () => {
try {
const result = await wait(500);
console.log(result);
} catch (ex) {
console.log(ex);
}
};
someWork();
  • 这里创建了一个箭头函数someWork,使用关键字async标注为异步。
  • 使用关键字awaitwait前面声明。wait下一行的执行将被暂停(halt)直到这个异步操作完成。
  • try / catch将捕获任何异常信息。

该方法有点像是一个异步操作的管理者。执行代码后,控制台打印:

1
then > Successfully waited
  1. 将等待时间改为1500毫秒:
1
const result = await wait(1500);

控制台打印错误信息:

1
Too long

因此,使用asyncawait使得代码更易于阅读。另一个奖励是,这种实现在旧的浏览器中仍然支持。

到目前为止,我们已经对如何编写更好的异步代码有更好的理解,下面会就RESTful API的实现进行练习。

Using fetch

fetch函数是一个JavaScript本地函数。本小节会对一些常见的RESTful API通过fetch进行交互。

Geting data with fetch

首先开始从GET请求开始。

Baisc GET request

打开 TypeScript playground,输入如下代码:

1
2
3
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data))

其中:

  • fetch函数的第一个参数是一个URL请求地址
  • fetch是一个promised-base函数
  • 第一个then方法处理返回
  • 第二个then方法处理当返回body是JSON

Getting response status

通常,我们需要处理返回的status code:

1
2
3
4
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => {
console.log(response.status, respons.ok);
})
  • 返回的status给出了HTTP的状态信息
  • ok返回一个boolean值表示200的状态码

另一个404的不存在的示例如下:

1
2
3
4
fetch("https://jsonplaceholder.typicode.com/posts/1001")
.then(response => {
console.log(response.status, response.ok);
})

Handling errors

通过catch方法处理错误信息:

1
2
3
4
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data))
.catch(json => console.log("error", json));

然而,catch并没有捕获非200状态的机制。所以对于非200的错误返回请求时,可以在第一个then方法中处理。

那么catch方法是干嘛的?它是用来捕获网络异常的,非200返回并不是一种网络异常。

Creating data with fetch

本小节将使用fetch来创建一些数据。

Basic POST request

通常情况下,调用post请求来创建数据。

1
2
3
4
5
6
7
8
9
10
11
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
body: JSON.stringify({
title: "Interesting post",
body: "This si an interesting post abount ...",
userId: 1
})
}).then(response => {
console.log(response.status);
return response.json();
}).then(data => console.log(data));

fetch函数的第二个参数是一个可选对象。包含请求的method和body信息。

Request HTTP headers

通常,请求信息需要包含标头(header)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "bearer some-bearer-token"
},
body: JSON.stringify({
title: "Interesting post",
body: "This is an interesting post about ...",
userId: 1
})
}).then(response => {
console.log(response.status);
return response.json();
}).then(data => console.log(data));

对于GET请求,可以用如下形式:

1
2
3
4
5
6
fetch("https://jsonplaceholder.typicode.com/posts/1", {
headers: {
"Content-Type": "application/json",
Authorization: "bearer some-bearer-token"
}
}).then(...);

Changing data with fetch

Basic PUT request

通常情况下,对于数据的更改是用PUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Corrected post",
body: "This is corrected post about ...",
userId: 1
})
}).then(response => {
console.log(response.status);
return response.json();
}).then(data => console.log(data));

Basic PATCH request

某些情况下,PATCH的请求用于部分请求的更改。

1
2
3
4
5
6
7
8
9
10
11
12
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "PATCH",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
title: "Corrected post"
})
}).then(response => {
console.log(response.status);
return response.json();
}).then(data => console.log(data));

Deleting data with fetch

通常情况下,RESTful接口都是用DELETE方法删除数据。

1
2
3
4
5
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "DELETE"
}).then(response => {
console.log(response.status);
});

到目前为止,我们已经学习了如何使用fetch函数来操作RESTful API。

Using axios with class components

axios是一个流行的开源JavaScript HTTP客户端。我们会创建一个React App包含有create,read,upate,delete等操作。并探索axios相比fetch有哪些优势。

Installing axios

首先新建一个应用:

  1. 在控制台通过命令新建一个TypeScript的React项目:
1
npx create-react-app crud-api --typescript

注意我们使用的React版本至少是16.7.0-alpha.0。我们可以在package.json里面检查。如果低于16.7.0-alpha.0,可以使用下面命令安装:

1
2
npm install react@16.7.0-alpha.0
npm install react@16.7.0-alpaha.0
  1. 项目创建后,添加TSLint到项目中,并带有某些规则:
1
2
cd crud-api
npm install tslint tslint-react tslint-config-prettier --save-dev
  1. 新建tslint.json包含下面规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"rules": {
"ordered-imports": false,
"object-literal-sort-keys": false,
"jsx-no-lambda": false,
"no-debugger": false,
"no-console": false,
},
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"converage/lcov-report/*.js"
]
}
}
  1. 打开App.tsx文件,会有一个linting错误。在render()方法中添加public关键字处理该问题:
1
2
3
4
5
class App extends Component {
public render() {
return ( ... );
}
}
  1. 添加axios
1
npm install axios

注意axios内已经包含TypeScript类型了,不需要额外安装。

  1. 运行我们的应用。
1
npm start

Getting data with axios

Basic GET request

首先从GET请求开始。

  1. 打开App.tsx,导入:
1
import axios from "axios";
  1. 创建接口类型,表示JSONPlaceholder返回的内容:
1
2
3
4
5
6
interface IPost {
userId: number;
id?: number;
title: string;
body: string;
}
  1. 我们需要存储上述的邮件信息到state中,因此添加下面这个接口:
1
2
3
4
interface IState {
posts: IPost[];
}
class App extends React.Component<{}, IState> { ... }
  1. 在构造器生命周期中初始化state:
1
2
3
4
5
6
7
8
class App extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
posts: []
};
}
}
  1. 通常是在componentDidMount生命周期函数中获取REST API数据。
1
2
3
4
5
6
7
public componentDidMount() {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
.then(response => {
this.setState({ posts: response.data });
});
}
  • 这是使用get函数表述GET请求,它跟fetch一样,是一个promised-based函数。
  • 这里是一个泛型函数,泛型参数为返回消息数据类型。
  • URL地址作为传入参数。
  • then方法处理返回信息。
  • 通过data对象属性获取请求的返回对象。

因此,相比fetch有两点好处:

  • 可以定义返回的数据类型
  • 只需要一步即可获取返回体
  1. render方法渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public render() {
return (
<div className="App">
<url className="posts">
{this.state.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</url>
</div>
);
}

我们使用posts数组的map函数来展现数据。

  1. index.css添加CSS属性,
1
2
3
4
5
6
.posts {
list-style: none;
margin: 0px auto;
width: 800px;
text-align: left;
}

因此,使用axios来处理请求更简单方便。以及我们需要在componentDidMount生命周期函数中调用。

那么对于网络错误如何处理?

Handling errors

  1. 首先添加一个错误的URL,
1
.get<IPost[]>("https://jsonplaceholder.typicode.com/postsX")
  1. 我们希望出现网络错误的情况下,依然给用户以反馈内容。可以用catch方法:
1
2
3
4
5
6
7
8
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/postsX")
.then( ... )
.catch(ex => {
const error =
ex.response.status === 404 ? "Resource not found" : "An unexpected error has occurred";
this.setState({ error });
});

fetch不同,HTTP status错误返回码可以在catch方法处理。错误信息包含有一个属性response表示请求的返回内容。

  1. 另外修改该部分渲染,我们希望将错误信息显示出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
interface IState {
posts: IPost[];
error: string;
}
class App extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
posts: [],
error: ""
};
}
}
  1. 渲染错误内容:
1
2
3
4
<ul className="posts">
...
</ul>
{this.state.error && <p className="error">{this.state.error}</p>}
  1. 添加错误的样式:
1
2
3
.error {
color: red;
}

再次执行应用,可以看到红色的 Resource not found 字体。

  1. 将URL更改为原来有效的地址,
1
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")

Request Http headers

有时候我们希望带上header请求信息。

1
2
3
4
5
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
}
})

因此,我们在HTTP请求时定义了headers属性。

Timeouts

超时机制用于提高用户体验。

  1. 在我们的app添加一个请求超时:
1
2
3
4
5
6
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
},
timeout: 1
})

这里的单位是毫秒。表示期望请求在1毫秒内做出响应。

  1. catch方法处理超时:
1
2
3
4
5
6
7
8
9
.catch(ex => {
const error =
ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
this.setState({ error });
});

我们检测code属性来判断是否发生了超时。

再次执行应用,可以看到红色A timeout has occurred字体。

  1. 将超时更改为合适的值。
1
2
3
4
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
...
timeout: 5000
})

Canceling requests

允许用户取消请求可以提升用户体验效果。

  1. 首先,导入CancelTokenSource类型:
1
import axios, { CancelTokenSource } from "axios";
  1. 添加 cancel token和加载flag到state中:
1
2
3
4
5
6
interface IState {
posts: IPost[];
error: string;
cancelTokenSource?: CancelTokenSource;
loading: boolean;
}
  1. 在构造器初始化:
1
2
3
4
5
this.state = {
posts: [],
error: "",
loading: true
};
  1. GET请求之前,生成token资源:
1
2
3
4
5
6
7
8
9
public componentDidMount() {
const cancelToken = axios.CancelToken;
const cancelTokenSource = cancelToken.source();
this.setState({ cancelTokenSource });
axios
.get<IPost[]>(...)
.then(...)
.catch(...);
}
  1. 然后在GET请求中使用这个token:
1
2
3
4
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
cancelToken: cancelTokenSource.token,
...
})
  1. 我们可以在catch方法处理取消的情况。并设置loading状态为false
1
2
3
4
5
6
7
8
9
.catch((ex) => {
const error =
ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
this.setState({ error });
});

因此我们使用axios里面的isCancel函数来检测请求是否已经被取消。

  1. componentDidMount方法里面,在then方法设置loading的状态为false
1
2
3
.then(response => {
this.setState({ posts: response.data, loading: false });
})
  1. render方法中,添加一个Cancel按钮,允许用户取消请求:
1
2
3
4
{this.state.loading && (
<button onClick={this.handleCancelClick}>Cancel</button>
)}
<url className="posts">...</url>
  1. 实现取消处理:
1
2
3
4
5
private handleCancelClick = () => {
if (this.state.cancelTokenSource) {
this.state.cancelTokenSource.cancel("User cancelled operation");
}
};
  1. 现在有点难测,因为请求一般很快。为了可以看到取消请求的动作。我们可以在componentDidMount方法内立即取消请求动作:
1
2
3
4
5
axios
.get<IPost[]>(...)
.then(response => {...})
.catch(ex => {...});
cancelTokenSource.cancel("User cancelled operation");

回到浏览器可以看到红色字体的Request cancelled字样。

Creating data with axios

使用POST请求创建数据:

  1. 首先添加状态属性:
1
2
3
4
interface IState {
...
editPost: IPost;
}
  1. 在构造器初始化:
1
2
3
4
5
6
7
8
9
10
11
public constructor(props: {}) {
super(props);
this.state = {
...,
editPost: {
body: "",
title: "",
userId: 1
}
};
}
  1. 创建表单内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div className="App">
<div className="post-eidt">
<input
type="text"
placeholder="Enter title"
value={this.state.editPost.title}
onChange={this.handleTitleChange}
/>
<textarea
placeholder="Enter body"
value={this.state.editPost.body}
onChange={this.handleBodyChange}
/>
<button onClick={this.handleSaveClick}>Save</button>
{this.state.loading && (
<button onClick={this.handleCancelClick}>Cancel</button>
)}
...
</div>
</div>
  1. 下面是对状态的更新处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
editPost: {
...this.state.editPost,
title: e.currentTarget.value
}
});
};

private handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({
editPost: {
...this.state.editPost,
body: e.currentTarget.value
}
});
};
  1. index.css中添加一些CSS样式让它看起来更合理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.post-edit {
display: flex;
flex-direction: column;
width: 300px;
margin: 0 auto;
}
.post-edit input {
font-family: inherit;
width: 100%;
margin-bottom: 5px;
}
.post-edit textarea {
font-family: inherit;
width: 100%;
margin-bottom: 5px;
}
.post-edit button {
font-family: inherit;
width: 100px;
}
  1. 然后在点击时,触发POST请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private handleSaveClick = () => {
axios
.post<IPost>("https://jsonplaceholder.typicode.com/posts", {
body: this.state.editPost.body,
title: this.state.editPost.title,
userId: this.state.editPost.userId
},
{
headers: {
"Content-Type": "application/json"
}
}
).then(response => {
this.setState({
posts: this.state.posts.concat(response.data)
});
});
}

post函数的结构和get非常相似。实际上,可以像get方法一样,添加错误处理、超时、取消等动作。

Updating data with axios

我们希望用户可以点击Update按钮来更新数据。

  1. 首先创建一个Update按钮。
1
2
3
4
5
6
7
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => this.handleUpdateClick(post)}>
Update
</button>
</li>
  1. 实现Update按钮的事件处理:
1
2
3
private handleUpdateClick = (post:IPost) => {
this.setState({ editPost: post });
};
  1. 在原来的保存点击句柄,需要实现两个分支:
1
2
3
4
5
6
7
8
9
private handleSaveClick = () => {
if (this.state.editPost.id) {
// TODO - make a PUT request
} else {
axios
.post<IPost>( ... )
.then ( ... );
}
};
  1. 实现PUT请求分支:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (this.state.editPost.id) {
axios
.put<IPost>(
`https://jsonplaceholder.typicode.com/posts/${this.state.editPost.id}`, this.state.editPost, {
headers: {
"Content-Type": "application/json"
}
}
).then(() => {
this.setState({
editPost: {
body: "",
title: "",
userId: 1
},
posts: this.state.posts
.filter(post => post.id !== this.state.editPost.id)
.concat(this.state.editPost)
});
});
} else {
...
}

Delete data with axios

添加Delete按钮以允许用户删除数据:

  1. 首先创建一个Delete按钮:
1
2
3
4
5
6
7
8
9
10
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => this.handleUpdateClick(post)}>
Update
</button>
<button onClick={() => this.handleDeleteClick(post)}>
Delete
</button>
</li>
  1. 添加删除的事件处理:
1
2
3
4
5
6
7
8
9
private handleDeleteClick = (post: IPost) => {
axios
.delete(`https://jsonplaceholder.typicode.com/posts/${post.id}`)
.then(() => {
this.setState({
posts: this.state.posts.filter(p => p.id !== post.id)
});
});
};

Using axios with function components

本小节将实现函数组件(function component)版本的axios调用。我们将重构上一节的App的代码:

  1. 首先声明defaultPosts常量,它包含了邮箱的初始状态。
1
const defaultPosts: IPost[] = [];
  1. 删除IState接口,因为状态被构造为独立的块。
  2. 移除先前的App类组件。
  3. 接下来,在常量defaultPosts下开始我们的App函数组件。
1
const App: React.FC = () => {}
  1. 下面创建独立的状态块,包括post、error、cancel token、loading falg、editpost。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App: React.FC = () => {
const [posts, setPosts]: [IPost[], (posts: IPost[]) => void] = React.useState(defaultPosts);
const [error, setError]: [string, (error: string) => void] = React.useState('');
const cancelToken = axios.CancelToken;

const [cancelTokenSource, setCancelTokenSource]: [
CancelTokenSource,
(cancelSourceToken: CancelTokenSource) => void,
] = React.useState(cancelToken.source());

const [loading, setLoading]: [boolean, (loading: boolean) => void] = React.useState<boolean>(false);
const [editPost, setEditPost]: [IPost, (post: IPost) => void] = React.useState({
body: '',
title: '',
userId: 1,
});
}

因此,我们使用了useState函数来定义和初始化所有这些状态块。

  1. 我们希望在组件首次被挂载时调用REST API以获取邮箱信息。我们可以使用useEffect函数,在状态定义下添加:
1
2
3
React.useEffect(() => {
// TODO - get posts
}, []);
  1. 在arrow function调用REST API获取邮件数据:
1
2
3
4
5
6
7
8
9
10
React.useEffect(() => {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
cancelToken: cancelTokenSource.token,
headers: {
"Content-Type": "application/json"
},
timeout: 5000
});
}, []);
  1. 处理返回数据,设置邮箱数据和加载状态:
1
2
3
4
5
6
7
8
React.useEffect(() => {
axios
.get<IPost[]>(...)
.then(response => {
setPosts(response.data);
setLoading(false);
})
}, [])
  1. 处理错误状态信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
React.useEffect(() => {
axios
.get<IPost[]>(...)
.then(...)
.catch(ex => {
const err = axios.isCancel(ex)
? 'Request cancelled'
: ex.code === 'ECONNABORTED'
? 'A timeout has occurred'
: ex.response.status === 404
? 'Resource not found'
: 'An unexpected error has occurred';
setError(err);
setLoading(false);
});
}, []);

  1. 接下来处理事件。事件的处理并没有多大变化,只是使用const来声明,以及用前面声明的状态块来设置状态。
1
2
3
4
5
const handleCancelClick = () => {
if (cancelTokenSource) {
cancelTokenSource.cancel("User cancelled operation");
}
}
  1. 输入变更事件:
1
2
3
4
5
6
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditPost({ ...editPost, title: e.currentTarget.value });
};
const handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditPost({ ...editPost, body: e.currentTarget.value });
};
  1. Save按钮事件:
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 handleSaveClick = () => {
if (editPost.id) {
axios
.put<IPost>(`https://jsonplaceholder.typicode.com/posts/${editPost.id}`, editPost, {
headers: {
'Content-Type': 'application/json',
},
})
.then(() => {
setEditPost({
body: '',
title: '',
userId: 1,
});
setPosts(posts.filter(post => post.id !== editPost.id).concat(editPost));
});
} else {
axios
.post<IPost>(
'https://jsonplaceholder.typicode.com/posts',
{
body: editPost.body,
title: editPost.title,
userId: editPost.userId,
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
.then(response => {
setPosts(posts.concat(response.data));
});
}
};
  1. Update 按钮:
1
2
3
const handleUpdateClick = (post: IPost) => {
setEditPost(post);
};
  1. Delete按钮:
1
2
3
4
5
const handleDeleteClick = (post: IPost) => {
axios.delete(`https://jsonplaceholder.typicode.com/posts/${post.id}`).then(() => {
setPosts(posts.filter(p => p.id !== post.id));
});
};
  1. 最后的任务是实现返回语句。和原来的没有太大改变,只是删掉了this引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (
<div className="App">
<div className="post-edit">
<input type="text" placeholder="Enter title" value={editPost.title} onChange={handleTitleChange} />
<textarea placeholder="Enter body" value={editPost.body} onChange={handleBodyChange} />
<button onClick={handleSaveClick}>Save</button>
</div>
{loading && <button onClick={handleCancelClick}>Cancel</button>}
<ul className="posts">
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => handleUpdateClick(post)}>Update</button>
<button onClick={() => handleDeleteClick(post)}>Delete</button>
</li>
))}
</ul>
{error && <p className="error">{error}</p>}
</div>
);

主要不同的地方是,我们使用了useEffect函数来调用REST API,替换原来的componentDidMount()

Summary

Callback-based异步代码难于阅读和维护。谁会花几个小时,从根节点开始去追踪Callback-based异步代码的bug呢?又或者花好几个小时来逐步理解每个回调的意思?幸运的是,我们有Promise-based的写法。

Promise-based函数相比基于回调的写法有很大提升。更易于阅读、错误更易处理。结合关键字asyncawait后的使用相比原来代码有更大的阅读性。

现代浏览器有一个很好的fetch函数来处理REST请求。它是一个Promise-based的函数,提供对异步请求很好的处理。

axios是相对fetch的另一种选择。它的API更清晰,TypeScript更友好,错误处理更方便。

最后我们还对异步请求的实现做了两个不同的版本。一个是class component,另一个是function component(FC)。在类组件,异步的处理要放在componentDidMount生命周期函数中。在函数组件,使用useEffect函数来处理每次的渲染。两种方式,你会选择哪种?

REST API并不是唯一会交互的API。GraphQL是另一种流行的API服务。将在下个章节学习。

Questions

问题时间。

  1. 下面程序执行后,会在console输出什么?
1
2
3
4
5
6
7
try {
setInterval(() => {
throw new Error("Oops");
}, 1000);
} catch (ex) {
console.log("Sorry, there is a problem", ex);
}
  1. 假设没有9999这个邮箱,下面程序会输出什么?
1
2
3
4
5
6
7
fetch("https://jsonplaceholder.typicode.com/posts/999")
.then(response => {
console.log("HTTP status code", response.status);
return response.json();
})
.then(data => console.log("Response body", data))
.catch(error => console.log("Error", error));
  1. 下面程序执行后,console会输出什么?
1
2
3
4
5
6
7
8
axios
.get("https://jsonplaceholder.typicode.com/posts/9999")
.then(response => {
console.log("HTTP status code", response.status);
})
.catch(error => {
console.log("Error", error.response.status);
});
  1. 使用fetchaxios有什么好处?
  2. 下面程序如何添加一个bearer token?
1
axios.get("https://jsonplaceholder.typicode.com/posts/1")
  1. 我们使用下面程序来更新邮箱的标题?
1
2
3
4
axios.put("https://jsonplaceholder.typicode.com/posts/1", {
title: "corrected title",
body: "some stuff"
})
  1. 如果要用到PATCH请求,怎么改更高效。
  2. 我们实现了一个FC来显示邮箱,下面代码在执行时会有什么错误?
1
2
3
4
5
6
React.useEffect(() => {
axios
.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(...)
.catch(...);
});