跳到内容

a

React-router

本课程第七章节的练习与之前的练习有一些不同。在这一章和下一章,像往常一样,有与本章理论相关的练习

除了这一章和下一章的练习外,还有一系列的练习,在这些练习中,我们将通过扩展我们在第四和第五章节所做的Bloglist应用来复习我们在整个课程中所学到的知识。

Application navigation structure

在第六章节之后,我们将回到没有Redux的React。

对于网络应用来说,有一个导航条是很常见的,它可以切换应用的视图。

我们的应用可以有一个主页面

fullstack content

以及显示笔记和用户信息的独立页面。

fullstack content

在一个老式网络应用中,改变应用显示的页面将由浏览器向服务器发出HTTP GET请求并渲染代表返回的视图的HTML来完成。

在单页应用中,实际上我们总是在同一个页面上。浏览器运行的Javascript代码创造了一个不同 "页面 "的假象。如果在切换视图时发出HTTP请求,它们只是为了获取JSON格式的数据,而新的视图可能需要它来显示。

导航栏和一个包含多个视图的应用,使用React很容易实现。

这里是一种方法。

import React, { useState }  from 'react'
import ReactDOM from 'react-dom/client'

const Home = () => (
  <div> <h2>TKTL notes app</h2> </div>
)

const Notes = () => (
  <div> <h2>Notes</h2> </div>
)

const Users = () => (
  <div> <h2>Users</h2> </div>
)

const App = () => {
  const [page, setPage] = useState('home')

 const toPage = (page) => (event) => {
    event.preventDefault()
    setPage(page)
  }

  const content = () => {
    if (page === 'home') {
      return <Home />
    } else if (page === 'notes') {
      return <Notes />
    } else if (page === 'users') {
      return <Users />
    }
  }

  const padding = {
    padding: 5
  }

  return (
    <div>
      <div>
        <a href="" onClick={toPage('home')} style={padding}>
          home
        </a>
        <a href="" onClick={toPage('notes')} style={padding}>
          notes
        </a>
        <a href="" onClick={toPage('users')} style={padding}>
          users
        </a>
      </div>

      {content()}
    </div>
  )
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />, document.getElementById('root'))

每个视图都作为自己的组件来实现。我们在名为page的应用状态中存储视图组件信息。这个信息告诉我们应该在菜单栏下面显示哪个代表视图的组件。

然而,这个方法并不是很理想。正如我们从图片上看到的,即使有时我们在不同的视图中,地址也保持不变。每个视图最好都有自己的地址,比如说,为了使书签成为可能。返回按钮在我们的应用中也没有起到预期的作用,这意味着返回不会把你移到先前显示的应用的视图,而是移到完全不同的地方。如果应用进一步扩大,例如我们想为每个用户和笔记添加单独的视图,那么这种自制的routing,也就是应用的导航管理,就会变得过于复杂。

React Router

幸运的是,React有React Router库,它为管理React应用中的导航提供了一个很好的解决方案。

让我们把上面的应用改为使用React Router。首先,我们用以下命令安装React Router

npm install react-router-dom

通过改变应用,启用React Router提供的路由,如下所示。

import {
  BrowserRouter as Router,
  Routes, Route, Link
} from "react-router-dom"

const App = () => {

  const padding = {
    padding: 5
  }

  return (
    <Router>
      <div>
        <Link style={padding} to="/">home</Link>
        <Link style={padding} to="/notes">notes</Link>
        <Link style={padding} to="/users">users</Link>
      </div>

      <Routes>
        <Route path="/notes" element={<Notes />} />
        <Route path="/users" element={<Users />} />
        <Route path="/" element={<Home />} />
      </Routes>

      <div>
        <i>Note app, Department of Computer Science 2022</i>
      </div>
    </Router>
  )
}

路由,或者说在浏览器中基于url的组件的有条件渲染,是通过将组件作为Router组件的子代,也就是在Router标签中使用。

注意,尽管该组件被称为Router,我们实际上是在谈论BrowserRouter,因为这里的导入是通过重命名导入的对象发生的。

import {
  BrowserRouter as Router,  Routes, Route, Link
} from "react-router-dom"

根据手册

BrowserRouter是一个Router,使用HTML5历史API(pushState、replaceState和popState事件)来保持你的UI与URL同步。

通常情况下,当地址栏中的URL发生变化时,浏览器会加载一个新页面。然而,在HTML5历史API的帮助下,BrowserRouter使我们能够在React应用中使用浏览器地址栏中的URL进行内部 "路由"。因此,即使地址栏中的URL发生变化,页面的内容也只是使用Javascript进行操作,浏览器不会从服务器上加载新的内容。使用后退和前进的动作,以及做书签,仍然像在一个传统的网页上那样合乎逻辑。

在路由器内部,我们定义了链接,在链接组件的帮助下修改地址栏。例如。

<Link to="/notes">notes</Link>

在应用中创建一个文本为notes的链接,当点击时将地址栏中的URL改为/notes

基于浏览器的URL渲染的组件是在组件Route的帮助下定义的。例如。

<Route path="/notes" element={<Notes />} />

定义了,如果浏览器的地址是/notes,我们就渲染Notes组件。

我们用一个Routes组件来包装要根据网址渲染的组件

<Routes>
  <Route path="/notes" element={<Notes />} />
  <Route path="/users" element={<Users />} />
  <Route path="/" element={<Home />} />
</Routes>

Routes的作用是渲染第一个路径与浏览器地址栏中的网址相匹配的组件。

Parameterized route

让我们来看看前面例子中稍加修改的版本。这个例子的完整代码可以在这里找到。

该应用现在包含五个不同的视图,其显示由路由器控制。除了前面例子中的组件(HomeNotesUsers),我们还有代表登录视图的Login和代表单个笔记视图的Note

Home and Users are unchanged from the previous exercise. Notes is a bit more complicated. It renders the list of notes passed to it as props in such a way that the name of each note is clickable.

fullstack content

点击名字的能力是通过组件Link实现的,点击id为3的笔记的名字会触发一个事件,将浏览器的地址变成notes/3

const Notes = ({notes}) => (
  <div>
    <h2>Notes</h2>
    <ul>
      {notes.map(note =>
        <li key={note.id}>
          <Link to={`/notes/${note.id}`}>{note.content}</Link>
        </li>
      )}
    </ul>
  </div>
)

我们在App组件的路由中定义参数化的UR,如下所示。

<Router>
  // ...

  <Routes>
    <Route path="/notes/:id" element={<Note notes={notes} />} />    <Route path="/notes" element={<Notes notes={notes} />} />
    <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
    <Route path="/login" element={<Login onLogin={login} />} />
    <Route path="/" element={<Home />} />
  </Routes>
</Router>

我们通过用冒号标记参数来定义路由渲染一个特定的笔记 "表达风格":id

<Route path="/notes/:id" element={<Note notes={notes} />} />

当浏览器导航到一个特定笔记的网址,例如/notes/3,我们渲染Note组件。

import {
  // ...
  useParams} from "react-router-dom"

const Note = ({ notes }) => {
  const id = useParams().id  const note = notes.find(n => n.id === Number(id))
  return (
    <div>
      <h2>{note.content}</h2>
      <div>{note.user}</div>
      <div><strong>{note.important ? 'important' : ''}</strong></div>
    </div>
  )
}

Note组件接收所有的笔记作为propnotes,它可以通过React Router的useParams函数访问url参数(要显示的笔记的id)。

useNavigate

我们还在我们的应用中实现了一个简单的登录功能。如果一个用户登录了,关于登录用户的信息就会被保存到App组件的状态的user字段。

导航到Login视图的选项在菜单中被有条件地渲染。

<Router>
  <div>
    <Link style={padding} to="/">home</Link>
    <Link style={padding} to="/notes">notes</Link>
    <Link style={padding} to="/users">users</Link>
    {user      ? <em>{user} logged in</em>      : <Link style={padding} to="/login">login</Link>    }  </div>

  // ...
</Router>

所以如果用户已经登录了,我们不会显示链接Login,而是显示用户的用户名。

fullstack content

处理登录功能的组件的代码如下。

import {
  // ...
  useNavigate} from 'react-router-dom'

const Login = (props) => {
  const navigate = useNavigate()
  const onSubmit = (event) => {
    event.preventDefault()
    props.onLogin('mluukkai')
    navigate('/')  }

  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          username: <input />
        </div>
        <div>
          password: <input type='password' />
        </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

这个组件的有趣之处在于使用了React Router的useNavigate函数。有了这个函数,浏览器的url可以以编程方式改变。

随着用户的登录,我们调用navigate("/"),使浏览器的url改变为/,应用渲染相应的组件Home

useParamsuseNavigate都是钩子函数,就像我们现在已经多次使用的useState和useEffect。 正如你在第一章节所记得的,使用钩子函数有一些 规则 。Create-react-app已经被配置为在你违反这些规则时发出警告,例如,从条件语句中调用钩子函数。

redirect

关于Users路由还有一个有趣的细节。

<Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />

如果一个用户没有登录,Users组件就不会被渲染。相反,用户会被使用组件Navigate重定向到登录视图。

<Navigate replace to="/login" />

在现实中,如果用户没有登录到应用,也许最好不要在导航栏中显示需要登录的链接。

这里是App组件的全部内容。

const App = () => {
  const [notes, setNotes] = useState([
    // ...
  ])

  const [user, setUser] = useState(null)

  const login = (user) => {
    setUser(user)
  }

  const padding = {
    padding: 5
  }

  return (
    <div>
    <Router>
      <div>
        <Link style={padding} to="/">home</Link>
        <Link style={padding} to="/notes">notes</Link>
        <Link style={padding} to="/users">users</Link>
        {user
          ? <em>{user} logged in</em>
          : <Link style={padding} to="/login">login</Link>
        }
      </div>

      <Routes>
        <Route path="/notes/:id" element={<Note notes={notes} />} />
        <Route path="/notes" element={<Notes notes={notes} />} />
        <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
        <Route path="/login" element={<Login onLogin={login} />} />
        <Route path="/" element={<Home />} />
      </Routes>
    </Router>
      <div>
        <br />
        <em>Note app, Department of Computer Science 2022</em>
      </div>
    </div>
  )
}

我们定义了一个现代网络应用常见的元素,叫做footer,它定义了屏幕底部的部分,在Router之外,所以无论应用的路由部分显示什么组件,它都会显示。

Parameterized route revisited

我们的应用有一个缺陷。Note组件接收所有的笔记,尽管它只显示id与url参数相匹配的那个。

const Note = ({ notes }) => {
  const id = useParams().id
  const note = notes.find(n => n.id === Number(id))
  // ...
}

是否有可能修改应用,使Note只接收它应该显示的组件?

const Note = ({ note }) => {
  return (
    <div>
      <h2>{note.content}</h2>
      <div>{note.user}</div>
      <div><strong>{note.important ? 'important' : ''}</strong></div>
    </div>
  )
}

一种方法是使用React Router's useMatch钩子来计算出要在App组件中显示的笔记的id。

在定义应用的路由部分的组件中不可能使用useMatch钩。让我们把Router组件的使用从App移开。

ReactDOM.createRoot(document.getElementById('root')).render(
  <Router>    <App />
  </Router>,  document.getElementById('root')
)

App组件变成。

import {
  // ...
  useMatch} from "react-router-dom"

const App = () => {
  // ...

  const match = useMatch('/notes/:id')  const note = match    ? notes.find(note => note.id === Number(match.params.id))    : null
  return (
    <div>
      <div>
        <Link style={padding} to="/">home</Link>
        // ...
      </div>

      <Routes>
        <Route path="/notes/:id" element={<Note note={note} />} />        <Route path="/notes" element={<Notes notes={notes} />} />
        <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
        <Route path="/login" element={<Login onLogin={login} />} />
        <Route path="/" element={<Home />} />
      </Routes>

      <div>
        <em>Note app, Department of Computer Science 2022</em>
      </div>
    </div>
  )
}

每当该组件被渲染时,所以实际上每当浏览器的网址改变时,就会执行以下命令。

const match = useMatch('/notes/:id')

如果网址与/notes/:id相匹配,匹配变量将包含一个对象,我们可以从中访问路径的参数化部分,即要显示的笔记的id,然后我们可以获取要显示的正确笔记。

const note = match
  ? notes.find(note => note.id === Number(match.params.id))
  : null

完成的代码可以在这里找到。