五、将 React 路由用于路由

到目前为止,我们的 Q&A 应用只包含一个页面,所以是时候向该应用添加更多页面了。在第 1 章理解 ASP.NET 5 React 模板中,我们了解到单页应用SPA中的页面是在浏览器中构建的,不需要向服务器发送任何 HTML 请求。

React Router 是一个很棒的库,它可以帮助我们实现客户端页面以及它们之间的导航。因此,我们将在本章中把它引入我们的项目中。

在本章中,我们将声明性地定义应用中可用的路由。我们学习如何在用户导航到不存在的路径时向他们提供反馈。我们将实现一个页面,显示问题的详细信息及其答案。这是我们将学习如何实现路由参数的地方。我们将从实现问题搜索功能开始,在这里我们将学习如何处理查询参数。我们还将开始实现用于提问的页面,并对其进行优化,使其 JavaScript 按需加载,而不是在应用加载时加载。

本章将介绍以下主题:

  • 安装 React 路由
  • 申报路线
  • 未找到处理路线
  • 实施链接
  • 使用路线参数
  • 使用查询参数
  • 延迟加载路径

技术要求

在本章中,我们将使用以下工具:

本章中的所有代码片段可在网上找到 https://github.com/PacktPublishing/ASP.NET-Core-5-and-React-Second-Edition 。为了从章节中恢复代码,可以下载源代码存储库,并在相关编辑器中打开相关文件夹。如果代码为前端代码,则可在终端中输入npm install恢复依赖关系。

查看以下视频以查看代码的运行:http://bit.ly/34XoKyz

安装 React 路由

在本节中,我们将通过执行以下步骤来安装具有相应类型脚本的 React Router:

  1. Make sure the frontend project is open in Visual Studio Code and enter the following command to install React Router in the terminal:

    ```cs

    npm install react-router-dom ```

    重要提示

    确保已安装react-router-dom版本 6+,并在package.json中列出。如果已安装版本 5,则可通过运行npm install react-router-dom@next安装版本 6。

  2. React router has a peer dependency on the history package, so let's install this using the terminal as well:

    ```cs

    npm install history ```

    对等依赖项是 npm 不会自动安装的依赖项。这就是为什么我们在项目中安装了它。

就这样简单又好!我们将在下一节开始在应用中声明路由。

申报航线

我们使用BrowserRouterRoutesRoute组件在应用中声明页面。BrowserRouter是执行页面间导航的顶级组件。页面路径在嵌套在Routes组件中的Route组件中定义。Routes组件决定应为当前浏览器位置呈现哪个Route组件。

我们将从创建空白页开始这一部分,我们最终将在本书中实现这些空白页。然后,我们将使用BrowserRouterRoutesRoute组件在我们的应用中声明这些页面。

创建一些空白页

让我们创建空白页面,用于登录、提问、查看搜索结果以及查看问题及其答案,具体步骤如下:

  1. Create a file called SignInPage.tsx with the following content:

    cs import React from 'react'; import { Page } from './Page'; export const SignInPage = () => ( <Page title="Sign In">{null}</Page> );

    在这里,我们使用上一章中创建的Page组件创建了一个标题为登录的空页面。我们将对需要创建的其他页面使用类似的方法。

    请注意,我们目前正在Page组件的内容中呈现null。这是一种告诉 React to render nothing 的方法。

  2. 创建一个名为AskPage.tsx的文件,内容如下:

    cs import React from 'react'; import { Page } from './Page'; export const AskPage = () => ( <Page title="Ask a question">{null}</Page> );

  3. 创建一个名为SearchPage.tsx的文件,其内容如下:

    cs import React from 'react'; import { Page } from './Page'; export const SearchPage = () => ( <Page title="Search Results">{null}</Page> );

  4. 创建一个名为QuestionPage.tsx的文件,其内容如下:

    cs import React from 'react'; import { Page } from './Page'; export const QuestionPage = () => ( <Page>Question Page</Page> );

问题页面上的标题将采用不同的样式,这就是为什么我们不在Page组件上使用title道具的原因。我们只是暂时在页面上添加了一些文本,以便能够将此页面与其他页面区分开来。

这就是我们创建的页面。现在,是定义这些页面的所有路由的时候了。

创建包含路由的组件

我们将通过执行以下步骤来定义到我们创建的页面的所有路由:

  1. 打开App.tsx并在现有import语句下添加以下import语句:

    cs import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AskPage } from './AskPage'; import { SearchPage } from './SearchPage'; import { SignInPage } from './SignInPage';

  2. App组件的 JSX 中,添加BrowserRouter作为最外层元素:

    cs <BrowserRouter> <div css={ ... } > <Header /> <HomePage /> </div> </BrowserRouter>

  3. Let's define the routes in our app under the Header component, replacing the previous reference to HomePage:

    cs <BrowserRouter> <div css={ ... } > <Header /> <Routes> <Route path="" element={<HomePage/>} /> <Route path="search" element={<SearchPage/>} /> <Route path="ask" element={<AskPage/>} /> <Route path="signin" element={<SignInPage/>} /> </Routes> </div> </BrowserRouter>

    每个路由在Route组件中定义,该组件定义了element道具中针对path道具中给定路径应呈现的内容。将渲染路径与浏览器位置最匹配的管线。

    例如,如果浏览器位置为http://localhost:3000/search ,则第二个Route组件(将path设置为"search"的组件)将是最佳匹配。这意味着SearchPage组件被渲染。

    请注意,路径上不需要前面的斜杠(/,因为 React Router 默认执行相对匹配。

  4. 通过在 Visual Studio 代码终端中输入npm start命令来运行应用。我们将看到主页呈现与以前一样,这非常好。

  5. 现在,在浏览器位置路径的末尾输入/search

Figure 5.1 – Search page

图 5.1–搜索页面

在这里,我们可以看到 React Router 已经确定最佳匹配是路径为"search"Route组件,因此呈现SearchPage组件。

也可以随意访问其他页面–它们现在将呈现良好效果。

这就是我们的基本路由配置。如果用户在浏览器中输入的路径在我们的应用中不存在,会发生什么情况?我们将在下一节中找到答案。

未找到处理路线

在本节中,我们将处理任何Route组件都无法处理的路径。通过以下步骤,我们将首先了解如果在浏览器中放置未处理的路径会发生什么:

  1. Enter a path that isn't handled in the browser and see what happens:

    Figure 5.2 – Unhandled path

    图 5.2–未处理的路径

    因此,当我们浏览到一个不由Route组件处理的路径时,标题下不会呈现任何内容。如果我们仔细想想,这是有道理的。

  2. We'd like to improve the user experience of routes not found and inform the user that this is the case. Let's add the following highlighted route inside the Routes component:

    cs <Routes> <Route path="" element={<HomePage/>} /> <Route path="search" element={<SearchPage/>} /> <Route path="ask" element={<AskPage/>} /> <Route path="signin" element={<SignInPage/>} /> <Route path="*" element={<NotFoundPage/>} /> </Routes>

    为了理解这是如何工作的,让我们再考虑一下Routes组件的作用–它呈现出与浏览器位置最匹配的Route组件。路径*将匹配任何浏览器位置,但不是非常具体。因此,*将不是//search/ask/signin浏览器位置的最佳匹配,但将捕获无效路由。

  3. NotFoundPage尚未实现,我们创建一个名为NotFoundPage.tsx的文件,内容如下:

    cs import React from 'react'; import { Page } from './Page'; export const NotFoundPage = () => ( <Page title="Page Not Found">{null}</Page> );

  4. 回到App.tsx,让我们导入NotFoundPage组件:

    cs import { NotFoundPage } from './NotFoundPage';

  5. 现在,如果我们在浏览器中输入一个/invalid路径,我们将看到我们的NotFoundPage组件已经呈现:

Figure 5.3 – Unhandled path

图 5.3–未处理的路径

因此,一旦我们了解了Routes组件的工作原理,实现一个未找到的页面就非常容易了。我们只需在Routes组件内部使用路径为*Route组件。

目前,我们正在通过手动更改浏览器中的位置来导航到应用中的不同页面。在下一节中,我们将学习如何实现链接以在应用本身中执行导航。

实施环节

在本节中,我们将使用 React Router 中的组件Link在点击应用标题中的应用名称时声明性地执行导航。然后,单击提问按钮进入提问页面时,我们将继续以编程方式执行导航。

使用链路组件

此时,当我们点击应用左上角的Q 和时,它正在执行一个 HTTP 请求,返回整个 React 应用,然后呈现主页。我们将通过使用 React 路由的Link组件来改变这一点,以便在浏览器中进行导航,而无需 HTTP 请求。我们还将使用Link组件作为登录页面的链接。我们将通过执行以下步骤了解如何实现这一点:

  1. Header.tsx中,从 React 路由导入Link组件。在现有的import语句下放置以下行:

    cs import { Link } from 'react-router-dom';

  2. 让我们将Q & A文本周围的锚定标记更改为Link元素。href属性也需要更改为to属性:

    ```cs <Link to="/" css={ ... }

    Q & A ```

  3. 我们还将登录链接更改为以下内容:

    ```cs <Link to="signin" css={ ... }

    Sign In ```

  4. 如果我们转到 running 应用并单击登录链接,我们将看到登录页面。现在,点击应用标题中的Q&A。我们将被带回主页,就像我们想要的那样。

  5. 再次执行步骤 4,但这次打开浏览器开发工具,查看网络选项卡。我们会发现,当点击登录Q&A链接时,没有网络请求。

因此,Link组件是在 JSX 中声明性地提供客户端导航选项的一种好方法。我们在上一步中执行的任务确认,所有导航都在浏览器中进行,没有任何服务器请求,这对性能非常有利。

编程导航

有时,有必要以编程方式进行导航。单击提问按钮时,按照以下步骤以编程方式导航至提问页面:

  1. Import useNavigate from React Router into HomePage.tsx:

    cs import { useNavigate } from 'react-router-dom';

    这是一个钩子,它返回一个我们可以用来执行导航的函数。

  2. useNavigate钩子分配给handleAskQuestionClick事件处理程序

    cs const navigate = useNavigate(); const handleAskQuestionClick = () => { ... };

    前面名为navigate的函数 3. 在handleAskQuestionClick中,我们可以将console.log语句替换为导航:

    cs const handleAskQuestionClick = () => { navigate('ask'); };

  3. 在 running 应用中,如果我们尝试一下并单击提问按钮,它将成功导航到提问页面。

因此,我们可以使用Link组件以声明方式导航,并使用 React Router 中的useNavigate钩子以编程方式导航。我们将在下一节继续使用Link组件。

使用路线参数

在本节中,我们将定义一个Route组件,用于导航到问题页面。这将在路径的末尾包含一个名为questionId的变量,因此我们需要使用一个名为的路由参数。在本节中,我们还将实现更多的问题页面内容。

添加问题页面路径

让我们执行以下步骤来添加问题页面路径:

  1. App.tsx中,导入我们在本章前面创建的QuestionPage组件:

    cs import { QuestionPage } from './QuestionPage';

  2. In the App component's JSX, add a Route component for navigation to the question page inside the Routes component just above the wildcard route:

    cs <Routes> … <Route path="questions/:questionId" element={<QuestionPage />} /> <Route path="*" element={<NotFoundPage/>} /> </Routes>

    请注意,我们输入的路径末尾包含:questionId

    重要提示

    路由参数在路径中定义,前面有一个冒号。然后,该参数的值可用于在useParams钩子中解构。

    Route组件可以放置在Routes组件内的任何位置。可以说,将通配符路由保留在底部更具可读性,因为这是最不特定的路径,因此将是最后一个要匹配的路径。

  3. 我们转到QuestionPage.tsx并从 React 路由

    cs import { useParams } from 'react-router-dom';

    导入useParams 4. We can destructure the value of the questionId route parameter from the useParams hook:

    cs export const QuestionPage = () => { const { questionId } = useParams(); return <Page>Question Page</Page>; };

    我们还将QuestionPage更改为具有显式返回语句。

  4. For now, we are going to output questionId on the page as follows in the JSX:

    cs <Page>Question Page {questionId}</Page>;

    我们将返回并全面实施第 6 章中的问题页面,使用表单。现在,我们将从Question组件链接到此页面。

  5. 因此,在Question.tsx中,添加以下import语句来导入Link组件:

    cs import { Link } from 'react-router-dom';

  6. 现在,我们可以在QuestionJSX 中的标题文本周围包装一个Link组件,同时指定导航到的路径:

    ``cs <div css={css padding: 10px 0px; font-size: 19px; `}

    <Link css={csstext-decoration: none; color: ${gray2};} to={/questions/${data.questionId}}

    {data.title}
    

    ```

  7. 转到 running 应用并尝试点击我应该使用哪个状态管理工具?问题。它将成功导航到问题页面,显示正确的questionId

Figure 5.4 – Question page with route parameter

图 5.4–带路线参数的问题页面

因此,我们通过在路由路径中使用冒号定义变量,然后使用useParams钩子拾取值来实现路由参数。

实现更多问题页面

让我们再执行一些步骤来进一步实现问题页面:

  1. In QuestionsData.ts, add a function that will simulate a web request to get a question:

    cs export const getQuestion = async ( questionId: number ): Promise<QuestionData | null> => { await wait(500); const results = questions.filter(q => q.questionId === questionId); return results.length === 0 ? null : results[0]; };

    我们已经使用数组filter方法为传入的questionId获取问题。

    请注意函数返回类型的类型注释。传入Promise泛型类型的类型为Question | null,称为联合类型

    重要提示

    联合类型是一种机制,用于定义包含多个类型值的类型。如果我们将一个类型看作一组值,那么多个类型的并集与值集的并集是相同的。更多信息请访问https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-类型

    因此,函数应该异步返回一个QuestionDatanull类型的对象。

  2. 转到QuestionPage.tsx,我们导入刚才创建的函数,以及问题界面:

    cs import { QuestionData, getQuestion } from './QuestionsData';

  3. 添加 Emotionimport语句,并从我们的标准颜色中导入一些灰色。在QuestionPage.tsx顶部添加以下行:

    cs /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; import { gray3, gray6 } from './Styles';

  4. In the QuestionPage component, create a state for the question:

    cs export const QuestionPage = () => { const [ question, setQuestion, ] = React.useState<QuestionData | null>(null); const { questionId } = useParams(); return <Page>Question Page {questionId}</Page>; };

    我们将把问题存储在组件最初渲染时的状态中。

    请注意,我们正在为状态使用联合类型,因为在获取问题时,状态最初是null,如果找不到问题,状态也是null

  5. We want to call the getQuestion function during the initial render, so let's call it inside a call to the useEffect hook:

    cs export const QuestionPage = () => { … const { questionId } = useParams(); React.useEffect(() => { const doGetQuestion = async ( questionId: number, ) => { const foundQuestion = await getQuestion( questionId, ); setQuestion(foundQuestion); }; if (questionId) { doGetQuestion(Number(questionId)); } }, [questionId]); return ... };

    因此,当第一次呈现时,问题组件将获取问题并将其设置为导致组件第二次呈现的状态。请注意,我们使用Number构造函数将questionIdstring转换为number

    另外,请注意,useEffect函数中的第二个参数在数组中具有questionId。这是因为useEffect运行的函数(第一个参数)依赖于questionId值,如果该值发生变化,则应重新运行。如果没有提供[questionId],它将进入无限循环,因为每次调用setQuestion,它都会导致重新渲染,如果没有[questionId],它将始终重新运行该方法。

  6. Let's start to implement the JSX for the QuestionPage component by adding a container element for the page and the question title:

    ``cs <Page> <div css={css background-color: white; padding: 15px 20px 20px 20px; border-radius: 4px; border: 1px solid ${gray6}; box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.16); `}

    <div
      css={css`
        font-size: 19px;
        font-weight: bold;
        margin: 10px 0px 5px;
      `}
    
      {question === null ? '' : question.title}
    </div>
    

    ```

    question状态设置完成之前,我们不会呈现标题。获取问题时,question状态为空,如果找不到问题,则状态为空。请注意,我们使用三重等于(===来检查question变量是否为null而不是双等于(==

    重要提示

    当使用三重相等(===时,我们检查严格相等。这意味着我们要比较的类型和值必须相同。使用双等于(==时,不检查类型。通常,使用三重相等(===来执行严格的相等检查是一种良好的做法。

    如果我们查看 running 应用,我们将看到问题标题已呈现在一张漂亮的白卡中:

    Figure 5.5 – Question page title

    图 5.5–问题页面标题

  7. Let's now implement the question content:

    cs <Page> <div ... > <div ... > {question === null ? '' : question.title} </div> {question !== null && ( <React.Fragment> <p css={css` margin-top: 0px; background-color: white; `} > {question.content} </p> </React.Fragment> )} </div> </Page>

    因此,如果从获取的数据中设置了question状态,我们将在 JSX 中显示问题的输出内容。请注意,这是嵌套在Fragment组件中的。这是做什么用的?

    重要提示

    在 React 中,组件只能返回单个元素。此规则适用于条件呈现逻辑,其中只能呈现单个父元素。ReactFragment允许我们绕过此规则,因为我们可以在其中嵌套多个元素,而无需创建 DOM 节点。

    如果我们尝试在短路操作符之后返回两个元素,我们可以看到Fragment解决的问题:

    Figure 5.6 – Reason for react fragment

    图 5.6–React 碎片的原因

  8. 让我们在Fragment

    cs {question !== null && ( <React.Fragment> <p ... > {question.content} </p> <div css={css` font-size: 12px; font-style: italic; color: ${gray3}; `} > {`Asked by ${question.userName} on ${question.created.toLocaleDateString()} ${question.created.toLocaleTimeString()}`} </div> </React.Fragment> )}

    中添加提问时间和提问者

现在,问题的所有细节将在问题页面上的跑步应用中呈现在一张漂亮的白卡中:

Figure 5.7 – Question page

图 5.7–问题页面

因此,问题页面现在看起来不错。我们还没有给出任何答案,所以让我们接下来看看。

创建应答器列表组件

按照以下步骤创建一个将呈现答案列表的组件:

  1. Create a new file called AnswerList.tsx with the following import statements:

    cs /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; import React from 'react'; import { AnswerData } from './QuestionsData'; import { Answer } from './Answer'; import { gray5 } from './Styles';

    因此,我们将使用无序列表来呈现没有要点的答案。我们已经引用了一个组件Answer,稍后我们将在这些步骤中创建它。

  2. 让我们定义接口,使其包含用于答案数组的data道具:

    cs interface Props { data: AnswerData[]; }

  3. Let's create the AnswerList component, which outputs the answers:

    ``cs export const AnswerList = ({ data }: Props) => ( <ul css={css list-style: none; margin: 10px 0 0 0; padding: 0; `}

    {data.map(answer => (
      <li
        css={css`
          border-top: 1px solid ${gray5};
        `}
        key={answer.answerId}
    
        <Answer data={answer} />
      </li>
    ))}
    

    ); ```

    每个答案都输出到Answer组件中的无序列表中,我们将在下一步实现该组件。

  4. 让我们继续往下看,通过创建一个名为Answer.tsx的文件并使用以下import语句来实现组件:

    cs /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; import React from 'react'; import { AnswerData } from './QuestionsData'; import { gray3 } from './Styles';

  5. Answer组件的接口将只包含应答数据:

    cs interface Props { data: AnswerData; }

  6. 现在,Answer组件将简单地呈现答案内容,以及回答者和回答时间:

    ``cs export const Answer = ({ data }: Props) => ( <div css={css padding: 10px 0px; `}

    <div
      css={css`
        padding: 10px 0px;
        font-size: 13px;
      `}
    
      {data.content}
    </div>
    <div
      css={css`
        font-size: 12px;
        font-style: italic;
        color: ${gray3};
      `}
    
      {`Answered by ${data.userName} on
      ${data.created.toLocaleDateString()} 
      ${data.created.toLocaleTimeString()}`}
    </div>
    

    ); ```

  7. 现在我们回到QuestionPage.tsx并导入AnswerList

    cs import { AnswerList } from './AnswerList';

  8. Now, we can add AnswerList to the Fragment element:

    cs {question !== null && ( <React.Fragment> <p ... > {question.content} </p> <div ... > {`Asked by ${question.userName} on ${question.created.toLocaleDateString()} ${question.created.toLocaleTimeString()}`} </div> <AnswerList data={question.answers} /> </React.Fragment> )}

    如果我们查看questions/1问题页面上的 running 应用,我们会看到答案呈现得很好:

Figure 5.8 – Question page with answers

图 5.8–带答案的问题页面

这就完成了我们在本章问题页面上需要做的工作。但是,我们需要允许用户提交问题的答案,我们将在第 6 章处理表单中介绍。

接下来,我们将了解如何使用 React Router 处理查询参数。

使用查询参数

查询参数是允许将其他参数传递到路径的 URL 的一部分。例如,/search?criteria=typescript有一个名为criteria的查询参数,其值为typescript。查询参数有时称为搜索参数。

在本节中,我们将在搜索页面上实现一个名为criteria的查询参数,该参数将驱动搜索。我们将在此过程中实现搜索页面。让我们执行以下步骤来执行此操作:

  1. We are going to start in QuestionsData.ts by creating a function to simulate a search via a web request:

    cs export const searchQuestions = async ( criteria: string, ): Promise<QuestionData[]> => { await wait(500); return questions.filter( q => q.title.toLowerCase() .indexOf(criteria.toLowerCase()) >= 0 || q.content.toLowerCase() .indexOf(criteria.toLowerCase()) >= 0, ); };

    因此,函数使用数组filter方法,并将标准与问题标题或内容的任何部分相匹配。

  2. Let's import this function, along with the other items we need, into SearchPage.tsx. Place these statements above the existing import statements in SearchPage.tsx:

    cs /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react' import { useSearchParams } from 'react-router-dom'; import { QuestionList } from './QuestionList'; import { searchQuestions, QuestionData } from './QuestionsData';

    React 路由的useSearchParams钩子用于访问查询参数。

  3. Add an explicit return statement to the SearchPage component and destructure the object from useSearchParams that contains the search parameters:

    cs export const SearchPage = () => { const [searchParams] = useSearchParams(); return ( <Page title="Search Results">{null}</Page> ); };

    useSearchParams钩子返回一个包含两个元素的数组。第一个元素是包含搜索参数的对象,第二个元素是更新查询参数的函数。我们只分解了代码中的第一个元素,因为我们不需要更新这个组件中的查询参数。

  4. 我们现在将创建一些状态来保存搜索中匹配的问题:

    cs export const SearchPage = () => { const [searchParams] = useSearchParams(); const [ questions, setQuestions, ] = React.useState<QuestionData[]>([]); return … };

  5. Next, we are going to get the criteria query parameter value:

    cs export const SearchPage = () => { const [searchParams] = useSearchParams(); const [ questions, setQuestions, ] = React.useState<QuestionData[]>([]); const search = searchParams.get('criteria') || ""; return … };

    searchParams对象包含一个get方法,可用于获取查询参数的值。

  6. 我们将在组件第一次呈现时以及当search变量使用useEffect钩子

    cs const search = searchParams.get('criteria') || ''; React.useEffect(() => { const doSearch = async (criteria: string) => { const foundResults = await searchQuestions( criteria, ); setQuestions(foundResults); }; doSearch(search); }, [search]);

    更改时调用搜索 7. 我们现在可以在页面标题下呈现搜索条件。将{null}替换为突出显示的代码:

    cs <Page title="Search Results"> {search && ( <p css={css` font-size: 16px; font-style: italic; margin-top: 0px; `} > for "{search}" </p> )} </Page>

  7. The last task is to use the QuestionList component to render the questions that are returned from the search:

    cs <Page title="Search Results"> {search && ( <p ... > for "{search}" </p> )} <QuestionList data={questions} /> </Page>

    我们的QuestionList组件现在用于具有不同数据源的主页和搜索页面。这个组件的可重用性已经成为可能,因为我们遵循了第 3 章中简要提到的容器模式,即【React 和 TypeScript 入门】

  8. 在 running 应用中,在浏览器中输入/search?criteria=type。搜索将被调用,结果将按预期呈现:

Figure 5.9 – Search results

图 5.9–搜索结果

因此,React 路由中的useSearchParams钩子使得与查询参数的交互变得轻松愉快。

第 6 章处理表单中,我们将把标题中的搜索框连接到我们的搜索表单。

在下一节中,我们将学习如何按需加载组件。

延迟装载路线

目前,我们应用的所有 JavaScript 都是在应用首次加载时加载的。这对于小型应用来说很好,但对于大型应用来说,这可能会对性能产生负面影响。可能有一些在应用中很少使用的大页面,我们希望加载 JavaScript 以便按需使用。这个过程被称为延迟加载。

我们将在本节中延迟加载 ask 页面。延迟加载的用处不大,因为这可能是我们应用中的一个流行页面,但它将帮助我们了解如何实现这一点。让我们执行以下步骤:

  1. 首先,我们将向AskPage.tsx

    cs export const AskPage = () => <Page title="Ask a question" />; export default AskPage;

    中的AskPage组件添加一个默认导出 2. 打开App.tsx并删除AskPage组件的当前import语句。 3. 为 React:

    cs import React from 'react';

    添加一条import语句 4. Add a new import statement for the AskPage component after all the other import statements:

    cs const AskPage = React.lazy(() => import('./AskPage'));

    这是文件中最后一条import语句,这一点很重要,因为否则,ESLint 可能会抱怨它下面的import语句位于模块主体中。

    React 中的lazy函数允许我们将动态导入作为常规组件呈现。动态导入为请求的模块返回一个承诺,该承诺在获取、实例化和评估模块后解析。

  2. So, the AskPage component is being loaded on demand now, but the App component is expecting this component to be loaded immediately. If we enter the ask path in the browser's address bar and press the Enter key, we may receive an error with a clue of how to resolve this:

    Figure 5.10 – No Suspense component warning

    图 5.10–无悬念组件警告

  3. As suggested by the error message, we are going to use the Suspense component from React to resolve this issue. For ask Route, we wrap the Suspense component around the AskPage component:

    cs <Route path="ask" element={ <React.Suspense fallback={ <div css={css` margin-top: 100px; text-align: center; `} > Loading... </div> } > <AskPage /> </React.Suspense> } />

    Suspense``fallback道具允许我们在AskPage加载时渲染组件。所以,我们正在呈现加载。。。加载AskPage组件时的消息。

  4. 让我们转到主页上的 running 应用,按F12打开浏览器开发者工具。

  5. On the Network tab, let's clear the previous network activity by clicking the no entry icon. Then, if we click the Ask a question button, we will see confirmation that additional JavaScript has been downloaded in order to render the AskPage component:

    Figure 5.11 – AskPage component loaded on demand

    图 5.11–按需加载的 AskPage 组件

  6. The AskPage component loads so fast that we are unlikely to see the Loading component being rendered. In the Chrome browser developer tools, there is an option to simulate a Slow 3G network in the Network tab:

    Figure 5.12 – Slow 3G option

    图 5.12–慢速 3G 选项

  7. 如果我们打开此功能,按主页上的F5再次加载应用,然后单击提问按钮,我们将看到加载。。。暂时呈现的消息:

Figure 5.13 – Suspense fallback

图 5.13–悬念回退

在本例中,AskPage组件的大小很小,因此这种方法实际上不会对性能产生积极影响。但是,按需加载较大的组件确实可以提高性能,特别是在连接速度较慢的情况下。

总结

React Router 为我们提供了一套全面的组件,用于管理应用中页面之间的导航。我们了解到,顶级组件是BrowserRouter,它在其下方的Routes组件中查找Route组件,我们在其中定义应为某些路径呈现哪些组件。Route组件中与当前浏览器位置最匹配的path是渲染的组件。

useParams钩子让我们访问路由参数,useSearchParams钩子让我们访问查询参数。这些挂钩可用于组件树中BrowserRouter下的任何 React 组件。

我们了解到,Reactlazy功能及其Suspense组件可用于用户很少使用的大型组件,以便按需加载它们。这有助于提高应用启动时间的性能。

在下一章中,我们将继续构建问答应用的前端,这一次将重点放在实现表单上。

问题

以下问题将巩固您在本章中刚刚学到的知识:

  1. We have the following routes defined:

    cs <BrowserRouter> <Routes> <Route path="search" element={<SearchPage/>} /> <Route path="" element={<HomePage/>} /> </Routes> </BrowserRouter>

    回答以下问题:

    • 在浏览器中输入/位置时,将呈现哪些组件?
    • 当在浏览器中输入/search位置时,情况如何?
    • 在我们的问答应用中,我们需要一个/login路径来导航到登录页面,以及/signin路径。我们如何实施这一点?
    • We have the following routes defined:

    cs <BrowserRouter> <Routes> <Route path="search" element={<SearchPage/>} /> <Route path="" element ={<HomePage/>} /> <Route path="*" element={<NotFoundPage/>} /> </Routes> </BrowserRouter>

    在浏览器中输入/signin位置时,将呈现哪个组件?

  2. We have the following route defined:

    cs <Route path="users/:userId" component={UserPage} />

    如何在组件中引用userId路由参数?

  3. 如何从/users?id=1等路径获取id查询参数的值?

  4. We have an option that navigates to another page when clicked. The JSX for this option is as follows:

    cs <a href="/products">Products</a>

    此时,导航会发出服务器请求。我们如何改变这一点,使导航只在浏览器中进行?

  5. 我们需要在流程完成时以编程方式导航到/success路径。我们怎样才能做到这一点?

答案

  1. 当浏览器位置为/时呈现HomePage组件,当浏览器位置为/search时呈现SearchPage组件。
  2. 为了使/login的路径能够呈现登录页面,我们可以定义一个额外的Route组件,如下所示:

    cs <Route path="signin" element={<SignInPage />} /> <Route path="login" element={<SignInPage />} />

  3. 将呈现NotFoundPage组件。

  4. 我们可以使用useParams钩子参照userId路由参数如下:

    cs const { userId } = useParams();

  5. 我们可以使用useSearchParams钩子引用id查询参数,如下所示:

    cs const [searchParams] = useSearchParams(); const id = searchParams.get('id');

  6. 可以使用Link组件,以便只在客户端

    cs <Link to="products">Products</Link>

    上进行导航 7. In order to programmatically navigate, we first need to get a function from the useNavigate hook that can perform the navigation:

    cs const navigate = useNavigate();

    然后,我们可以在代码中的适当位置使用此功能导航到/success路径:

    cs navigate('success');

进一步阅读

以下是一些有用的链接,可用于了解有关本章所涵盖主题的更多信息: