a

React-router

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

除了本章的练习外,还有一系列的练习,通过扩展我们在第4和第5章节中使用的 Bloglist 应用来复习我们在整个课程中学到的知识。

Application navigation structure

【应用的导航结构】

在学完第6章节后,我们回到没有 Redux 的 React。

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

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

fullstack content

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

fullstack content

老派的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

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

Home 和 Users 与上次练习相同。Notes 有点复杂。 它以这样一种方式渲染作为props传递给它的便笺列表,即每个便笺的名称都是可点击的。

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-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,而是显示用户的用户名:

fullstack content

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

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

useParamsuseHistory 都是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