一. 認識react-router

1.1. 認識前端路由

路由其實是網絡工程中的一個術語:在架構一個網絡時,非常重要的兩個設備就是路由器和交換機。

當然,目前在我們生產中路由器也是越來越被大家所熟知,因爲我們生活中都會用到路由器:

  • 事實上,路由器主要維護的是一個映射表;

  • 映射表會決定數據的流向;

路由的概念出現最早是在後端路由中實現的,原因是web的發展主要經歷了這樣一些階段:

  • 後端路由階段;

  • 前後端分離階段;

  • 單頁面富應用(SPA);

階段一:後端路由階段

早期的網站開發整個HTML頁面是由服務器來渲染的.

  • 服務器直接生產渲染好對應的HTML頁面, 返回給客戶端進行展示.

但是, 一個網站, 這麼多頁面服務器如何處理呢?

  • 一個頁面有自己對應的網址, 也就是URL.

  • URL會發送到服務器, 服務器會通過正則對該URL進行匹配, 並且最後交給一個Controller進行處理.

  • Controller進行各種處理, 最終生成HTML或者數據, 返回給前端.

  • 這就完成了一個IO操作.

上面的這種操作, 就是後端路由.

  • 當我們頁面中需要請求不同的 路徑 內容時, 交給服務器來進行處理, 服務器渲染好整個頁面, 並且將頁面返回給客戶端.

  • 這種情況下渲染好的頁面, 不需要單獨加載任何的js和css, 可以直接交給瀏覽器展示, 這樣也有利於SEO的優化.

後端路由的缺點:

  • 一種情況是整個頁面的模塊由後端人員來編寫和維護的.

  • 另一種情況是前端開發人員如果要開發頁面, 需要通過PHP和Java等語言來編寫頁面代碼.

  • 而且通常情況下HTML代碼和數據以及對應的邏輯會混在一起, 編寫和維護都是非常糟糕的事情.

階段二:前後端分離階段

前端渲染的理解:

  • 每次請求涉及到的靜態資源都會從靜態資源服務器獲取,這些資源包括HTML+CSS+JS,然後在前端對這些請求回來的資源進行渲染;

  • 需要注意的是,客戶端的每一次請求,都會從靜態資源服務器請求文件;

  • 同時可以看到,和之前的後端路由不同,這時後端只是負責提供API了;

前後端分離階段:

  • 隨着Ajax的出現, 有了前後端分離的開發模式;

  • 後端只提供API來返回數據,前端通過Ajax獲取數據,並且可以通過JavaScript將數據渲染到頁面中;

  • 這樣做最大的優點就是前後端責任的清晰,後端專注於數據上,前端專注於交互和可視化上;

  • 並且當移動端(iOS/Android)出現後,後端不需要進行任何處理,依然使用之前的一套API即可;

  • 目前很多的網站依然採用這種模式開發(jQuery開發模式);

階段三:單頁面富應用(SPA)

單頁面富應用的理解:

  • 單頁面富應用的英文是single-page application,簡稱SPA;

  • 整個Web應用只有實際上只有一個頁面,當URL發生改變時,並不會從服務器請求新的靜態資源;

  • 而是通過JavaScript監聽URL的改變,並且根據URL的不同去渲染新的頁面;

如何可以應用URL和渲染的頁面呢?前端路由

  • 前端路由維護着URL和渲染頁面的映射關係;

  • 路由可以根據不同的URL,最終讓我們的框架(比如Vue、React、Angular)去渲染不同的組件;

  • 最終我們在頁面上看到的實際就是渲染的一個個組件頁面;

1.2. 前端路由原理

前端路由是如何做到URL和內容進行映射呢?監聽URL的改變。

URL的hash

  • URL的hash也就是錨點(#), 本質上是改變window.location的href屬性;

  • 我們可以通過直接賦值location.hash來改變href, 但是頁面不發生刷新;

<div id="app">
  <a href="#/home">home</a>
  <a href="#/about">about</a>
  <div class="router-view"></div>
</div>

<script>
  // 1.獲取router-view
  const routerViewEl = document.querySelector(".router-view");

  // 2.監聽hashchange
  window.addEventListener("hashchange", () => {
    switch(location.hash) {
      case "#/home":
        routerViewEl.innerHTML = "home";
        break;
      case "#/about":
        routerViewEl.innerHTML = "about";
        break;
      default:
        routerViewEl.innerHTML = "default";
    }
  })
</script>

hash的優勢就是兼容性更好,在老版IE中都可以運行,但是缺陷是有一個#,顯得不像一個真實的路徑。

HTML5的History

history接口是HTML5新增的, 它有l六種模式改變URL而不刷新頁面:

  • replaceState:替換原來的路徑;

  • pushState:使用新的路徑;

  • popState:路徑的回退;

  • go:向前或向後改變路徑;

  • forword:向前改變路徑;

  • back:向後改變路徑;

我們這裏來簡單演示幾個方法:

<div id="app">
  <a href="/home">home</a>
  <a href="/about">about</a>
  <div class="router-view"></div>
</div>

<script>
  // 1.獲取router-view
  const routerViewEl = document.querySelector(".router-view");

  // 2.監聽所有的a元素
  const aEls = document.getElementsByTagName("a");
  for (let aEl of aEls) {
    aEl.addEventListener("click", (e) => {
      e.preventDefault();
      const href = aEl.getAttribute("href");
      console.log(href);
      history.pushState({}, "", href);
      historyChange();
    })
  }

  // 3.監聽popstate和go操作
  window.addEventListener("popstate", historyChange);
  window.addEventListener("go", historyChange);

  // 4.執行設置頁面操作
  function historyChange() {
    switch(location.pathname) {
      case "/home":
        routerViewEl.innerHTML = "home";
        break;
      case "/about":
        routerViewEl.innerHTML = "about";
        break;
      default:
        routerViewEl.innerHTML = "default";
    }
  }

</script>

1.3. react-router

目前前端流行的三大框架, 都有自己的路由實現:

  • Angular的ngRouter

  • React的ReactRouter

  • Vue的vue-router

React Router的版本4開始,路由不再集中在一個包中進行管理了:

  • react-router是router的核心部分代碼;

  • react-router-dom是用於瀏覽器的;

  • react-router-native是用於原生應用的;

目前我們使用最新的React Router版本是v5的版本:

  • 實際上v4的版本和v5的版本差異並不大;

安裝react-router:

  • 安裝react-router-dom會自動幫助我們安裝react-router的依賴;

yarn add react-router-dom

二. react-router基本使用

2.1. Router基本使用

react-router最主要的API是給我們提供的一些組件:

  • BrowserRouter或HashRouter

    • Router中包含了對路徑改變的監聽,並且會將相應的路徑傳遞給子組件;

    • BrowserRouter使用history模式;

    • HashRouter使用hash模式;

  • Link和NavLink:

    • 通常路徑的跳轉是使用Link組件,最終會被渲染成a元素;

    • NavLink是在Link基礎之上增加了一些樣式屬性(後續學習);

    • to屬性:Link中最重要的屬性,用於設置跳轉到的路徑;

  • Route:

    • Route用於路徑的匹配;

    • path屬性:用於設置匹配到的路徑;

    • component屬性:設置匹配到路徑後,渲染的組件;

    • exact:精準匹配,只有精準匹配到完全一致的路徑,纔會渲染對應的組件;

在App中進行如下演練:

import React, { PureComponent } from 'react';

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

import Home from './pages/home';
import About from './pages/about';
import Profile from './pages/profile';

export default class App extends PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Link to="/">首頁</Link>
        <Link to="/about">關於</Link>
        <Link to="/profile">我的</Link>

        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/profile" component={Profile} />
      </BrowserRouter>
    )
  }
}
顯示效果

2.2. NavLink的使用

路徑選中時,對應的a元素變爲紅色

這個時候,我們要使用NavLink組件來替代Link組件:

  • activeStyle:活躍時(匹配時)的樣式;

  • activeClassName:活躍時添加的class;

  • exact:是否精準匹配;

先演示activeStyle:

<NavLink to="/" activeStyle={{color: "red"}}>首頁</NavLink>
<NavLink to="/about" activeStyle={{color: "red"}}>關於</NavLink>
<NavLink to="/profile" activeStyle={{color: "red"}}>我的</NavLink>

但是,我們會發現在選中about或profile時,第一個也會變成紅色:

  • 原因是/路徑也匹配到了/about或/profile;

  • 這個時候,我們可以在第一個NavLink中添加上exact屬性;

<NavLink exact to="/" activeStyle={{color: "red"}}>首頁</NavLink>

默認的activeClassName:

  • 事實上在默認匹配成功時,NavLink就會添加上一個動態的active class;

  • 所以我們也可以直接編寫樣式

a.active {
  color: red;
}

當然,如果你擔心這個class在其他地方被使用了,出現樣式的層疊,也可以自定義class

<NavLink exact to="/" activeClassName="link-active">首頁</NavLink>
<NavLink to="/about" activeClassName="link-active">關於</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>
設置activeClassName效果

2.3. Switch的作用

我們來看下面的路由規則:

  • 當我們匹配到某一個路徑時,我們會發現有一些問題;

  • 比如/about路徑匹配到的同時, /:userid 也被匹配到了,並且最後的一個NoMatch組件總是被匹配到;
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:userid" component={User}/>
<Route component={NoMatch}/>
匹配效果如下

原因是什麼呢?默認情況下,react-router中只要是路徑被匹配到的Route對應的組件都會被渲染;

但是實際開發中,我們往往希望有一種排他的思想:

  • 只要匹配到了第一個,那麼後面的就不應該繼續匹配了;

  • 這個時候我們可以使用Switch來將所有的Route進行包裹即可;

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/profile" component={Profile} />
  <Route path="/:userid" component={User} />
  <Route component={NoMatch} />
</Switch>

2.3. Redirect的使用

Redirect用於路由的重定向,當這個組件出現時,就會執行跳轉到對應的to路徑中:

我們這裏使用這個的一個案例:

  • 用戶跳轉到User界面;

  • 但是在User界面有一個isLogin用於記錄用戶是否登錄:

    • true:那麼顯示用戶的名稱;

    • false:直接重定向到登錄界面;

App.js中提前定義好Login頁面對應的Route:

<Switch>
  ...其他Route
  <Route path="/login" component={Login} />
  <Route component={NoMatch} />
</Switch>

在User.js中寫上對應的邏輯代碼:

import React, { PureComponent } from 'react'
import { Redirect } from 'react-router-dom';

export default class User extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      isLogin: false
    }
  }

  render() {
    return this.state.isLogin ? (
      <div>
        <h2>User</h2>
        <h2>用戶名: coderwhy</h2>
      </div>
    ): <Redirect to="/login"/>
  }
}

三. react-router高級使用

3.1. 路由嵌套

在開發中,路由之間是存在嵌套關係的。

這裏我們假設about頁面中有兩個頁面內容:

  • 商品列表和消息列表;

  • 點擊不同的鏈接可以跳轉到不同的地方,顯示不同的內容;

import React, { PureComponent } from 'react';

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

function AboutProduct(props) {
  return (
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
    </ul>
  )
}

function AboutMessage(props) {
  return (
    <ul>
      <li>消息列表1</li>
      <li>消息列表2</li>
      <li>消息列表3</li>
    </ul>
  )
}

export default class About extends PureComponent {
  render() {
    return (
      <div>
        <Link to="/about">商品</Link>
        <Link to="/about/message">消息</Link>

        <Switch>
          <Route exact path="/about" component={AboutProduct} />
          <Route path="/about/message" component={AboutMessage} />
        </Switch>
      </div>
    )
  }
}

3.2. 手動跳轉

目前我們實現的跳轉主要是通過Link或者NavLink進行跳轉的,實際上我們也可以通過 JavaScript代碼 進行跳轉。

但是通過 JavaScript代碼 進行跳轉有一個前提:必須獲取到history對象。

如何可以獲取到history的對象呢?兩種方式

  • 方式一:如果該組件是通過路由直接跳轉過來的,那麼可以直接獲取history、location、match對象;

  • 方式二:如果該組件是一個普通渲染的組件,那麼不可以直接獲取history、location、match對象;

那麼如果普通的組件也希望獲取對應的對象屬性應該怎麼做呢?

  • 前面我們學習過高階組件,可以在組件中添加想要的屬性;

  • react-router也是通過高階組件爲我們的組件添加相關的屬性的;

如果我們希望在App組件中獲取到history對象,必須滿足以下兩個條件:

  • App組件必須包裹在Router組件之內;

  • App組件使用withRouter高階組件包裹;

index.js代碼修改如下:

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

App.js代碼修改如下:

import { Route, Switch, NavLink, withRouter } from 'react-router-dom';
...省略其他的導入代碼

class App extends PureComponent {
  render() {
    console.log(this.props.history);

    return (
      <div>
        ...其他代碼
        <button onClick={e => this.pushToProfile()}>我的</button>

        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/profile" component={Profile} />
          <Route path="/:userid" component={User} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    )
  }

  pushToProfile() {
    this.props.history.push("/profile");
  }
}

export default withRouter(App);

源碼選讀:這裏的history來自哪裏呢?是否和之前使用的window.history一樣呢?

我們發現withRouter的高階函數來自react-router-dom:

  • 實際上來自react-router的包;

image-20200721175654993

withRouter函數:

withRouter函數

history對象來自哪裏呢?

  • 實際來自上面代碼的context;

context

這個context的值來自哪裏呢?

  • 來自於context.Consumer的value中;

Router中的value

this.props.history來自哪裏呢?

  • 來自BrowserRouter或者HashRouter在創建時,傳入的值;

  • 又傳遞給了Router,Router的子組件可以通過該context獲取到這個值;

BrowserRouter的源碼

createBrowserHistory來自哪裏呢?

history模塊

執行push操作的本質:

push操作

2.5. 傳遞參數

傳遞參數有三種方式:

  • 動態路由的方式;

  • search傳遞參數;

  • to傳入對象;

動態路由的方式

動態路由的概念指的是路由中的路徑並不會固定:

  • 比如 /detail 的path對應一個組件Detail;
  • /detail/:id
    /detail/abc
    /detail/123
    
  • 這個匹配規則,我們就稱之爲動態路由;

通常情況下,使用動態路由可以爲路由傳遞參數。

<div>
 ...其他Link
  <NavLink to="/detail/abc123">詳情</NavLink>

  <Switch>
    ... 其他Route
    <Route path="/detail/:id" component={Detail}/>
    <Route component={NoMatch} />
  </Switch>
</div>

detail.js的代碼如下:

  • 我們可以直接通過match對象中獲取id;

  • 這裏我們沒有使用withRouter,原因是因爲Detail本身就是通過路由進行的跳轉;

import React, { PureComponent } from 'react'

export default class Detail extends PureComponent {
  render() {
    console.log(this.props.match.params.id);

    return (
      <div>
        <h2>Detail: {this.props.match.params.id}</h2>
      </div>
    )
  }
}

search傳遞參數

NavLink寫法:

  • 我們在跳轉的路徑中添加了一些query參數;

<NavLink to="/detail2?name=why&age=18">詳情2</NavLink>

<Switch>
  <Route path="/detail2" component={Detail2}/>
</Switch>

Detail2中如何獲取呢?

  • Detail2中是需要在location中獲取search的;

  • 注意:這個search沒有被解析,需要我們自己來解析;

import React, { PureComponent } from 'react'

export default class Detail2 extends PureComponent {
  render() {
    console.log(this.props.location.search); // ?name=why&age=18

    return (
      <div>
        <h2>Detail2:</h2>
      </div>
    )
  }
}

to傳入對象

to可以直接傳入一個對象

<NavLink to={{
    pathname: "/detail2", 
    query: {name: "kobe", age: 30},
    state: {height: 1.98, address: "洛杉磯"},
    search: "?apikey=123"
  }}>
  詳情2
</NavLink>

獲取參數:

import React, { PureComponent } from 'react'

export default class Detail2 extends PureComponent {
  render() {
    console.log(this.props.location);

    return (
      <div>
        <h2>Detail2:</h2>
      </div>
    )
  }
}
location對象

四. react-router-config

目前我們所有的路由定義都是直接使用Route組件,並且添加屬性來完成的。

但是這樣的方式會讓路由變得非常混亂,我們希望將所有的路由配置放到一個地方進行集中管理:

  • 這個時候可以使用react-router-config來完成;

安裝react-router-config:

yarn add react-router-config

常見router/index.js文件:

import Home from "../pages/home";
import About, { AboutMessage, AboutProduct } from "../pages/about";
import Profile from "../pages/profile";
import Login from "../pages/login";
import User from "../pages/user";
import Detail from "../pages/detail";
import Detail2 from "../pages/detail2";
import NoMatch from "../pages/nomatch";

const routes = [
  {
    path: "/",
    exact: true,
    component: Home
  },
  {
    path: "/about",
    component: About,
    routes: [
      {
        path: "/about",
        exact: true,
        component: AboutProduct
      },
      {
        path: "/about/message",
        component: AboutMessage
      },
    ]
  },
  {
    path: "/profile",
    component: Profile
  },
  {
    path: "/login",
    component: Login
  },
  {
    path: "/user",
    component: User
  },
  {
    path: "/detail/:id",
    component: Detail
  },
  {
    path: "/detail2",
    component: Detail2
  },
  {
    component: NoMatch
  }
];

export default routes;

將之前的Switch配置,換成react-router-config中提供的renderRoutes函數:

{renderRoutes(routes)}

{/* <Switch>
     <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/profile" component={Profile} />
      <Route path="/user" component={User} />
      <Route path="/login" component={Login} />
      <Route path="/detail/:id" component={Detail}/>
      <Route path="/detail2" component={Detail2}/>
      <Route component={NoMatch} />
 </Switch> */}

如果是子組件中,需要路由跳轉,那麼需要在子組件中使用renderRoutes函數:

  • 在跳轉到的路由組件中會多一個 this.props.route 屬性;
  • route 屬性代表當前跳轉到的路由對象,可以通過該屬性獲取到 routes
export default class About extends PureComponent {
  render() {
    return (
      <div>
        <Link to="/about">商品</Link>
        <Link to="/about/message">消息</Link>

        {renderRoutes(this.props.route.routes)}
      </div>
    )
  }
}

實際上react-router-config中還提供了一個 matchRoutes 輔助函數:

  • matchRoutes(routes, pathname) 傳入一個路由對象數組,獲取所有匹配的路徑;
const routes = matchRoutes(this.props.route.routes, "/about");
console.log(routes);

查看renderRoutes的源碼也是非常簡單的:

renderRoutes源碼
相關文章