七、将挂钩用于路由

在上一章中,我们学习了如何使用挂钩请求资源。我们首先使用 State/Reducer 和 Effect 挂钩实现请求资源。然后,我们了解了axiosreact-request-hook库。

在本章中,我们将创建多个页面,并在我们的应用中实现路由。路由在几乎所有应用中都很重要。为了实现路由,我们将学习如何使用 Navi 库,一个基于挂钩的导航系统。最后,我们还将学习动态链接,以及如何使用挂钩访问路由信息。

本章将介绍以下主题:

  • 创建多个页面
  • 实现路由
  • 使用路由挂钩

技术要求

应该已经安装了 Node.js 的最新版本(v11.12.0 或更高版本)。Node.js 的npm包管理器也需要安装。

本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter07

请查看以下视频以查看代码的运行情况:

http://bit.ly/2Mm9yoC

Please note that it is highly recommended that you write the code on your own. Do not simply run the code examples that have been provided. It is important that you write the code yourself in order to be able to learn and understand properly. However, if you run into any issues, you can always refer to the code example.

现在,让我们从这一章开始。

创建多个页面

目前,我们的博客应用是一个所谓的单页应用。然而,大多数大型应用都由多个页面组成。在博客应用中,我们至少希望每个博客文章都有一个单独的页面。

在设置路由之前,我们需要创建要呈现的各种页面。在我们的博客应用中,我们将定义以下页面:

  • 主页,将显示所有帖子的列表
  • 一个帖子页面,将显示一篇帖子

所有页面将显示一个HeaderBar,它呈现HeaderUserBarChangeThemeCreatePost组件。我们现在开始为HeaderBar创建一个组件。之后,我们将实现页面组件。

创建 HeaderBar 组件

首先,我们将把App组件的一些内容重构为HeaderBar组件。HeaderBar组件将包含我们希望在每个页面上显示的所有内容:HeaderUserBarChangeThemeCreatePost组件。

让我们开始创建HeaderBar组件:

  1. 新建文件夹:src/pages/
  2. 创建一个新文件src/pages/HeaderBar.js,导入React(使用useContext挂钩),并在那里定义组件。它将接受setTheme功能作为道具:
import React, { useContext } from 'react'

export default function HeaderBar ({ setTheme }) {
   return (
        <div>
        </div>
    )
}
  1. 现在,从src/App.js组件中剪切以下代码,并将其插入HeaderBar组件的<div>标记之间:
            <Header text="React Hooks Blog" />
            <ChangeTheme theme={theme} setTheme={setTheme} />
            <br />
            <React.Suspense fallback={"Loading..."}>
                <UserBar />
            </React.Suspense>
            <br />
            {user && <CreatePost />}
  1. 另外,从src/App.js中剪切以下导入语句(并调整路径),并将它们插入src/pages/HeaderBar.js文件的开头,插入import React from 'react'语句之后:
import CreatePost from '../post/CreatePost'
import UserBar from '../user/UserBar'
import Header from '../Header'
import ChangeTheme from '../ChangeTheme'
  1. 另外,导入ThemeContextStateContext
import { ThemeContext, StateContext } from '../contexts'
  1. 然后,为themestate定义两个上下文挂钩,并将user变量从src/pages/HeaderBar.js中的state对象中拉出,因为我们需要它进行条件检查,以确定是否应该呈现CreatePost组件:
export default function HeaderBar ({ setTheme }) { const theme = useContext(ThemeContext)

    const { state } = useContext(StateContext)
    const { user } = state 
    return (
  1. 现在我们导入src/App.js中的HeaderBar组件:
import HeaderBar from './pages/HeaderBar'
  1. 最后,我们在src/App.js中呈现HeaderBar组件:
        <div style={{ padding: 8 }}>
            <HeaderBar setTheme={setTheme} />
            <hr />

现在,我们为HeaderBar提供了一个单独的组件,它将显示在所有页面上。接下来,我们继续创建HomePage组件。

创建主页组件

现在,我们将从PostList组件和与帖子相关的资源挂钩创建HomePage组件。同样,我们将重构src/App.js,以创建一个新组件。

让我们开始创建HomePage组件:

  1. 创建一个新文件src/pages/HomePage.js,导入带有useEffectuseContext挂钩的React,并在那里定义组件。我们还定义了一个上下文挂钩并拉出了state对象和dispatch函数:
import React, { useEffect, useContext } from 'react'
import { StateContext } from '../contexts'

export default function HomePage () {
    const { state, dispatch } = useContext(StateContext)
    const { error } = state

    return (
        <div>
        </div>
    )
}
  1. 然后,从src/App.js中剪切以下导入语句(并调整路径),将它们添加到src/pages/HomePage.js中的import React from 'react'语句之后:
import { useResource } from 'react-request-hook'
import PostList from '../post/PostList'
  1. 接下来,从src/App.js中剪切以下挂钩定义,并将其插入HomePage函数的return语句之前:
    const [ posts, getPosts ] = useResource(() => ({
        url: '/posts',
        method: 'get'
    }))
    useEffect(getPosts, [])
    useEffect(() => {
        if (posts && posts.error) {
            dispatch({ type: 'POSTS_ERROR' })
        }
        if (posts && posts.data) {
            dispatch({ type: 'FETCH_POSTS', posts: posts.data.reverse() })
        }
    }, [posts])
  1. 现在,从src/App.js中剪切以下呈现代码,并将其插入src/pages/HomePage.js<div>标记之间:
            {error && <b>{error}</b>}
            <PostList />
  1. 然后,导入src/App.js中的HomePage组件:
import HomePage from './pages/HomePage'
  1. 最后,呈现<hr />标签下方的HomePage组件:
            <hr />
            <HomePage />

现在,我们已经成功地将当前代码重构为一个HomePage组件。接下来,我们继续创建PostPage组件。

创建后期组件

我们现在将定义一个新的页面组件,在这个组件中,我们将只从 API 获取一篇文章并显示它。

现在让我们开始创建PostPage组件:

  1. 创建一个新的src/pages/PostPage.js文件。
  2. 进口ReactuseEffectuseResource挂钩及Post组件:
import React, { useEffect } from 'react'
import { useResource } from 'react-request-hook'

import Post from '../post/Post'
  1. 现在,定义PostPage组件,它将接受 postid作为 prop:
export default function PostPage ({ id }) {
  1. 这里,我们定义了一个资源挂钩,它将获取相应的post对象。我们将id作为依赖项传递给 Effect 挂钩,以便在id更改时重新获取资源:
    const [ post, getPost ] = useResource(() => ({
        url: `/posts/${id}`,
        method: 'get'
    }))
    useEffect(getPost, [id])
  1. 最后,我们呈现Post组件:
    return (
        <div>
            {(post && post.data)
                ? <Post {...post.data} />
                : 'Loading...'
            }
            <hr />
        </div>
    )
}

我们现在也有一个单独的页面为单一的职位。

测试延迟

为了测试新页面,我们将用PostPage组件替换src/App.js中的HomePage组件,如下所示:

  1. 导入src/App.js中的PostPage组件:
import PostPage from './pages/PostPage'
  1. 现在,将HomePage组件更换为PostPage组件:
            <PostPage id={'react-hooks'} />

正如我们所看到的,现在只有一个帖子,React Hooks 帖子被渲染。

示例代码

示例代码可在Chapter07/chapter7_1文件夹中找到。

只需运行npm install安装所有依赖项,并启动npm start应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

实现路由

我们将使用 Navi 库进行路由。Navi 支持 React 本机的 React Suspense、Hooks 和 error boundary API,这使得它非常适合通过使用 Hooks 来实现路由。要实现路由,我们首先要从上一节中定义的页面定义路由。最后,我们将定义从主页到相应文章页面的链接,以及从这些页面返回主页的链接。

在本章末尾,我们将通过实现路由挂钩来扩展路由功能。

定义路线

实现路由的第一步是安装navireact-navi库。然后,我们定义路由。请按照给定的步骤执行此操作:

  1. 首先,我们必须使用npm安装库:
>npm install --save navi react-navi
  1. 然后在src/App.js中,我们从 Navi 库导入RouterView组件以及mountroute函数:
import { Router, View } from 'react-navi'
import { mount, route } from 'navi'
  1. 确保HomePage组件已导入:
import HomePage from './pages/HomePage'
  1. 现在,我们可以使用mount函数定义routes对象:
const routes = mount({
  1. 在此函数中,我们从主路线开始定义路线:
    '/': route({ view: <HomePage /> }),
  1. 接下来,我们为一篇文章定义路由,这里我们使用 URL 参数(:id和一个函数来动态创建view
    '/view/:id': route(req => {
        return { view: <PostPage id={req.params.id} /> }
    }),
})
  1. 最后,我们用<Router>组件包装呈现的代码,并用<View>组件替换<PostPage>组件,以便动态呈现当前页面:
 <Router routes={routes}>
            <div style={{ padding: 8 }}>
                <HeaderBar setTheme={setTheme} />
                <hr />
 <View />
            </div>
 </Router>

现在,如果我们转到http://localhost:3000,我们可以看到所有帖子的列表,当我们转到http://localhost:3000/view/react-hooks时,我们可以看到一个帖子:React Hooks post。

定义链接

现在,我们将定义每个帖子到相应单个帖子页面的链接,然后从帖子页面返回主页。这些链接将用于访问我们应用中定义的各种路线。首先,我们将定义从主页到单个帖子页面的链接。接下来,我们将定义从单个 post 页面到主页的链接。

定义到帖子的链接

我们首先缩短列表中的 postcontent,并定义从PostList到相应 post 页面的链接。为此,我们必须定义从主页上的PostList到特定帖子页面的静态链接。

现在让我们定义这些链接:

  1. 编辑src/post/Post.js,从react-navi导入Link组件:
import { Link } from 'react-navi'
  1. 然后,我们将在Post组件中添加两个新道具:idshort,当我们想要显示文章的缩短版本时,这两个道具将被设置为true。稍后,我们将在PostList组件中将short设置为true
function Post ({ id, title, content, author, short = false }) {
  1. 接下来,我们将添加一些逻辑,以便在列出时将 postcontent字符修剪为30字符:
    let processedContent = content
    if (short) {
        if (content.length > 30) {
            processedContent = content.substring(0, 30) + '...'
        }
    }
  1. 现在,我们可以显示processedContent值而不是content值,并显示Link以查看完整帖子:
            <div>{processedContent}</div>
 {short &&
 <div>
 <br />
 <Link href={`/view/${id}`}>View full post</Link>
 </div>
 }
  1. 最后,我们在PostList组件中将short属性设置为true。编辑src/post/PostList.js,并调整以下代码:
                <Post {...p} short={true} />

现在我们可以看到,主页上的每篇文章都被裁剪为30个字符,并且有一个指向相应单个文章页面的链接:

Displaying a link in the PostList

正如我们所看到的,路由非常简单。现在,每篇文章都有一个指向其相应完整文章页面的链接。

定义指向主页的链接

现在,我们只需要一种方法,从一个帖子页面返回主页。我们将重复我们之前所做的类似过程。现在让我们定义返回主页的链接:

  1. 编辑src/pages/PostPage.js,并在此处导入Link组件:
import { Link } from 'react-navi'
  1. 然后,在显示文章之前,将新链接插入主页:
    return (
        <div>
            <div><Link href="/">Go back</Link></div>
  1. 进入页面后,我们现在可以使用返回链接返回主页:

Displaying a link on the single post page

现在,我们的应用还提供了返回主页的方法。

调整 CREATE_POST 操作

之前,我们在创建新帖子时发送了一个CREATE_POST操作。但是,此操作不包含帖子id,这意味着指向新创建帖子的链接将不起作用。

我们现在要调整代码,将 postid传递给CREATE_POST动作:

  1. 编辑src/post/CreatePost.js,导入useEffect挂钩:
import React, { useState, useContext, useEffect } from 'react'
  1. 接下来,调整现有资源挂钩,在 post 创建完成后拉出post对象:
    const [ post, createPost ] = useResource(({ title, content, author }) => ({
  1. 现在,我们可以在资源挂钩之后创建一个新的效果挂钩,一旦 create post 请求的结果可用,我们就可以调度CREATE_POST操作:
    useEffect(() => {
        if (post && post.data) {
            dispatch({ type: 'CREATE_POST', ...post.data })
        }
    }, [post])
  1. 接下来,我们删除对handleCreate处理函数中dispatch函数的调用:
    function handleCreate () {
        createPost({ title, content, author: user })
 dispatch({ type: 'CREATE_POST', title, content, author: user })
    }
  1. 最后编辑src/reducers.js,对postsReducer进行如下调整:
function postsReducer (state, action) {
    switch (action.type) {
        case 'FETCH_POSTS':
            return action.posts

        case 'CREATE_POST':
            const newPost = { title: action.title, content: action.content, author: action.author, id: action.id }
            return [ newPost, ...state ]

现在,指向新创建的帖子的链接工作正常,因为id值被添加到插入的post对象中。

示例代码

示例代码可在Chapter07/chapter7_2文件夹中找到。

只需运行npm install安装所有依赖项,并启动npm start应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

使用路由挂钩

在使用navireact-navi实现基本路由之后,我们现在将使用react-navi提供的路由挂钩实现更高级的用例。路由挂钩可以用来使路由更加动态。例如,允许从其他挂钩导航到不同的路线。此外,我们可以使用挂钩访问组件中所有与路由相关的信息。

Navi 挂钩概述

首先,我们将了解 Navi 库提供的三个挂钩:

  • useNavigation挂钩
  • useCurrentRoute挂钩
  • useLoadingRoute挂钩

使用导航挂钩

useNavigation挂钩具有以下签名:

const navigation = useNavigation()

返回 Navi 的navigation对象,该对象包含以下功能,用于管理应用的导航状态:

  • extractState():返回window.history.state的当前值;这在处理服务器端渲染时非常有用。
  • getCurrentValue():返回当前 URL 对应的Route对象。
  • getRoute():返回对当前 URL 对应的满载Route对象的承诺。只有在Route对象完全加载后,承诺才会解决。
  • goBack():返回一页;这类似于按浏览器的“后退”按钮的工作方式。
  • navigate(url, options):使用提供的选项(bodyheadersmethodreplacestate导航到提供的 URL。有关选项的更多信息,请参见官方 Navi 文档:https://frontarm.com/navi/en/reference/navigation/#navigationnavigate.

useCurrentRoute 挂钩

useCurrentRoute挂钩具有以下签名:

const route = useCurrentRoute()

它返回最新的非繁忙路线,其中包含 Navi 知道的有关当前页面的所有信息:

  • data:包含所有data块的合并值。
  • title:包含应在document.title上设置的title值。
  • url:包含当前路线的信息,如hrefqueryhash等。
  • views:包含将在路由视图中呈现的组件或元素数组。

useLoadingRoute 挂钩

useLoadingRoute挂钩具有以下签名:

const loadingRoute = useLoadingRoute()

它返回当前正在获取的页面的Route对象。如果当前未提取页面,则输出undefined。该对象看起来与useCurrentRoute挂钩的Route对象相同。

程序导航

首先,我们将使用useNavigation挂钩实现编程导航。我们希望在创建新帖子后自动重定向到相应的帖子页面。

让我们使用挂钩在CreatePost组件中实现编程导航:

  1. 编辑src/post/CreatePost.js,并在此处导入useNavigation挂钩:
import { useNavigation } from 'react-navi'
  1. 现在,在现有资源挂钩之后定义一个导航挂钩:
    const navigation = useNavigation()
  1. 最后,一旦 create post 请求的结果可用,我们将 Effect Hook 调整为调用navigation.navigate()
    useEffect(() => {
        if (post && post.data) {
            dispatch({ type: 'CREATE_POST', ...post.data })
            navigation.navigate(`/view/${post.data.id}`)
        }
    }, [post])

如果我们现在创建一个新的post对象,我们可以看到在按下创建按钮后,我们会自动重定向到相应帖子的页面。我们现在可以继续使用挂钩访问路由信息。

访问路线信息

接下来,我们将使用useCurrentRoute挂钩访问有关当前路由/URL 的信息。我们将使用这个挂钩实现一个页脚,它将显示当前路由的href值。

现在让我们开始实现页脚:

  1. 首先,我们为页脚创建一个新组件。新建一个src/pages/FooterBar.js文件,从react-navi导入ReactuseCurrentRoute挂钩:
import React from 'react'
import { useCurrentRoute } from 'react-navi'
  1. 然后,我们定义一个新的FooterBar组件:
export default function FooterBar () {
  1. 我们使用useCurrentRoute挂钩,拉出url对象,可以在页脚显示当前href值:
    const { url } = useCurrentRoute()
  1. 最后,我们在页脚中呈现一个指向当前href值的链接:
    return (
        <div>
            <a href={url.href}>{url.href}</a>
        </div>
    )
}

现在,例如,当我们打开一个帖子页面时,我们可以在页脚中看到当前帖子的href值:

Displaying a footer with the current href value

正如我们所看到的,我们的页脚工作正常,它总是显示当前页面的href值。

示例代码

示例代码可在Chapter07/chapter7_3文件夹中找到。

只需运行npm install安装所有依赖项,并启动npm start应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

总结

在本章中,我们首先为我们的博客定义了两个页面:主页和一个用于单个帖子的页面。我们还为HeaderBar创建了一个组件。之后,我们通过定义路由、单个帖子的链接以及返回主页的链接来实现路由。最后,我们使用路由挂钩在创建新帖子时实现动态导航,并实现一个显示当前 URL 的页脚。

路由非常重要,几乎在每个应用中都使用。我们现在知道如何定义单独的页面以及如何在它们之间链接。此外,我们还学习了如何使用挂钩在页面之间动态导航。我们还学习了如何使用挂钩访问路由信息,以获得更高级的用例。

Navi 库还可以做很多事情。然而,这本书的重点是挂钩,所以 Navi 的大部分功能都超出了范围。例如,我们可以使用 Navi 获取数据、实现错误页面(例如 404 页面)、延迟加载和组合路由。请随意阅读 Navi 官方文档中的这些功能。

在下一章中,我们将学习 React 社区提供的各种挂钩:用于输入处理、响应设计、实现撤销/重做,以及使用挂钩实现各种数据结构和 React 生命周期方法。我们还将学习如何找到社区提供的更多挂钩。

问题

为了重述我们在本章学到的知识,请尝试回答以下问题:

  1. 为什么我们需要定义单独的页面?
  2. 如何使用 Navi 库定义管线?
  3. 我们如何使用 URL 参数定义路由?
  4. 如何使用 Navi 定义静态链接?
  5. 我们如何实现动态导航?
  6. 哪个挂钩用于访问当前路由的路由信息?
  7. 哪个挂钩用于访问当前装载路线的路线信息?

进一步阅读

如果您对我们在本章中所学概念的更多信息感兴趣,请查看 Navi 图书馆的官方文档:https://frontarm.com/navi/en/