a
React-router
本课程第七章节的练习与以前的有一点不同。 在本章和下一章中,像往常一样有与本章理论相关的练习。
除了本章的练习外,还有一系列的练习,通过扩展我们在第4和第5章节中使用的 Bloglist 应用来复习我们在整个课程中学到的知识。
Application navigation structure
【应用的导航结构】
在学完第6章节后,我们回到没有 Redux 的 React。
对于 web 应用来说,有一个导航条是很常见的,它可以切换应用的视图。
我们的应用可以有一个主页

以及显示便笺及用户资料的独立网页:

在老派的web应用中,更改应用显示的页面将由浏览器向服务器发出 HTTP GET 请求并显示表示返回视图的 HTML 来完成。
在单页应用中,我们实际上总是在同一页上。 浏览器运行的 Javascript 代码会产生不同“页面”的错觉。 如果 HTTP 请求是在切换视图时发出的,那么它们只用于获取 JSON 格式的数据,新视图可能需要这些数据才能显示出来。
导航栏和包含多个视图的应用非常容易使用 React 来实现。
这里有一个方法:
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
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.render(<App />, document.getElementById('root'))
每个视图都作为自己的组件实现。 我们将视图组件信息存储在名为page 的应用状态中。 这个信息告诉我们,表示视图的哪个组件应该显示在菜单栏下面。
然而,这种方法并不十分理想。 正如我们从图片中看到的,即使有时我们处于不同的视角,地址仍然保持不变。 每个视图最好都有自己的地址,例如使浏览器书签成为可能。后退-按钮 对于我们的应用也不能正常工作,这意味着后退 不会将您移动到以前显示的应用视图,而是移动到完全不同的位置。 如果应用变得更大,例如,我们希望为每个用户添加单独的视图和便笺,那么这个自制的routing (这意味着应用的导航管理)将变得过于复杂。
幸运的是 React 有React router-库,它为管理 React-application 中的导航提供了一个很好的解决方案。
让我们将上面的应用改为使用 React 路由
npm install --save react-router-dom
React Router 提供的路由通过更改应用启用,如下所示:
import {
BrowserRouter as Router,
Switch, 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>
<Switch>
<Route path="/notes">
<Notes />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
<div>
<i>Note app, Department of Computer Science 2020</i>
</div>
</Router>
)
}
路由,或基于浏览器中的 url的组件的条件渲染,通过将组件放置为Router 组件的子组件来使用,也就是在Router-tags 内部。
注意,即使组件名为Router,我们实际上讨论的是BrowserRouter ,因为这里的导入是通过重命名导入的对象实现的:
import {
BrowserRouter as Router, Switch, Route, Link
} from "react-router-dom"
根据文档
BrowserRouter 是一个 Router,它使用 HTML5的history API (pushState、 replaceState 和 popState 事件)保持 UI 与 URL 同步。
通常,当地址栏中的 URL 发生更改时,浏览器会加载一个新页面。 然而,借助于HTML5 history APIBrowserRouter,我们可以使用浏览器地址栏中的 URL 在 React-application 中进行内部“路由”。 因此,即使地址栏中的 URL 发生了变化,页面的内容也可以通过 Javascript 来操作,浏览器也不会从服务器加载新的内容。 使用后退和前进操作,以及制作书签,仍然像在传统网页上一样合乎逻辑。
在路由内部,我们定义了links,这个links 借助于Link组件来修改地址栏,
<Link to="/notes">notes</Link>
在应用中创建一个带有文本notes 的链接,当单击该文本时,会将地址栏中的 URL 更改为/ notes。
基于浏览器的 URL 渲染的组件是在组件Route的帮助下定义的,
<Route path="/notes">
<Notes />
</Route>
定义,如果浏览器地址是/notes,则渲染Notes 组件。
我们用一个Switch-组件包装要基于 url 渲染的组件
<Switch>
<Route path="/notes">
<Notes />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
这个开关的工作原理是,我们渲染第一个组件,它的path 匹配浏览器地址栏中的 url。
注意,组件的顺序很重要。 如果我们使用Home-组件,它的路径是 path="/",首先,没有其他东西会被渲染,因为"non existing" 路径"/" 是每个路径的开始:
<Switch>
<Route path="/"> <Home /> </Route>
<Route path="/notes">
<Notes />
</Route>
// ...
</Switch>
Parameterized route
【参数化路由】
让我们检查一下前一个例子中稍微修改过的版本,这个例子的完整代码可以在这里找到 here。
应用现在包含五个不同的视图,其显示由路由控制。 除了前面示例中的组件(Home、Notes 和Users)外,我们还有Login 表示登录视图,Note 表示单个便笺的视图。
Home 和 Users 与上次练习相同。Notes 有点复杂。 它以这样一种方式渲染作为props传递给它的便笺列表,即每个便笺的名称都是可点击的。

单击名称的能力是通过组件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-component 的路由中定义参数化 url 如下:
<Router>
<div>
<div>
<Link style={padding} to="/">home</Link>
<Link style={padding} to="/notes">notes</Link>
<Link style={padding} to="/users">users</Link>
</div>
<Switch>
<Route path="/notes/:id"> <Note notes={notes} /> </Route> <Route path="/notes">
<Notes notes={notes} />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
我们通过用冒号 :id 标签参数来定义渲染特定便笺的路由"express style"
<Route path="/notes/:id">
当浏览器导航到特定便笺的 url 时,例如/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 组件接收所有的便笺作为 props notes,它可以通过 react-router 的useParams函数访问 url 参数(要显示的便笺的 id)。
useHistory
我们还在应用中实现了一个简单的登录函数。 如果用户登录,则关于登录用户的信息将保存到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 {
// ...
useHistory} from 'react-router-dom'
const Login = (props) => {
const history = useHistory()
const onSubmit = (event) => {
event.preventDefault()
props.onLogin('mluukkai')
history.push('/') }
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路由的useHistory功能。
有了这个函数,组件就可以访问一个 history对象。 历史记录对象可以用于编程化地修改浏览器的 url。
对于用户登录,我们调用历史对象的 push 方法。 history.push('/') 调用导致浏览器的 url 更改为/,应用渲染相应的组件Home。
useParams和 useHistory 都是Hook函数,就像我们已经多次使用的 useState 和 useEffect 一样。 正如您在第1章节中记得的,使用hook函数有一些原则 。 Create-react-app 已经配置为警告,如果你打破这些规则,例如从一个 If判断语句中调用一个Hook函数,你就会收到警告。
redirect
【重新定向】
关于Users 路由还有一个有趣的细节:
<Route path="/users" render={() =>
user ? <Users /> : <Redirect to="/login" />
} />
如果用户未登录,则不渲染Users 组件。 相反,用户使用Redirect组件重定向到登录视图
<Redirect 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>
<Switch>
<Route path="/notes/:id">
<Note notes={notes} />
</Route>
<Route path="/notes">
<Notes notes={notes} />
</Route>
<Route path="/users">
{user ? <Users /> : <Redirect to="/login" />}
</Route>
<Route path="/login">
<Login onLogin={login} />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
<div>
<br />
<em>Note app, Department of Computer Science 2020</em>
</div>
</div>
)
}
我们定义了一个现代 web 应用通用的元素footer,它定义了屏幕底部Router 之外的部分,因此不管应用路由部分显示的是哪个组件,它都会显示出来。
Parameterized route revisited
【复习参数化路由】
我们的应用有一个缺陷。 组件接收所有的便笺,即使它只显示与 url 参数匹配的 id:
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 的useRouteMatchHook来计算出应用组件中显示的便笺的 id。
在定义应用路由部分的组件中不可能使用useRouteMatch-hook。 让我们把路由组件的使用从 App 中移除:
ReactDOM.render(
<Router> <App />
</Router>, document.getElementById('root')
)
App组件变成:
import {
// ...
useRouteMatch} from "react-router-dom"
const App = () => {
// ...
const match = useRouteMatch('/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>
<Switch>
<Route path="/notes/:id">
<Note note={note} /> </Route>
<Route path="/notes">
<Notes notes={notes} />
</Route>
// ...
</Switch>
<div>
<em>Note app, Department of Computer Science 2020</em>
</div>
</div>
)
}
每次渲染组件时,实际上每次浏览器 url 发生更改时,都会执行如下命令
const match = useRouteMatch('/notes/:id')
如果 url 匹配 /notes/:id,match 变量将包含一个对象,我们可以从该对象访问路径的参数化部分,即要显示便笺的 id,然后我们可以获取要显示的正确便笺
const note = match
? notes.find(note => note.id === Number(match.params.id))
: null
完成的代码可以在这里找到here。