- 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 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 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 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
以及一个回调处理等待动作。
- 消费这个
promised-based
函数:
1 2 3
| wait(500) .then(result => console.log("then >", result)) .then(error => console.log("catch >", error));
|
等待500毫秒后,函数将输出正确或失败信息。
- 将等待时间延长,大于1000,
catch
方法被调用。
1 2 3
| wait(1500) .then(result => console.log("then >", result)) .then(error => console.log("catch >", error));
|
Promise
对于异步代码有一个很好的处理机制。致辞,还有另一种异步处理的实现方式。
async and awit
async
和await
是JavaScript的关键字。
- 首先从
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
标注为异步。
- 使用关键字
await
在wait
前面声明。wait
下一行的执行将被暂停(halt)直到这个异步操作完成。
try / catch
将捕获任何异常信息。
该方法有点像是一个异步操作的管理者。执行代码后,控制台打印:
1
| then > Successfully waited
|
- 将等待时间改为1500毫秒:
1
| const result = await wait(1500);
|
控制台打印错误信息:
因此,使用async
和await
使得代码更易于阅读。另一个奖励是,这种实现在旧的浏览器中仍然支持。
到目前为止,我们已经对如何编写更好的异步代码有更好的理解,下面会就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信息。
通常,请求信息需要包含标头(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
首先新建一个应用:
- 在控制台通过命令新建一个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
|
- 项目创建后,添加TSLint到项目中,并带有某些规则:
1 2
| cd crud-api npm install tslint tslint-react tslint-config-prettier --save-dev
|
- 新建
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" ] } }
|
- 打开
App.tsx
文件,会有一个linting错误。在render()
方法中添加public
关键字处理该问题:
1 2 3 4 5
| class App extends Component { public render() { return ( ... ); } }
|
- 添加
axios
:
注意axios
内已经包含TypeScript类型了,不需要额外安装。
- 运行我们的应用。
Getting data with axios
Basic GET request
首先从GET请求开始。
- 打开
App.tsx
,导入:
1
| import axios from "axios";
|
- 创建接口类型,表示JSONPlaceholder返回的内容:
1 2 3 4 5 6
| interface IPost { userId: number; id?: number; title: string; body: string; }
|
- 我们需要存储上述的邮件信息到state中,因此添加下面这个接口:
1 2 3 4
| interface IState { posts: IPost[]; } class App extends React.Component<{}, IState> { ... }
|
- 在构造器生命周期中初始化state:
1 2 3 4 5 6 7 8
| class App extends React.Component<{}, IState> { public constructor(props: {}) { super(props); this.state = { posts: [] }; } }
|
- 通常是在
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
有两点好处:
- 在
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
函数来展现数据。
- 在
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
- 首先添加一个错误的URL,
1
| .get<IPost[]>("https://jsonplaceholder.typicode.com/postsX")
|
- 我们希望出现网络错误的情况下,依然给用户以反馈内容。可以用
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 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 2 3 4
| <ul className="posts"> ... </ul> {this.state.error && <p className="error">{this.state.error}</p>}
|
- 添加错误的样式:
再次执行应用,可以看到红色的 Resource not found 字体。
- 将URL更改为原来有效的地址,
1
| .get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
|
有时候我们希望带上header请求信息。
1 2 3 4 5
| .get<IPost[]>("https://jsonplaceholder.typicode.com/posts", { headers: { "Content-Type": "application/json" } })
|
因此,我们在HTTP请求时定义了headers
属性。
Timeouts
超时机制用于提高用户体验。
- 在我们的app添加一个请求超时:
1 2 3 4 5 6
| .get<IPost[]>("https://jsonplaceholder.typicode.com/posts", { headers: { "Content-Type": "application/json" }, timeout: 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 2 3 4
| .get<IPost[]>("https://jsonplaceholder.typicode.com/posts", { ... timeout: 5000 })
|
Canceling requests
允许用户取消请求可以提升用户体验效果。
- 首先,导入
CancelTokenSource
类型:
1
| import axios, { CancelTokenSource } from "axios";
|
- 添加 cancel token和加载flag到state中:
1 2 3 4 5 6
| interface IState { posts: IPost[]; error: string; cancelTokenSource?: CancelTokenSource; loading: boolean; }
|
- 在构造器初始化:
1 2 3 4 5
| this.state = { posts: [], error: "", loading: true };
|
- 在
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(...); }
|
- 然后在GET请求中使用这个token:
1 2 3 4
| .get<IPost[]>("https://jsonplaceholder.typicode.com/posts", { cancelToken: cancelTokenSource.token, ... })
|
- 我们可以在
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
函数来检测请求是否已经被取消。
- 在
componentDidMount
方法里面,在then
方法设置loading
的状态为false
:
1 2 3
| .then(response => { this.setState({ posts: response.data, loading: false }); })
|
- 在
render
方法中,添加一个Cancel
按钮,允许用户取消请求:
1 2 3 4
| {this.state.loading && ( <button onClick={this.handleCancelClick}>Cancel</button> )} <url className="posts">...</url>
|
- 实现取消处理:
1 2 3 4 5
| private handleCancelClick = () => { if (this.state.cancelTokenSource) { this.state.cancelTokenSource.cancel("User cancelled operation"); } };
|
- 现在有点难测,因为请求一般很快。为了可以看到取消请求的动作。我们可以在
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 2 3 4
| interface IState { ... editPost: IPost; }
|
- 在构造器初始化:
1 2 3 4 5 6 7 8 9 10 11
| public constructor(props: {}) { super(props); this.state = { ..., editPost: { body: "", title: "", userId: 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 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 } }); };
|
- 在
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; }
|
- 然后在点击时,触发
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按钮来更新数据。
- 首先创建一个
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>
|
- 实现Update按钮的事件处理:
1 2 3
| private handleUpdateClick = (post:IPost) => { this.setState({ editPost: post }); };
|
- 在原来的保存点击句柄,需要实现两个分支:
1 2 3 4 5 6 7 8 9
| private handleSaveClick = () => { if (this.state.editPost.id) { } else { axios .post<IPost>( ... ) .then ( ... ); } };
|
- 实现
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
按钮以允许用户删除数据:
- 首先创建一个
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 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
的代码:
- 首先声明
defaultPosts
常量,它包含了邮箱的初始状态。
1
| const defaultPosts: IPost[] = [];
|
- 删除
IState
接口,因为状态被构造为独立的块。
- 移除先前的
App
类组件。
- 接下来,在常量
defaultPosts
下开始我们的App
函数组件。
1
| const App: React.FC = () => {}
|
- 下面创建独立的状态块,包括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
函数来定义和初始化所有这些状态块。
- 我们希望在组件首次被挂载时调用REST API以获取邮箱信息。我们可以使用
useEffect
函数,在状态定义下添加:
1 2 3
| React.useEffect(() => { }, []);
|
- 在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 2 3 4 5 6 7 8
| React.useEffect(() => { axios .get<IPost[]>(...) .then(response => { setPosts(response.data); setLoading(false); }) }, [])
|
- 处理错误状态信息:
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); }); }, []);
|
- 接下来处理事件。事件的处理并没有多大变化,只是使用
const
来声明,以及用前面声明的状态块来设置状态。
1 2 3 4 5
| const handleCancelClick = () => { if (cancelTokenSource) { cancelTokenSource.cancel("User cancelled operation"); } }
|
- 输入变更事件:
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 }); };
|
- 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)); }); } };
|
- Update 按钮:
1 2 3
| const handleUpdateClick = (post: IPost) => { setEditPost(post); };
|
- 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)); }); };
|
- 最后的任务是实现返回语句。和原来的没有太大改变,只是删掉了
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函数相比基于回调的写法有很大提升。更易于阅读、错误更易处理。结合关键字async
和await
后的使用相比原来代码有更大的阅读性。
现代浏览器有一个很好的fetch
函数来处理REST请求。它是一个Promise-based的函数,提供对异步请求很好的处理。
axios
是相对fetch
的另一种选择。它的API更清晰,TypeScript更友好,错误处理更方便。
最后我们还对异步请求的实现做了两个不同的版本。一个是class component,另一个是function component(FC)。在类组件,异步的处理要放在componentDidMount
生命周期函数中。在函数组件,使用useEffect
函数来处理每次的渲染。两种方式,你会选择哪种?
REST API并不是唯一会交互的API。GraphQL是另一种流行的API服务。将在下个章节学习。
Questions
问题时间。
- 下面程序执行后,会在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); }
|
- 假设没有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));
|
- 下面程序执行后,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); });
|
- 使用
fetch
和axios
有什么好处?
- 下面程序如何添加一个bearer token?
1
| axios.get("https://jsonplaceholder.typicode.com/posts/1")
|
- 我们使用下面程序来更新邮箱的标题?
1 2 3 4
| axios.put("https://jsonplaceholder.typicode.com/posts/1", { title: "corrected title", body: "some stuff" })
|
- 如果要用到
PATCH
请求,怎么改更高效。
- 我们实现了一个FC来显示邮箱,下面代码在执行时会有什么错误?
1 2 3 4 5 6
| React.useEffect(() => { axios .get(`https://jsonplaceholder.typicode.com/posts/${id}`) .then(...) .catch(...); });
|