React Router V4 精講

1、前端路由和後端路由

1)後端路由

多頁應用中,一個URL對應一個HTML頁面,一個Web應用包含不少HTML頁面,在多頁應用中,頁面路由控制由服務器端負責,這種路由方式稱爲後端路由。css

多頁應用中,每次頁面切換都須要向服務器發送一次請求,頁面使用到的靜態資源也須要從新加載,存在必定的浪費。並且,頁面的總體刷新對用戶體驗也有影響,由於不一樣頁面間每每存在共同的部分,例如導航欄、側邊欄等,頁面總體刷新也會致使共用部分的刷新。前端

2)前端路由

在單面應用中,URL發生並不會向服務器發送新的請求,因此「邏輯頁面」的路由只能由前端負責,這種路由方式稱爲前端路由。react

目前,國內的搜索引擎大多對單頁應用的SEO支持的很差,所以,對於 SEO 很是看重的 Web
應用(例如,企業官方網站,電商網站等),通常仍是會選擇採用多頁面應用。React 也並不是只能用於開發單頁面應用。

2、React Router 安裝

這裏使用的 React Router 的大版本號是 v4, 這也是目前最新版本。webpack

React Router 包含3個庫, react-router、react-router-dom、和 react-router-native。react-router 提供最基本的路由功能,實際使用,咱們不會直接安裝 react-router,而是根據應用運行的環境選擇安裝 react-router-dom(在瀏覽器中使用)或 react-router-native(在 react-native中使用)。react-router-dom 和 react-router-native 都依賴 react-router,因此在安裝時, react-router 也會自動安裝。
建立 Web應用,使用web

npm install react-router-dom

建立 navtive 應用,使用npm

npm install react-router-native

3、路由器

React Router 經過 Router 和 Route 兩個組件完成路由功能。Router 能夠理解成路由器,一個應用中須要一個 Router 實例,全部跌幅配置組件 Route 都定義爲 Router 的子組件。在 Web應用中,咱們通常會使用對 Router 進行包裝的 BrowserRouter 或 HashRouter 兩個組件 BrowserRouter使用 HTML5 的 history API(pushState、replaceState等)實現應用的 UI 和 URL 的同步。HashRouter 使用 URL 的 hash 實現應用的 UI 和 URL 同步。後端

BrowserRouter 建立的 URL 形式以下:

http://example.com/some/path

HashRouter 建立的 URL 形式以下:

http://example.com/#/some/path

使用 BrowserRouter 時,通常還須要對服務器進行配置,讓服務器能正確地處理全部可能的URL。例如,當瀏覽器發生 http://example.com/some/pathhttp://example.com/some/path2 兩個請求時,服務器須要能返回正確的 HTML 頁面(也就是單頁面應用中惟一的 HTML 頁面)react-native

HashRouter 則不存在這個問題,由於 hash 部分的內容會被服務器自動忽略,真正有效的信息是 hash 前端的部分,而對於單頁應用來講,這部分是固定的。瀏覽器

Router 會建立一個 history 對象,history 用來跟蹤 URL, 當URL 發生變化時, Router,的後代組件會從新渲染。React Router 中提供的其餘組件能夠經過 context 獲取 history 對象,這也隱含說明了 React Router 中其餘組件必須做爲 Router 組件後代使用。但 Router 中只能惟一的一個子元素,例如:服務器

// 正確
ReactDOM.render(
  (
  <BrowserRouter>
    <App />
  </BrowserRouter>),
  document.getElementById('root')
)
//錯誤,Router 中包含兩個子元素
ReactDOM.render(
  (
    <BrowserRouter>
      <App1 />
      <App2 />
    </BrowserRouter>),
  document.getElementById('root')
)

4、路由器

Route 是 React Router中用於配置路由信息的組件,也是 React Router 中使用頻率最高的組件。每當有一個組件須要根據 URL 決定是否渲染時,就須要建立一個 Route。

1) path

每一個 Route 都須要定義一個 path 屬性,當使用 BrowserRouter 時,path 用來描述這個Router匹配的 URL 的pathname;當使用 HashRouter時,path 用來描述這個 Route 匹配的 URL 的 hash。例如,使用 BrowserRouter 時,<Route path=''foo' /> 會匹配一個 pathname 以 foo 開始的 URL (如: http://example.com/foo)。當 URL 匹配一個 Route 時,這個 Route 中定義的組件就會被渲染出來。

2)match

當 URL 和 Route匹配時,Route 會建立一個 match 對象做爲 props 中的一個 屬性傳遞給被渲染的組件。這個對象包含如下4個屬性。

(1)params: Route的 path 能夠包含參數,例如 <Route path="/foo/:id" 包含一個參數 id。params就是用於從匹配的 URL 中解析出 path 中的參數,例如,當 URL = 'http://example.ocm/foo/1' 時,params= {id: 1}。

(2)isExact: 是一個布爾值,當 URL 徹底匹時,值爲 true; 當 URL 部分匹配時,值爲 false.例如,當 path='/foo'、URL="http://example.com/foo" 時,是徹底匹配; 當 URL="http://example.com/foo/1" 時,是部分匹配。

(3)path: Route 的 path 屬性,構建嵌套路由時會使用到。

(4)url: URL 的匹配的方式

3)Route 渲染組件的方式

(1)component

component 的值是一個組件,當 URL 和 Route 匹配時,Component屬性定義的組件就會被渲染。例如:

<Route path='/foo' component={Foo} >

當 URL = "http://example.com/foo" 時,Foo組件會被渲染。

(2) render
render 的值是一個函數,這個函數返回一個 React 元素。這種方式方便地爲待渲染的組件傳遞額外的屬性。例如:

<Route path='/foo' render={(props) => {
  <Foo {...props} data={extraProps} />
}}>
</Route>

Foo 組件接收了一個額外的 data 屬性。

(3)children
children 的值也是一個函數,函數返回要渲染的 React 元素。 與前兩種方式不一樣之處是,不管是否匹配成功, children 返回的組件都會被渲染。可是,當匹配不成功時,match 屬性爲 null。例如:

<Route path='/foo' render={(props) => {
  <div className={props.match ? 'active': ''}>
    <Foo {...props} data={extraProps} />
  </div>
}}>
</Route>

若是 Route 匹配當前 URL,待渲染元素的根節點 div 的 class 將設置成 active.

4)Switch 和 exact

當URL 和多個 Route 匹配時,這些 Route 都會執行渲染操做。若是隻想讓第一個匹配的 Route 沉浸,那麼能夠把這些 Route 包到一個 Switch 組件中。若是想讓 URL 和 Route 徹底匹配時,Route才渲染,那麼可使用 Route 的 exact 屬性。Switch 和 exact 經常聯合使用,用於應用首頁的導航。例如:

<Router>
 <Switch>
    <Route exact path='/' component={Home}/>
    <Route exact path='/posts' component={Posts} />
    <Route exact path='/:user' component={User} />
  </Switch>
</Router>

若是不使用 Switch,當 URL 的 pathname 爲 "/posts" 時,<Route path='/posts' /> 和 <Route path=':user' /> 都會被匹配,但顯然咱們並不但願 <Route path=':user' /> 被匹配,實際上也沒有用戶名爲 posts 的用戶。若是不使用 exact, "/" "/posts" "/user1"等幾乎全部 URL 都會匹配第一個 Route,又由於Switch 的存在,後面的兩個 Route永遠不會被匹配。使用 exact,保證 只有當 URL 的 pathname 爲 '/'時,第一個Route纔會匹配。

5)嵌套路由

嵌套路由是指在Route 渲染的組件內部定義新的 Route。例如,在上一個例子中,在 Posts 組件內再定義兩個 Route:

const Posts = ({match}) => {
  return (
    <div>
      {/* 這裏 match.url 等於 /posts */}
      <Route path={`${match.url}/:id`} component={PostDetail} />
      <Route exact path={match.url} component={PostList} />
    </div>
  )
}

5、連接

Link 是 React Router提供的連接組件,一個 Link 組件定義了當點擊該 Link 時,頁面應該如何路由。例如:

const Navigation = () => {
  <header>
    <nav>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/posts'>Posts</Link></li>
      </ul>
    </nav>
  </header>
}

Link 使用 to 屬性聲明要導航到的URL地址。to 能夠是 string 或 object 類型,當 to 爲 object 類型時,能夠包含 pathname、search、hash、state 四個屬性,例如:

<Link to={{
  pathname: '/posts',
  search: '?sort=name',
  hash:'#the-hash',
  state: { fromHome: true}
}}>
</Link>

除了使用Link外,咱們還可使用 history 對象手動實現導航。history 中最經常使用的兩個方法是 push(path,[state]) 和 replace(path,[state]),push會向瀏覽器記錄中新增一條記錄,replace 會用新記錄替換記錄。例如:

history.push('/posts');
history.replace('/posts');

6、路由設計

路由設計的過程能夠分爲兩步:

  1. 爲每個頁面定義有語義的路由名稱(path)
  2. 組織 Route 結構層次

1)定義路由名稱

咱們有三個頁面,按照頁面功能不難定義出以下的路由名稱:

  • 登陸頁: /login
  • 帖子列表頁: /posts
  • 帖子詳情頁: /posts/:id(id表明帖子的ID)

可是這些還不夠,還須要考慮打開應用時的默認頁面,也就是根路徑"/"對應的頁面。結合業務場景,帖子列表做爲應用的默認頁面爲合適,所以,帖子列表對應兩個路由名稱: '/posts'和 '/'

2)組織 Route 結構層次

React Router 4並不須要在一個地方集中聲明應用須要的全部 Route, Route實際上也是一個普通的 React 組件,能夠在任意地方使用它(前提是,Route必須是 Router 的子節點)。固然,這樣的靈活性也必定程度上增長了組織 Route 結構層次的難度。
咱們先考慮第一層級的路由。登陸頁和帖子列表頁(首頁)應該屬於第一層級:

<Router>
  <Switch>
    <Route exact path="/" component={Home}></Route>
    <Route exact path="/login" component={Login}></Route>
    <Route exact path="/posts" component={Home}></Route>
  </Switch>
</Router>

第一個Route 使用了 exact 屬性,保證只有當訪問根路徑時,第一個 Route 纔會匹配成功。Home 是首頁對應組件,能夠經過 "/posts" 和 「/」 兩個路徑訪問首頁。注意,這裏並無直接渲染帖子列表組件,真正渲染帖子列表組件的地方在 Home 組件內,經過第二層級的路由處理帖子列表組件和帖子詳情組件渲染,components/Home.js 的主要代碼以下:

class Home extends Component {
  /**省略其他代碼 */
  render() {
    const {match, location } = this.props;
    const { username } = this.state;
    return(
      <div>
        <Header
          username = {username}
          onLogout={this.handleLogout}
          location = {location}
        >
        </Header>
        {/* 帖子列表路由配置 */}
        <Route
          path = {match.url}
          exact
          render={props => <PostList username={username} {...this.props}></PostList>}
        ></Route>
      </div>
    )
  }
}

Home的render內定義了兩個 Route,分別用於渲染帖子列表和帖子詳情。PostList 是帖子列表組件,Post是帖子詳情組件,代碼使用Router 的render屬性渲染這兩個組件,由於它們須要接收額外的 username 屬性。另外,不管訪問是帖子列表頁面仍是帖子詳情頁面,都會共用相同 Header 組件。

7、代碼分片

默認狀況下,當在項目根路徑下執行 npm run build 時 ,create-react-app內部使用 webpack將 src路徑下的全部代碼打包成一個 JS 文件和一個 Css 文件。

當項目代碼量很少時,把全部代碼打包到一個文件的作法並不會有什麼影響。可是,對於一個大型應用,若是還把全部的代碼都打包到一個文件中,顯然就不合適了。

create-react-app 支持經過動態 import() 的方式實現代碼分片。import()接收一個模塊的路徑做爲參數,而後返回一個 Promise 對象, Promise 對象的值就是待導入的模塊對象。例如

// moduleA.js

const moduleA = 'Hello'
export { moduleA };

// App.js

import React, { Component } from 'react';

class App extends Component {
  handleClick = () => {
    // 使用import 動態導入 moduleA.js
    import('./moduleA')
      .then(({moduleA}) => {
        // 使用moduleA
      })
      .catch(err=> {
        //處理錯誤
      })
  };
  render() {
    return(
      <div>
        <button onClick={this.handleClick}>加載 moduleA</button>
      </div>
    )
  }
}

export default App;

上面代碼會將 moduleA.js 和它全部依賴的其餘模塊單獨打包到一個chunk文件中,只有當用戶點擊加載按鈕,纔開始加載這個 chunk 文件。
當項目中使用 React Router 是,通常會根據路由信息將項目代碼分片,每一個路由依賴的代碼單獨打包成一個chunk文件。咱們建立一個函數統一處理這個邏輯:

import React, { Component } from 'react';
// importComponent 是使用 import()的函數
export default function asyncComponent(importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props);
      this.state = {
        component:  null //動態加載的組件
      }
    }
    componentDidMount() {
      importComponent().then((mod) => {
        this.setState({
          // 同時兼容 ES6 和 CommonJS 的模塊
          component: mod.default ? mod.default : mod;
        });
      })
    }
    render() {
      // 渲染動態加載組件
      const C = this.state.component;
      return C ? <C {...this.props}></C> : null
    }
  }

  return AsyncComponent;
}

asyncComponent接收一個函數參數 importComponent, importComponent 內經過import()語法動態導入模塊。在AsyncComponent被掛載後,importComponent就會陰調用,進而觸發動態導入模塊的動做。
下面利用 asyncComponent 對上面的例子進行改造,代碼以下:

import React, { Component } from 'react';
import { ReactDOM, BrowserRouter as Router, Switch, Route } from 'react-dom';
import asyncComponent from './asyncComponent'
//經過asyncComponent 導入組件,建立代碼分片點
const AsyncHome = asyncComponent(() => import("./components/Home"))
const AsyncLogin = asyncComponent(() => import("./components/Login"))

class App extends component {
  render() {
    return(
      <Router>
        <Switch>
          <Route exact path="/" component={AsyncHome}></Route>
          <Route exact path="/login" component={AsyncLogin}></Route>
          <Route exact path="/posts" component={AsyncHome}></Route>
        </Switch>
      </Router>
    )
  }
}

export default App;

這樣,只有當路由匹配時,對應的組件纔會被導入,實現按需加載的效果。

這裏還有一個須要注意的地方,打包後沒有單獨的CSS文件了。這是由於 CSS樣子被打包到各個 chunk 文件中,當 chunk文件被加載執行時,會有動態把 CSS 樣式插入頁面中。若是但願把 chunk 中的 css打包到一個單獨的文件中,就須要修改 webpack 使用的 ExtractTextPlugin 插件的配置,但 create-react-app 並無直接把 webpack 的配置文件暴露給用戶,爲了修改相應配置
,須要將 create-react-app 管理的配置文件「彈射」出來,在項目根路徑下執行:

npm run eject

項目中會多出兩個文件夾:config和 scripts,scrips中包含項目啓動、編譯和測試的腳本,config 中包含項目使用的配置文件,
webpack配置文件 就在這個路徑下,打包 webpack.config.prod.js 找到配置 ExtractTextPlugin 的地方,添加 allChunks:true 這項配置:

new ExtractTextPlugin({
  filename: cssFilename,
  allChunks: true
})

而後從新編譯項目,各個chunk 文件 使用的 CSS 樣式 又會統一打包到 main.css 中。

以上主要參考 《React 進階之路》這本書

願你成爲終身學習者

想了解更多生活鮮爲人知的一面,能夠關注個人大遷世界噢

圖片描述

相關文章
相關標籤/搜索