a
React-router
本课程第七章节的练习与之前的练习有一些不同。在这一章和下一章,像往常一样,有与本章理论相关的练习。
除了这一章和下一章的练习外,还有一系列的练习,在这些练习中,我们将通过扩展我们在第四和第五章节所做的Bloglist应用来复习我们在整个课程中所学到的知识。
Application navigation structure
在第六章节之后,我们将回到没有Redux的React。
对于网络应用来说,有一个导航条是很常见的,它可以切换应用的视图。
我们的应用可以有一个主页面
以及显示笔记和用户信息的独立页面。
在一个老式网络应用中,改变应用显示的页面将由浏览器向服务器发出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
让我们来看看前面例子中稍加修改的版本。这个例子的完整代码可以在这里找到。
该应用现在包含五个不同的视图,其显示由路由器控制。除了前面例子中的组件(Home、Notes和Users),我们还有代表登录视图的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.
点击名字的能力是通过组件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,而是显示用户的用户名。
处理登录功能的组件的代码如下。
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。
useParams和useNavigate都是钩子函数,就像我们现在已经多次使用的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
完成的代码可以在这里找到。