實現簡易版 React Route

實現簡易版 React Route

我們都知道 React中,一切皆組件。Router的使用就是引入一個個組件,非常方便。常見的路由組件有這些:

  • Link 路由鏈接
  • Route 基本路由
  • Switch 獨佔路由
  • Redirect 重定向組件
  • PrivateRoute 導航守衛
  • BrowserRouter 在Route基礎上添加了一些API方法

上面組件用法不多介紹了,參考react-router文檔 很容易上手。來看看如何實現它們:

Link

組件返回一個 a標籤,href路徑爲props中的 to 屬性,爲a標籤添加一個點擊事件,跳轉到對應組件

image.png

Switch

獨佔路由,即拿到Route數組,一旦匹配直接渲染,不往下繼續匹配
同時,Switch組件有location屬性,可指定path,無論瀏覽器path如何改變,總會顯示指定path對應組件

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";

export default class Switch extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          // 找出渲染的,第一個符合匹配的元素,存在element
          // const {location} = context;
          // 優先用props上的location
          const location = this.props.location || context.location;
          let element,
            match = null;
          let {children} = this.props;

      		// 此處直接用for循環亦可,不過要考慮children非數組情況
          React.Children.forEach(children, child => {
            if (match === null && React.isValidElement(child)) {
              element = child;
              const path = child.props.path;
              match = path
                ? matchPath(location.pathname, {
                    ...child.props,
                    path
                  })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, {
                location,
                computedMatch: match
              })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

Redirect

重定向組件,跳轉自然會用到 history.push

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";

export default class Redirect extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const {history} = context;
          const {to} = this.props;
          // history.push(to)
          return <LifeCycle onMount={() => history.push(to)} />;
        }}
      </RouterContext.Consumer>
    );
  }
}

// 考慮到渲染的生命週期,在Mount周後執行
class LifeCycle extends Component {
  componentDidMount() {
    if (this.props.onMount) {
      this.props.onMount();
    }
  }
  render() {
    return null;
  }
}

PrivateRoute

導航守衛,通常用於權限校驗或判斷登錄。
以判斷登錄狀態爲例,結合Redirect組件,已登錄,跳轉到to對應的path組件,未登錄則重定向到登錄頁

// 此處的connect方法是 react-redux中的方法,在react-redux一節提到過
export default connect(
  // mapStateToProps
  ({user}) => ({isLogin: user.isLogin})
)(
  class PrivateRoute extends Component {
    render() {
      const {isLogin, path, component} = this.props;
      if (isLogin) {
        // 登錄
        return <Route path={path} component={component} />;
      } else {
        // 去登錄,跳轉登錄頁面
        return <Redirect to={{pathname: "/login", state: {redirect: path}}} />;
      }
    }
  }
);

BrowserRouter

BrowserRouter暴露出三個屬性:

  • history 即history庫提供的 createBrowserHistory方法產生的對象
  • location 當前路由地址
  • match 是否匹配
import React, {Component} from "react";
import {createBrowserHistory} from "history";
import {RouterContext} from "./RouterContext";

export default class BrowserRouter extends Component {
  static computeRootMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }
  constructor(props) {
    super(props);
    this.history = createBrowserHistory();
    this.state = {
      location: this.history.location
    };
    this.unlisten = this.history.listen(location => {
      this.setState({location});
    });
  }
  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
    }
  }
  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.history,
          location: this.state.location,
          match: BrowserRouter.computeRootMatch(this.state.location.pathname)
        }}>
        {this.props.children}
      </RouterContext.Provider>
    );
  }
}

Route

Route相比其他幾個組件會複雜些,掌握以下要點:

  • props 除了人爲添加的 children/component/render, path 之外,還有BrowserRouter傳到context中的 match, location, history
  • 渲染的優先級 Children > Component > render
  • return 時先看 match結果,
    • 結果爲true,則按優先級渲染;
    • 結果爲false,則判斷是否有children,有則渲染,沒有爲null
  • 外面包一層Provide, 將需要傳遞的屬性如: context對象,location以及 match結果傳遞出去(可能跨層級,所以使用context方式傳遞)
import React, {Component, Children} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";

export default class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const {path, computedMatch, children, component, render} = this.props;
          const location = this.props.location || context.location;
          const match = computedMatch
            ? computedMatch
            : path
            ? matchPath(location.pathname, this.props)
            : context.match;
          const props = {
            ...context,
            location,
            match
          };
    
    			// match 匹配 children是function或者是節點
					// 非match 不匹配  children是function
          return (
            <RouterContext.Provider value={props}>
              {match
                ? children
                  ? typeof children === "function"
                    ? children(props)
                    : children
                  : component
                  ? // ? React.cloneElement(element, props)
                    React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? children(props)
                : null}
            </RouterContext.Provider>
          );

          // return match ? React.createElement(component, this.props) : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

爲了看的更清楚,當訪問首頁 / 時傳遞的props截圖如下:

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章