赞
踩
在 JavaScript 中,最常使用 fetch 发起 HTTP 请求。
fetch(`https://api.github.com/users/moonhighway`)
.then(response => response.json())
.then(console.log)
.catch(console.error);
promise 还可以使用 async/await 处理。由于 fetch 返回一个 promise,因此我们可以在 async 函数中异步等待(await)请求:
async function requestGithubUser(githubLogin) {
try {
const response = await fetch(
`https://api.github.com/users/${githubLogin}`
);
const userData = await response.json();
console.log(userData);
} catch (error) {
console.error(error);
}
}
requestGithubUser("moonhighway");
一般来说,创建数据使用 POST 请求,修改数据使用 PUT 请求。fetch 函数的第二个参数接收一个选项对象,供 fetch 创建 HTTP 请求。
fetch("/create/user", {
method: "POST",
body: JSON.stringify({ username, password, bio })
})
上传文件需要使用一种不同的 HTTP 请求:multipart-formdata 请求。这种请求告诉服务器,请求的主体中有一个或多个文件。在 JavaScript 中发起这种请求,只需在请求的主体中传送一个 FormData 对象。
const formData = new FormData();
formData.append("username", "xx");
formData.append("fullname", "yy");
formData.append("avatar", imgFile);
fetch("/create/user", {
method: "POST",
body: formData
})
有时,我们要获得授权才能发起请求。通常,授权意味着获取个人或敏感数据。而且基本上要求用户通过 POST、PUT 或 DELETE 请求在服务器上执行一定的操作。
一般情况下,我们在每个请求中添加一个唯一的令牌(token),供服务识别用户的身份。这个令牌通常在 Authorization 首部中添加。
fetch(`https://api.github.com/users/${login}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`
}
});
GitHub 可为你生成一个个人用户令牌。首先登录 GitHub,依次进入 Settings -> Developer Settings -> Personal Access Tokens。然后创建具有特定读写规则的令牌,最后使用令牌从 GitHub API 中获取个人信息。随 fetch 请求一起发送 Personal Access Tokens,GitHub 将提供更多有关账户的隐私信息。
在 React 组件中获取数据要合理使用 useState 和 useEffect 钩子。前者的作用是把响应存储在状态中,后者的作用是发起 fetch 请求。
import React, { useState, useEffect } from "react"; function GitHubUser({ login }) { const [data, setData] = useState(); useEffect(() => { if (!login) return; fetch(`https://api.github.com/users/${login}`) .then(response => response.json()) .then(setData) .catch(console.error); }, [login]); if (data) return <pre>{JSON.stringify(data, null, 2)}</pre>; return null; } export default function App() { return <GitHubUser login="moonhighway" />; }
我们可以使用 Web Storage API 把数据存储在本地浏览器中。保存数据可以使用 window.localStorage 或 window.sessionStorage 对象。sessionStorage API 只把数据保存到用户会话中,关闭标签页或重启浏览器,保存的数据都将清空。而 localStorage 将无限期保存数据,除非主动删除。
JSON 数据应该以字符串的形式保存到浏览器存储空间中。这意味着,保存时要把对象转换成 JSON 对象,而在加载时则要把字符串解析为 JSON。下面是可用于保存和加载 JSON 数据的函数:
const loadJSON = key => key && JSON.parse(localStorage.getItem(key));
const saveJSON = (key, data) => localStorage.setItem(key, JSON.stringify(data));
例子:
import React, { useState, useEffect } from "react"; const loadJSON = key => key && JSON.parse(localStorage.getItem(key)); const saveJSON = (key, data) => localStorage.setItem(key, JSON.stringify(data)); function GitHubUser({ login }) { const [data, setData] = useState(loadJSON(`user:${login}`)); useEffect(() => { if (!data) return; if (data.login === login) return; const { name, avatar_url, location } = data; saveJSON(`user:${login}`, { name, login, avatar_url, location }); }, [data]); useEffect(() => { if (!login) return; if (data && data.login === login) return; fetch(`https://api.github.com/users/${login}`) .then(response => response.json()) .then(setData) .catch(console.error); }, [login]); if (data) return <pre>{JSON.stringify(data, null, 2)}</pre>; return null; } export default function App() { return <GitHubUser login="moonhighway" />; }
清空存储空间:
localStorage.clear();
sessionStorage 和 localStorage 均是 Web 开发者的得力武器。
HTTP 请求和 promise 均有三种状态:待定、成功(完成)和失败(被拒)。发送请求后等待响应期间,请求处于待定状态。响应只有两种可能,成功或失败。成功的响应表示顺利连接上服务器,收到了数据。对 promise 来说,成功的响应表示 promise 得到解决了。倘若在请求的过程中有什么地方出错了,那就可以说 HTTP 请求失败,或者 promise 被拒了。在这两种情况下,我们将收到一个 error 对象,说明具体情况。
发起 HTTP 请求时,这三种状态都要处理。
import { useState, useEffect } from "react"; function GitHubUser({ login }) { const [data, setData] = useState(); const [error, setError] = useState(); const [loading, setLoading] = useState(false); useEffect(() => { if (!login) return; setLoading(true); fetch(`https://api.github.com/users/${login}`) .then((data) => data.json()) .then(setData) .then(() => setLoading(false)) .catch(setError); }, [login]); if (loading) return <h1>loading...</h1>; if (error) return <pre>{JSON.stringify(error, null, 2)}</pre>; if (!data) return null; return ( <div className="githubUser"> <img src={data.avatar_url} alt={data.login} style={{ width: 200 }} /> <div> <h1>{data.login}</h1> {data.name && <p>{data.name}</p>} {data.location && <p>{data.location}</p>} </div> </div> ); } export default function App() { return <GitHubUser login="moonhighway" />; }
在异步组件中,为了最大程度提升可重用性,经常利用渲染属性模式。采用这种模式创建组件,可以把开发应用所需的复杂机制或单调的样板代码抽离出来。
import React from "react"; const tahoe_peaks = [ { name: "Freel Peak", elevation: 10891 }, { name: "Monument Peak", elevation: 10067 }, { name: "Pyramid Peak", elevation: 9983 }, { name: "Mt. Tallac", elevation: 9735 } ]; function List({ data = [], renderItem, renderEmpty }) { return !data.length ? ( renderEmpty ) : ( <ul> {data.map((item, i) => ( <li key={i}>{renderItem(item)}</li> ))} </ul> ); } export default function App() { return ( <List data={tahoe_peaks} renderEmpty={<p>This list is empty</p>} renderItem={item => ( <> {item.name} - {item.elevation.toLocaleString()} </> )} /> ); }
生产环境中的应用通常要渲染大量数据,但是又不能一次性全部渲染。浏览器的渲染能力是有限的。渲染要耗费时间、占据处理能力及消耗内存,这三项都是有限制的。
在用户滚动界面的过程中,我们要消除用户已经看过的结果,并渲染位于屏幕范围以外的新结果,随时准备展示。这种解决方案一次只渲染11个元素,其他数据排队等候,后面再渲染。这种技术称为虚拟化。使用这种技术滚动特大型列表,数据量无穷尽时,浏览器也不会崩溃。
构建虚拟化列表组件要考虑很多事情。幸好,我们不用从头开始动手,社区已经开发了很多虚拟化列表组件,直接拿来使用即可。对浏览器渲染来说,最受欢迎的是 react-window 和 react-virtualized。虚拟化列表十分重要,React Native 甚至自带了一个这样的组件,即 FlatList。多数时候,我们无须自己动手构建虚拟化列表组件,知道如何使用就可以了。
为了实现虚拟化列表,我们需要大量数据。这里指的是大量虚拟数据。
npm i faker
import React from "react"; import faker from "faker"; const bigList = [...Array(5000)].map(() => ({ name: faker.name.findName(), email: faker.internet.email(), avatar: faker.internet.avatar() })); function List({ data = [], renderItem, renderEmpty }) { return !data.length ? ( renderEmpty ) : ( <ul> {data.map((item, i) => ( <li key={i}>{renderItem(item)}</li> ))} </ul> ); } export default function App() { const renderItem = item => ( <div style={{ display: "flex" }}> <img src={item.avatar} alt={item.name} width={50} /> <p> {item.name} - {item.email} </p> </div> ); return <List data={bigList} renderItem={renderItem} />; }
下面使用 react-window 渲染这个虚拟用户列表:
npm -i react-window
import React from "react"; import { FixedSizeList } from "react-window"; import faker from "faker"; const bigList = [...Array(5000)].map(() => ({ name: faker.name.findName(), email: faker.internet.email(), avatar: faker.internet.avatar() })); export default function App() { const renderRow = ({ index, style }) => ( <div style={{ ...style, ...{ display: "flex" } }}> <img src={bigList[index].avatar} alt={bigList[index].name} width={50} /> <p> {bigList[index].name} - {bigList[index].email} </p> </div> ); return ( <FixedSizeList height={window.innerHeight} width={window.innerWidth - 20} itemCount={bigList.length} itemSize={50} > {renderRow} </FixedSizeList> ); }
一个请求有三种状态:待定、成功或失败。为了重用 fetch 请求的这个逻辑,我们可以自定义一个钩子。在整个应用中,只要想发起 fetch 请求,就可以在组件中使用这个钩子。下面创建 useFetch 钩子:
import React, { useState, useEffect } from "react"; function useFetch(uri) { const [data, setData] = useState(); const [error, setError] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { if (!uri) return; fetch(uri) .then(data => data.json()) .then(setData) .then(() => setLoading(false)) .catch(setError); }, [uri]); return { loading, data, error }; }
钩子最大的作用是跨组件重用的功能。有时,在不同的组件中需要重复渲染同样的内容。一个应用中,fetch 请求的错误处理方式或许也应该保持一致。下面创建一个 Fetch 组件:
export default function Fetch({ uri, renderSuccess, loadingFallback = <p>loading...</p>, renderError = error => <pre>{JSON.stringify(error, null, 2)}</pre> }) { const { loading, data, error } = useFetch(uri); if (loading) return loadingFallback; if (error) return renderError(error); if (data) return renderSuccess({ data }); } // 如何使用 Fetch 组件如下: function GitHubUser({ login }) { return ( <Fetch uri={`https://api.github.com/users/${login}`} renderSuccess={UserDetails} /> ); } function UserDetails({ data }) { return ( <div className="githubUser"> <img src={data.avatar_url} alt={data.login} style={{ width: 200 }} /> <div> <h1>{data.login}</h1> {data.name && <p>{data.name}</p>} {data.location && <p>{data.location}</p>} </div> <UserRepositories login={data.login} onSelect={(repoName) => console.log(`${repoName} selected`)} /> </div> ); }
下面创建一个名为 useIterator 的自定义钩子,迭代任意类型的对象数组:
export const useIterator = (items = [], initialValue = 0) => { const [i, setIndex] = useState(initialValue); const prev = useCallback(() => { if (i === 0) return setIndex(items.length - 1); setIndex(i - 1); }, [i]); const next = useCallback(() => { if (i === items.length - 1) return setIndex(0); setIndex(i + 1); }, [i]); const item = useMemo(() => items[i], [i]); return [item || items[0], prev, next]; };
const [letter, previous, next] = useIterator([
"a",
"b",
"c"
])
下面例子中,我们使用 useIterator 钩子,让用户遍览仓库列表:
import React from "react"; import { useIterator } from "./hooks"; import RepositoryReadme from "./RepositoryReadme"; export default function RepoMenu({ repositories, login }) { const [{ name }, previous, next] = useIterator(repositories); return ( <> <div style={{ display: "flex" }}> <button onClick={previous}><</button> <p>{name}</p> <button onClick={next}>></button> </div> <RepositoryReadme login={login} repo={name} /> </> ); }
<
是小于号的实体,显示一个小于号“<”。瀑布式请求:一个接着一个地发起请求,前后请求之后有依赖关系,如果前一个请求出错了,后一个请求就不会再发起。
仓库的 README 文件是使用 Markdown 编写,这是一种文本格式,可使用 ReactMarkdown 组件渲染为 HTML。安装 react-markdown 包:
npm i react-markdown
请求仓库的 README 文件内容也涉及瀑布式请求。首先,要向仓库 README 文件的路由发起数据请求。GitHub 对这个路由的响应是关于仓库 README 文件的详情,而不是文件内容。不过,响应中有个 download_url,我们可以通过它请求 README 文件的内容。可见,为了获取 Markdown 内容,要多发起一次请求。这里两个请求可以在同一个异步函数中发起:
import React, { useState, useEffect, useCallback } from "react"; import ReactMarkdown from "react-markdown"; export default function RepositoryReadme({ repo, login }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [markdown, setMarkdown] = useState(""); const loadReadme = useCallback(async (login, repo) => { setLoading(true); const uri = `https://api.github.com/repos/${login}/${repo}/readme`; const { download_url } = await fetch(uri).then(res => res.json()); const markdown = await fetch(download_url).then(res => res.text()); setMarkdown(markdown); setLoading(false); }, []); useEffect(() => { if (!repo || !login) return; loadReadme(login, repo).catch(setError); }, [repo]); if (error) return <pre>{JSON.stringify(error, null, 2)}</pre>; if (loading) return <p>Loading...</p>; return <ReactMarkdown source={markdown} />; }
所有请求都可在开发者工具的“Network”标签页中查看。在这个标签页中,你可以查看每一个请求,还可以限制网络速度,考察请求在较慢的网速下表现如何。
在 Google Chrome 中如果想限制网络速度,单击“Online”旁边的箭头,在打开的菜单中,你可以选择不同的速度,选择“Fast 3G”和“Slow 3G”对网络请求的速度限制较大。另外,“Network”标签页还显示有全部 HTTP 请求的时间线。你可以筛选时间线,只查看“XHR”请求,即只显示 fetch 发出的请求。
有时候,可以一次发送全部请求,提升应用的速度。这种情况下,不再像瀑布式那样一个接一个发起请求,而是并行(或同时)发起所有请求。前面的例子之所以发送瀑布式请求,是因为组件是一个套一个渲染的,前一个组件未渲染之前不能发起下一个请求。如果在同一级渲染这三个组件,所有请求同时并行发起。
我们并不能始终猜测出一开始要渲染什么数据。遇到这种情况,我们干脆不渲染组件,等到获得所需的数据再渲染。
export default function App() { const [login, setLogin] = useState(); const [repo, setRepo] = useState(); return ( <> <SearchForm value={login} onSearch={setLogin} /> {login && <GitHubUser login={login} />} {login && ( <UserRepositoties login={login} repo={repo} onSelect={setRepo} /> )} {login && repo && ( <RepositoryReadme login={login} repo={repo} /> )} </SearchForm> ); }
当 fetch 请求返回响应时,如果组件已经卸载完毕,试图修改已被卸载的组件的状态值会在控制台报错。
export function useMountedRef() {
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
});
return mounted;
}
const mounted = useMountedRef();
const loadReadme = useCallback(async (login, repo) => {
setLoading(true);
const uri = `https://api.github.com/repos/${login}/${repo}/readme`;
const { download_url } = await fetch(uri).then(res => res.json());
const markdown = await fetch(download_url).then(res => res.text());
if (mounted.current) {
setMarkdown(markdown);
setLoading(false);
}
}, []);
React 为构建用户界面提供了一种声明式方案,GraphQL 为与 API 通信提供一种声明式方案。并行发起数据请求时,我们希望能同时获得所需的全部数据。GraphQL 就是为此而设计的。
若想在 React 应用中使用 GraphQL,与之通信的后端服务要按照 GraphQL 规范构建。幸好,GitHub 也开放了 GraphQL API。多数 GraphQL 服务都提供有探索 GraphQL API 的方式。GitHub 提供的是 GraphQL Explorer(https://developer.github.com/v4/explorer)。
GraphQL 请求是在请求主体中包含查询的 HTTP 请求。我们可以使用 fetch 发起 GraphQL 请求。此外,也有一些专门的库和框架能辅助我们处理这类请求各方面的细节。
安装 graphql-request 库:
npm i graphql-request
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。