react-router知多少(一)

pre-notify

取名字真難!react

測試用例搭建

首先是入口文件,git

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';

ReactDOM.render(
	<App/>
    ,window.root
);
複製代碼

咱們只讓入口文件幹一件事,即渲染真實DOM到掛載元素上。github

注意即便只幹這麼一件事,react庫也是必須引入的,不然會報緩存

'React' must be in scope when using JSX  react/react-in-jsx-scope
複製代碼

接下來咱們把路由和導航都統一放在App組件裏,只求一目瞭然bash

import React from 'react';
import {HashRouter as Router,Route} from './react-router-dom'; //引入咱們本身的router庫

export default class App extends React.Component{
	render(){
    return (
          <Router>
            <div className='container'>
              <ul className='Nav'>
                <li><Link to='/home'>首頁</Link></li>
                <li><Link to='/user'>用戶</Link></li>
                <li><Link to='/profile'>我的設置</Link></li>
              </ul>
              <div className='View'>
                <Route path='/home' component={Home}/>
                <Route path='/user'component={User}/>
                <Route path='/profile' component={Profile}/>
              </div>
            </div>
          </Router>
        )
  }
}
複製代碼

Router實現

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

export default class Router extends React.Component{
    static childContextTypes = {
    	location:PropTypes.object
        ,history:PropTypes.object
    }
    
    constructor(props){
    	...
    }
    
    getChildContext(){
    	...
    }
    
    componentDidMount(){
    	...
    }
    
    render(){
    	...
    }
}
複製代碼

Router返回值

Router組件只是一個路由容器,它並不會生成一個div什麼的,它會直接返回它包裹住的子元素們childrenreact-router

render(){
    return this.props.children; //注意children只是props下的一個屬性
}
複製代碼

須要注意的是Router依然遵循JSX tag嵌套時的單一入口規則,So若是有多個平級的子元素須要用一層div或則其它什麼的給包起來。dom

<Router>
    <div>
    	<Route ... />
        <Route ... />
        ...
    </div>
</Router>
複製代碼

hash值的初始化

爲了效仿原版(沒有hash值時,自動補上/函數

首先咱們在Router的構造函數中先建立一個location的狀態對象,並這對象中的 pathname屬性賦一個初始值,這個pathname就是咱們之後的hash值了( 除去#部分

constructor(props){
    super(props);
    this.state = {
        location:{
            pathname:window.location.hash.slice(1)||'/'
        }
    }
}
複製代碼

接着咱們須要在Router組件掛載完畢時對location.hash進行賦值post

window.location.hash = this.state.location.pathname;
複製代碼

這樣就完成了/的自動補全功能。測試

監聽hash

Router最重要的功能之一就是監聽hash值,一旦hash值發生改變,Router應該讓路由Route從新渲染。

那麼,怎麼才能讓路由組件從新匹配和渲染呢?

嗯,只須要調用this.setState便可,React中的setState方法只要被調用,就會從新渲染調用這個方法的組件,理所固然,也包括其子組件。

[danger] 注意: 雖然setState只要調用就會從新渲染,但有一種狀況例外,則是setState()什麼也不傳的時。而只要有傳參,哪怕是一個{},也會重啓渲染。

咱們選在在組件渲染完畢時開啓監聽

componentDidMount(){
   ...
    window.addEventListener('hashchange',()=>{
      	this.setState({location:{pathname:window.location.hash.slice(1)||'/'}});
    });
}
複製代碼

關於setState:

setState雖然有自動合併state的功能,但若這個state裏還嵌套了一層,它是不會自動合併的,好比你有一個location的state,它長這樣{location:{pathname:xxx,other:yyy}},而後你像這樣更新了一下state {location:{pathname:xxx}},那麼location中的other將再也不保留,由於setState並不支持第二層嵌套的自動合併。

緩存

Router在監聽hash的時會實時的把hash值同步緩存在state上,這樣咱們就不用在每一次的路由匹配中都重頭獲取這個hash值而只須要從Router中拿便可。

那麼咱們怎麼在route中從一個父組件(router)中拿取東東呢?

React中提供了一個叫context的東東,在一個組件類中添加這個東東,就至關於開闢了一塊做用域,讓子孫組件可以輕易的經過這個做用域拿到父組件共享出的屬性和方法,我稱之爲React中的任意門

這個門有兩側,一側在開通這個context的根組件(相對於其子孫組件的稱謂)這邊

// 申明父組件要在context做用域裏放哪些東東
...
static childContextTypes = {
    location:PropTypes.object
    ,history:PropTypes.object
};

// 定義要放這些東東的具體細節
getChildContext(){
    return {
        location:this.state.location
        ,history:{
            push(path){
                window.location.hash = path;
            }
        }
    }
}
...
複製代碼

一側在要從根組件拿取東東的子孫組件這邊

[danger] 注意: 這裏的的靜態屬性再也不帶child字樣

...
// 和根組件相比 去除了child字樣
// 要用哪些東東就須要申明哪些東東
static contextTypes = {
    location:propTypes.object
    ,history:propTypes.object
}

// 在聲明完要從根組件中拿取哪些東東後,能夠在任意地方獲取到這些東東
fn(){
	...
    console.log(this.context.location);
}
...
複製代碼

Route實現

從上一節中咱們已經知道,Router組件最後返回的實際上是它的children們,So,也就是一條條Route.

<Router>
    <Route path='/a' component={myComponent1} />
    <Route path='/b' component={myComponent2} />
    <Route path='/c' component={myComponent3} />
</Router>
複製代碼

其中每一條<Route .. />都表明一次Route類的實例化,而且返回這個類中render函數所返回的東東。

咱們經過將準備要渲染的組件做爲屬性傳遞給<Route ../>組件,以求Route組件能幫咱們控制住咱們真正想要渲染的那些組件的渲染。(路由Route的角色就相似於編輯,須要對要渲染的內容進行審稿)

路由的匹配

實際中,咱們只有當url中的pathname和咱們在Route中設置的path相匹配時纔會讓Route組件渲染咱們傳遞給它的那些個真正想要渲染在頁面上的可視組件。

像這樣

...
// 接收根組件(Router)Context做用域中的 location 和 history
static contextTypes = {
    location:propTypes.object
    ,history:propTypes.object
}
...
// class Route の render方法中
...
let {component:Component,path} = this.props;
let {location} = this.context;
let pathname = location.pathname;

if(path==pathname||pathname.startsWith(path)){
  return <Component />
}else{
  return null;
}
...
複製代碼

路由的傳參

當路由真正被匹配上時,會傳遞三個參數給真正要渲染的可視組件

// class Route の render方法中
....
...
static contextTypes = {
    location:PropTypes.object
    ,history:PropTypes.object
}
...
let props = {
  location
  ,history:this.context.history
  ,match:{}
};
...
if(path==pathname||pathname.startsWith(path)){
      return <Component {...props}/>
      ...
複製代碼

如上所示,這三個參數屬性分別是:

  • location:主要存放着當前實時pathname
  • history:主要存放着各類跳轉路由的方法
  • match:存放着url 和 給route指定的path 以及動態路由參數params對象

pathname、path、url三者的區別

pathname在hashrotuer中是指#後面那一串,是url的子集。

path是咱們給Route組件手動指定的匹配路徑,和pathname進行匹配的,但不必定等於pathname,有startsWith匹配。除此以外path還多是一個/user/:id這樣的動態路由。

最後url,在react中它並非咱們的url地址,而是pathname通過path轉換成的正則匹配後的結果,它不必定等於path(由於還有動態路由)。

路由的渲染

React中路由的渲染有三種方式

  1. component
<Route path=.. compoent={Component1}/>
複製代碼

這種就是最多見的,會根據路徑是否匹配決定是否渲染傳遞過來的組件。

  1. render (多用於權限驗證)
<Route path=.. render={(props)=>{...}}>
複製代碼

採用render方式渲染時,組件是否渲染不只要看路徑是否匹配,還要由render屬性所接受的函數來共同決定。

注意,此時render函數會接受一個參數props,即當前Route組件的props對象。

  1. children (多用於菜單)
<Route path=.. children={(props)=>{...}}>
複製代碼

貌似和render沒區別,實則區別挺大!由於這貨不論路由的路徑是否匹配都會調用children這個回調函數。

So,分清楚了三種渲染方式的區別後,咱們來大概寫下如何實現

// Routeのrender函數中
...
if(result){ //表示路由匹配得上
    if(this.props.render){
    	return this.props.render(this.props);
    }else{
    	return <Component {...this.props}/>
    }
}else{
    if(this.props.children){ //若是children存在,就算路徑沒有匹配也會調用
    	return this.props.children(this.props);
    }else{
    	return null;
    }
}
...
複製代碼

動態路由

要實現動態路由,須要咱們將給Route設置的/xxx/:xxx們替換成正則用以匹配路徑,爲了代碼的清晰我門使用path-to-regexp模塊對全部路由(包括非動態路由)都進行正則替換。

path-to-regexp 模塊的實如今個人這篇文章中講過 Express源碼級實現の路由全解析(下闋)

而這一步須要在路由初始化的時候就完成

constructor(props){
    super(props);
    let {path} = props; //user/detail/:id
    this.keys = [];
    this.regexp = pathToRegexp(path,this.keys,{end:false}); //false表示只要開頭匹配便可
    this.keys = this.keys.map(key=>key.name); //便是傳遞給渲染組件的match對象中的params對象
}
複製代碼

這樣路由規則就不會在每次render重繪時都進行一次計算

接下來咱們須要在每次render中對路徑從新進行匹配

// render()中
...
let result = location.pathname.match(this.regexp);
...
複製代碼

若是匹配上了,有結果,還要準備一個params對象傳放進match對象中傳遞給渲染組件

if(result){
    let [url,...values] = result;

    props.match = {
        url //匹配上的路徑(<=pathname)
        ,path //route上的path
        ,params:this.keys.reduce((memo,key,idx)=>{
          memo[key] = values[idx];
          return memo;
        },{})
    };
}
複製代碼

最後再判斷是根據三種渲染方式中的哪種來渲染

if (result) {
      ...
  if (render) {
    return render(props);
  } else if (Component) {
    return <Component {...props}/>
  } else if (children) {
    return children(props);
  }
  return null;

} else {
  if (children) {
    return children(props);
  } else {
    return null;
  }
}
複製代碼

Link 組件

Link組件能讓咱們經過點擊鏈接來達到切換顯示路由組件的效果

export default class xxx extends React.Component{
  static contextTypes = {
    history:PropTypes.object
  };
  render(){
    return (
      <a onClick={()=>this.context.history.push(this.props.to)}>
        {this.props.children}
      </a>
    )
  }
}
複製代碼

MenuLink 組件

export default ({to,children})=>{
  return <Route path={to} children={props=>(
    <li className={props.match?"active":""}>
      <Link to={to}>{children}</Link>
    </li>
  )}/>
}
複製代碼
<ul className='Nav'>
    <MenuLink to='/home'>首頁</MenuLink>
    <MenuLink to='/user'>用戶</MenuLink>
    <MenuLink to='/profile'>詳情</MenuLink>
</ul>
複製代碼

這組件的做用便是讓匹配得上當前路由的link高亮

登陸驗證與重定向

在介紹三個相關組件以前須要對Router中存儲的push方法作出調整,以便保存Redirect跳轉前的路徑

...
push(path){
	if(typeof path === 'object'){
        let {pathname,state} = path;
        that.setState({location:{...that.state.location,state}},()=>{
          window.location.hash = pathname;
        })
    }else{
    	window.location.hash = path; //會自動添加'#'
    }
}
...
複製代碼

Redirect 組件

export default class xxx extends React.Component {
  static contextTypes = {
    history:PropTypes.object
  }

  componentWillMount() {
    this.context.history.push(this.props.to);
  }

  render() {
    return null;
  }
}
複製代碼

Protected 組件

export default function({component:Component,...rest}){
  return <Route {...rest} render={props=>(
    localStorage.getItem('login')?<Component {...props}/>:<Redirect to={{pathname:'/login',state:{from:props.location.pathname}}}/>
  )}/>;
}
複製代碼

Login 組件

import React from 'react';

export default class xxx extends React.Component{
  handleClick=()=>{
    localStorage.setItem('login',true);
    this.props.history.push(this.props.location.state.from);
  }
  render(){
    return (
      <div>
        <button onClick={this.handleClick} className="btn btn-primary">登陸</button>
      </div>
    )
  }
}
複製代碼

Switch組件

<Router>
    <Route path='/a' component={myComponent1} />
    <Route path='/b' component={myComponent2} />
    <Route path='/c' component={myComponent3} />
</Router>
複製代碼

一般狀況下咱們這樣寫Route有一點很差的是,無論第一個路由匹配沒匹配上,Router都會接着往下匹配,這樣就增長運算量。

So,Switch組件就是爲了解決這個問題

<Router>
    <Switch>
        <Route path='/a' component={myComponent1} />
        <Route path='/b' component={myComponent2} />
        <Route path='/c' component={myComponent3} />
    </Switch>
</Router>
複製代碼
export default class xxx extends React.Component{
  static contextTypes = {
    location:PropTypes.object
  }
  render(){
    console.log('Router render'); //只會打印一次
    let {pathname} = this.context.location;
    let children = this.props.children;
    for(let i=0;i<children.length;++i){
      let child = children[i]; //一個route
      let {path} = child.props;
      if(pathToRegexp(path,[],{end:false}).test(pathname)){
        return child;
      }
    }
    return null;
  }
}
複製代碼

這樣只有一個Route會被初始化以及渲染

但,有一個bug,咱們上面寫Route時,是將path轉正則的部分放在constructor裏的,這意味着只有在這個Route初始化的時候纔會將path轉換爲正則,這樣很好,只用計算一次,但和Switch搭配使用時就很差了,由於React的複用機制,即便路由路徑已經不同了,它仍然把上次的Route拿過來進行渲染,So此時的正則仍是上一次的,也就不會被匹配上,嗯,bug。

解決方案:

  • 第一種,給Route增長key
  • 第二種,將正則替換的部分放在render中

獲取demo代碼

倉庫地址:點我~點我!


推薦:

=== ToBeContinue ===

相關文章
相關標籤/搜索