react-router4-training

前言

如今搭建項目通常都是使用腳本手架,其次項目完成搭建以後一些基礎的配置信息以及配置如何生效的,對於普通開發人員來講不太接觸的到,相似react-router的配置,長時間不配置的容易健忘html

因此寫一篇文章記錄下react-router的各類功能使用,主要是根據reacttraning網站的菜單目錄來開發的各個demo,以便於全面的理解react-router使用的知識點前端

先提幾個問題

文章開始以前,先留幾個問題,能夠邊看demo邊理解:react

  • react/react-router同構場景下的實現?
  • 路由代碼拆分如何實現?
  • 如何實現自定義React lazy以及Suspense?
  • 如何實現一個切換路由以後滾動到頂部的組件?
  • react-router如何跟react-redux整合?
  • 路由hooks的使用?

全部的DEMO我集中放在了一個文件夾下,戳DEMO地址查看git

DEMO演示及講解

DEMO1.搭建簡單的react-router

DEMO地址github

import { BrowserRouter, Route, Link, Switch } from 'react-router-dom'

<BrowserRouter>
    <ul style={{listStyle: 'none'}}>
      <li>
        <Link to="/dashboard">dashboard</Link>
      </li>
    </ul>
    <Switch>
      <Route path="/dashboard">
        <Dashboard />
      </Route>
      <Route path="/">
        <Dashboard />
      </Route>
    </Switch>
</BrowserRouter>
複製代碼

簡單的react-router-demo就搭建完成了。那麼有幾個問題?web

  • Switch是幹嗎用的?
  • Route沒有匹配到的話,如何配置404頁面?
  • 根路徑的配置須要注意什麼?

下面來解答下這些疑問redux

Switch是幹嗎用的?

問題場景:bash

咱們通常都須要定義一個根路徑的路由,可是根路徑的路由若是隻是這樣寫的話,會致使輸入路由 /dashboard 的時候 渲染了兩個Dashboardreact-router

⚠️⚠️⚠️ Switch的做用就來了,Switch的做用是隻渲染第一個匹配到的路由的項,解決了以上重複渲染的問題dom

那其實還有一種解決方案,放在下面【根路徑的配置須要注意什麼?】去解釋

根路徑的配置須要注意什麼?

Route下面有個exact屬性,exact屬性爲true時,表示精確匹配到/纔會進根路徑的Component,否則/dashboard路由也會同時匹配到/路由

<Route path="/" exact>
複製代碼
Route沒有匹配到的話,如何配置404頁面?

不要配置path,當找不到路由配置時就會走這個

<Route>
    <div>404</div>
</Route>
複製代碼

DEMO2.同構場景下的react-router使用

DEMO地址

原理

這個涉及到同構的概念,客戶端和服務端的結構不同

App:能夠理解這是個Route集合,服務端和客戶端通用

服務端: ReactServerDom + StaticRouter + App

  • react服務端渲染須要用到 react-dom/server 的 renderToString 轉成字符串傳給nunjucks模版引擎去渲染
  • 服務端沒有dom,因此BrowserRouter會報錯,因此須要換成StaticRouter

客戶端:ReactDom + BrowserRouter + App

  • 客戶端須要注意的是,同構
  • 其次服務端和客戶端渲染的時候StaticRouter和BrowserRouter的區別,我這裏是採用不一樣入口的方式解決的

涉及react-router使用的源碼實現以下:

服務端的實現
import ReactServerDom from 'react-dom/server'
import { StaticRouter } from 'react-router'
import App from '../client/App'

const markup = ReactServerDom.renderToString(
    <StaticRouter location={ctx.req.url}>
        <App />
    </StaticRouter>
)
複製代碼
客戶端的實現
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDom.render(<BrowserRouter>
    <App />
</BrowserRouter>, document.getElementById('root'))
複製代碼
App.js的實現
import React from 'react';
import { Switch, Route, Link } from 'react-router-dom'
import Dashboard from './router/dashboard'
import Home from './router/home/index'

function App() {
  return (
    <React.Fragment>
        <div style={{width: 200, float: 'left', listStyle: 'none'}}>
            <h3>菜單</h3>
            <ul style={{listStyle: 'none'}}>
              <li>
                <Link to="/dashboard">dashboard</Link>
              </li>
              <li>
                <Link to="/home">home</Link>
              </li>
            </ul>
          </div>
          <div style={{paddingLeft: 200}}>
            <Switch>
              <Route path="/dashboard">
                <Dashboard />
              </Route>
              <Route path="/home">
                <Home />
              </Route>
              <Route path="/">
                <Dashboard />
              </Route>
            </Switch>
        </div>
    </React.Fragment>
  );
}

export default App;
複製代碼
實現的過程當中碰到了幾個問題

koa-nunjucks-2 html被渲染成了字符串

解決方案 autoescape: false

同構作完以後發現路由切換仍是發起了請求,而且是404,如何解決?

這實際上是由於客戶端的同構沒有生效,檢查客戶端代碼,保證注入的client.js可以正常運行,路由切換就不會再發起請求了

同構實現正確,頁面出現以後,其實頁面的控制權已經交到前端js即BrowserRouter的手中,因此後續的history切換跟服務端沒有關係了

DEMO3.路由代碼拆分

DEMO地址

路由代碼拆分有三種方式:

  • @loadable/component
  • react-loadable
  • react lazy Suspense
@loadable/component的實現
import loadable from '@loadable/component'

const Home = loadable(() => import('./router/home'))
const Dashboard = loadable(() => import('./router/dashboard'))
複製代碼
react-loadable的實現
import loadable from 'react-loadable'

const Loading = () => <div>loading</div>
const Dashboard = loadable({
  loader: () => import('./router/dashboard'),
  loading: Loading,
});
const Home = loadable({
  loader: () => import('./router/home'),
  loading: Loading,
});
複製代碼
react lazy Suspense的實現
import { Switch, Route, Link } from 'react-router-dom'

const Home = lazy(() => import('./router/home'))
const Dashboard = lazy(() => import('./router/dashboard'))
複製代碼
擴展實現myReactLazy

實現一個Suspense.js和lazy.js,功能相似react原生的suspense,lazy

使用以下

<!--注意:這是僞代碼,要執行的話參考demo地址,或者本身修改下️-->
import Suspense from './suspense'
import lazy from './lazy'

const Dashboard = lazy(() => import('./router/dashboard'))
const Home = lazy(() => import('./router/home'))

<Suspense fallback={<div>loading</div>}>
    <div style={{paddingLeft: 200}}>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
    </div>
</Suspense>
複製代碼

兩個js的實現以下

Suspense.js

import React from 'react'
import PropTypes from 'prop-types'

export default class Suspense extends React.Component{
    getChildContext(){
        return {
            fallback: this.props.fallback || <div>loading...</div>
        }
    }

    render(){
        return this.props.children
    }
}

Suspense.childContextTypes = {
    fallback: PropTypes.object
}
複製代碼

lazy.js

import React from 'react'
import PropTypes from 'prop-types'

export default load => {
    class Lazy extends React.Component{
        state = { Comp: null }

        componentDidMount(){
            // 加個定時器,看下fallback的效果
            setTimeout(() => {
                load().then(module => {
                    this.setState({
                        Comp: module.default
                    })
                })
            }, 3000)

            // load().then(module => {
            //     this.setState({
            //         Comp: module.default
            //     })
            // })
        }
        
        render(){
            let { Comp } = this.state
            return Comp ? <Comp /> : this.context.fallback
        }
    }

    Lazy.contextTypes = {
        fallback: PropTypes.object
    }

    return Lazy
}
複製代碼

⚠️這裏有個注意點是:Suspense和lazy加載的組件非直接父子組件關係,因此這裏採用context實現

DEMO4.當有修改時如何阻止路由跳轉

DEMO地址

import { Prompt } from 'react-router-dom'
<Prompt
    when={isBlocking}
    message={location => `Are you sure you want to go to ${location.pathname}`
} />
複製代碼

其實就是利用了Prompt組件,when爲true時攔截,爲false時放行

DEMO5.路由切換滾動到頁面頂部

DEMO地址

這個DEMO解決的問題

有些時候在一個路由下操做,而後滾動到下面,致使頂部內容看不到了,此時切換菜單,但是頁面沒有自動滾動到頂部

第一種實現方案

使用withRouter把history,location,match塞入ScrollToTop組件的props中,當路由切換以後進入ScrollToTop的componentDidUpdate事件,在這個事件中window.scrollTo(0, 0)

ScrollToTop組件定義

class ScrollToTop extends React.Component{
  componentWillUpdate(prevProps){
    if(this.props.location.pathname != prevProps.location.pathname){
      window.scrollTo(0, 0)
    }
  }

  render(){
    return this.props.children
  }
}
複製代碼

ScrollToTop組件的使用

<Router>
  <ScrollToTopComp>
    <!--...其餘代碼-->
  </ScrollToTopComp>
</Router>
複製代碼
第二種實現方案

改進方案是使用hooks的useEffect
ScrollToTop組件定義

import { useEffect } from 'react'
function ScrollToTop({children, location: { pathname }}){
    useEffect(() => {
        window.scrollTo(0, 0)
    }, [pathname])

    return children
}

const ScrollToTopComp = withRouter(ScrollToTop)
複製代碼

ScrollToTop組件的使用和第一種方案同樣

第三種實現方案

第三種方案是在每一個路由頁面加一個ScrollToTop組件,在ScrollToTop的componentDidMount事件中加window.scrollTo(0, 0)

這個方案就不具體演示代碼了

DEMO6.redux整合

DEMO地址

⚠️爲何會有整合的問題❓❓❓

-> react-router:withRouter(Home)   
-> react-redux:connect(state => {   
    return { name: state.name }   
})(Home)   
複製代碼

兩個都須要再次封裝Home,那麼怎麼辦❓❓❓

解決方案一
connect(state => {
    return { name: state.name } 
})(withRouter(Home))
複製代碼
解決方案二
<Route path="/home" component={Home} />

connect(state => {
    return { name: state.name }
})(Home)
複製代碼

DEMO7.路由Hooks

DEMO地址

import { useLocation, useHistory, useParams, useRouteMatch } from 'react-router'

const location = useLocation()
const history = useHistory()
const params = useParams()
const match = useRouteMatch()

就能夠獲取到各個對象了
複製代碼

ok,到這裏差很少把reacttraining的功能都用demo實現了一遍,可是沒有具體解釋例如Route的屬性的含義等,這篇demo文章就看成是對react-router的複習

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息