React-Router V4 簡單實現

NarcissusLiu發表於2019-03-04

React-Router V4 簡單實現

聽說V4終於釋出了,本文是在閱讀RRV4時做的一點小總結

在此對React-Router4.0版本中重要的幾個元件做簡單的實現。

Match元件

Match元件的作用很簡單,就是基於url地址來判斷是否渲染對應元件。

使用方式:

<Match pattern=`/test`component={Component} />複製程式碼

###基本原理實現:

import React from `react`;

const Match = ({ pattern, component: Component }) => {
  const pathname = window.location.pathname;
  if (pathname.match(pattern)) {
    // 此處使用match方法對url地址進行解析是遠遠不夠的!
    return (
      <Component />
    );
  } else {
    return null;
  }
}複製程式碼

Match可以提取出URL中所有動態引數,然後將他們以傳入到對應的元件。

Link元件其實就是對<a>標籤的封裝。它會禁止掉瀏覽器的預設操作,通過history的API更新瀏覽器地址。

###使用方法

<Link to="/test" > To Test</Link>複製程式碼

基本實現原理

import createHistory from `history/createBroserHistory`;
const history = createHistory();

// Link元件因為不需要維持自身資料,因此我們在此可以使用無狀態(stateless)函式
const Link = ({ to, children }) => {
    <a
      onClick={(e) => 
        e.preventDefault();
        history.push(to)
      }
      href={to}
    >
      {children}
    </a>
  }複製程式碼

如上面程式碼所示,當我們點選a標籤的時候。首先會禁止瀏覽器的預設行為,然後會呼叫history.push的API介面更新URL的地址。

你可能會有疑問,既然通過點選就能更新URL地址了,那我們還設定a標籤的href屬性有何用呢?事實上的確沒什麼用處,但本著體驗最優這一原則我們仍需要設定該屬性,當使用者將滑鼠懸停在標籤上時能夠預覽連結地址和右擊複製連結。

React-Router的作用是在URL更改的時候,顯示對應的URL的SPA元件。因此當我們點選Link元件更新URL時,需要同時更新到對應的元件。history為我們提供了相應的監聽方法來監聽URL變化。

class App extends React.Component {
  componentDidMount() {
    history.listen(() => this.forceUPdate())
  }
  render(){
    return (
App
); } }複製程式碼

我們希望能夠根據給該元件傳入更多引數。例如:

  1. 當前URL為啟用狀態時,希望能夠顯示特別的樣式
  2. 如同普通a標籤一樣,居有target屬效能夠在另一個新頁面來顯示.
  3. 傳入特別的引數,給對應URL下元件使用,如query
function resolveToLocation(to, router) {
  return 
    typeof to === `function` ? to(router.location) : to
}
function isEmptyObject(object) {
  for (const p in object)
    if (Object.prototype.hasOwnProperty.call(object, p))
      return false

  return true
}
class Link extends React.Component {
  //這次要根據React-Router寫個相對簡單,但功能更全的版本
  handleClick(e) {
    // 如果有自定義的點選處理,就用該方法
    if (this.props.onClick) {
      this.props.onClick();
    }
    // 如果設定了target屬性,就使用瀏覽器預設方式來處理點選
    if (this.props.target) {
      return;
    }
    e.preventDefault();
    history.push(resolveToLocation(to));
  }
  render() {
    const { to, activeClassName, activeStyle, onlyActiveOnIndex, ...props } = this.props;

    const toLocation = resolveToLocation(to);
    props.href = toLocation;

    // 以下為React-router的程式碼,根據當前URL選擇樣式
     if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) {
        if (router.isActive(toLocation, onlyActiveOnIndex)) {
          if (activeClassName) {
            if (props.className) {
              props.className += ` ${activeClassName}`
            } else {
              props.className = activeClassName
            }
          }
          if (activeStyle)
            props.style = { ...props.style, ...activeStyle }
        }
      }
    return <a {...props} href={href}  onClick={this.handleClick} />
  }
}
Link.propTypes = {
  to: oneOfType([string, object, func]),
  query: object,
  state: object,
  activeStyle: object,
  activeClassName: string,
  onlyActiveOnIndex: bool.isRequired,
  onClick: func,
  target: string
}
Link.defaultProps = {
  onlyActiveOnIndex: false,
  style: {}
}複製程式碼

Redirect

Redirect元件和Link元件類似。和它不同之處在於Redirect不需要點選操作,直接就會更新URL地址。

使用方式

<Redirect to="/test" />複製程式碼

基本實現原理

class Redirect extends React.Component {
  static contextTypes = {
    history: React.PropTypes.object,
  };
  componentDidMount() {
    // 根本不需要DOM,只要你載入該元件,就立即進行跳轉
    const history = this.context.history;
    const to = this.props.to;
    history.push(to);
  }
  render() {
    return null;
  }
}複製程式碼

以上只是簡單的實現,V4版本的Redirect元件會判斷程式碼是不是在服務端渲染,並根據結果不同來選擇在元件生命週期的哪個階段進行URL更新操作(isServerRender ? componentWillMount : componentDidMount)。

晉級功能

class Redirect extends React.Component {
  ...
  // 服務端渲染的話,連Redirect元件都不需要渲染出來就可以跳轉。
  componentWillMount() {
    this.isServerRender = typeof window !== `object`;
    if (this.isServerRender) {
      this.perform();
    }
  }
  // 瀏覽器端渲染則需要Redirect元件渲染之後才能進行跳轉。
  componentDidMount() {
    if (!this.isServerRender) {
      this.perform();
    }
  }
  perform() {
    const { history } = this.context;
    const { push, to } = this.props;
    // push是個bool值,用於選擇使用history的哪個方法來跳轉連結
    if (push) {
      history.push(to);
    } else {
      history.replace(to);
    }
  }
}
Redirect.defaultProps = {
  push: false,
}複製程式碼

Router

react-router中的Router元件本質就是個高階元件,用於包裹所有的頁面元件(其實在react-router v4中,BrowserRouter才是最外層元件),併為所有react-router提供全域性引數history。因此Router很簡單,也很容易實現。

class Router extends React.Component {
  static propTypes = {
    history: ProTypes.object.isRequired,
    children: PropTypes.node
  };

  static childrenContextTypes = {
    history: PropTypes.object.isRequired
  };

  // 通過context給元件內所有元素提供共享資料: history
  getChildrenContext(){
    // 傳入給所有react-router元件使用的history引數
    return { history: this.props.history };
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  };
}複製程式碼

React-Router V4

SeverRouter (服務端渲染時使用)

createLocation方法很簡單,就是拆解URL地址,並將其分解為pathname,search,hash三個部分。並以陣列的形式返回。

// 例子url : www.webei.com/#/admin/dashboard/setting?username=liuxin&token=noface;
const createLocation = (url) => {
  let pathname = url || `/`
  let search = ``
  let hash = ``

  const hashIndex = pathname.indexOf(`#`)
    if (hashIndex !== -1) {
      hash = pathname.substr(hashIndex)
      pathname = pathname.substr(0, hashIndex)
    }

    const searchIndex = pathname.indexOf(`?`)
    if (searchIndex !== -1) {
      search = pathname.substr(searchIndex)
      pathname = pathname.substr(0, searchIndex)
    }

    return {
      pathname,
      search: search === `?` ? `` : search,
      hash: hash === `#` ? `` : hash
    }
    /* 最後返回 {
        pathname: `www.webei.com/`,
        search: ``,
        hash: `#/admin/dashboard/setting?username=liuxin&token=noface`,

      }

    */
  }複製程式碼

NavLink元件是對Route元件的封裝。目的是根據根據URL來Active對應的NavLink元件,為其新增activeClassNameactiveStyle

Route

在React-router V4版本中,Route元件存在於./Core.js檔案中。它是對createRouteElement方法的封裝。

const createRouteElement = ({ component, render, children, ...props} => (
  // createRouteElement 的執行優先順序是
  component ? 
    (props.match ? React.createElement(component, props) : null)
     : render ? (
      props.match ? 
        render(props) : null 
      ) : children ? (
        typeof children === `function` ? 
          children(props) : React.Children.only(children)
        ) : ( null )
));

// 上面的版本是不是很眼暈?下面這個版本的你大概能理解的更清楚。
const createRouteElement = ({ component, render, children, ...props} => {
  /* 
    引數優先順序 component > render > children
    要是同時有多個引數,則優先執行權重更高的方法。
  */
    // component 要求是方法
    if (component && props.match) {
      return React.createElement(component, props);
    } else {
      return null;
    }
    // render 要求是方法
    if (render && props.match) {
      return render(props);
    } else {
      return null;
    }
    // children 可以使方法也可以是DOM元素
    if (children && typeof children === `function`) {
      return children(props);
    } else {
      return React.Children.only(children)
    }
    return null;
}

const Route = ({ match, history, path, exact, ...props}) => (
  createRouteElement({
    ...props,
    match: match || matchPath(history.location.pathname, path, exact),
    history
  })
);

Route.propTypes = {
  match: PropTypes.object, // private, from <Switch>
  history: PropTypes.object.isRequired, // 必備屬性
  path: PropTypes.string,
  exact: PropTypes.bool,
  component: PropTypes.func,
  // TODO: Warn when used with other render props
  render: PropTypes.func, 
  // TODO: Warn when used with other render props
  children: PropTypes.oneOfType([
  // TODO: Warn when used with other render props
    PropTypes.func,
    PropTypes.node
  ])
}複製程式碼

Router

根據history引數的設定選擇使用不同封裝的Router元件

MemoryRouter

原始碼註釋
The public API for a that stores location in memory.

###BrowserRouter

原始碼註釋
The public API for a that uses HTML5 history.

###HashRouter

原始碼註釋
The public API for a that uses window.location.hash.

withHistory

相當重要的元件,沒有它就沒有根據URL變化而產生的重新渲染了。

/**
 * A higher-order component that starts listening for location
 * changes (calls `history.listen`) and re-renders the component
 * each time it does. Also, passes `context.history` as a prop.
 */
const withHistory = (component) => {
  return class extends React.Component {
    static displayName = `withHistory(${component.displayName || component.name})`

    static contextTypes = {
      history: PropTypes.shape({
        listen: PropTypes.func.isRequired
      }).isRequired
    }

    componentWillMount() {
      // Do this here so we can catch actions in componentDidMount (e.g. <Redirect>).
      this.unlisten = this.context.history.listen(() => this.forceUpdate())
    }

    componentWillUnmount() {
      this.unlisten()
    }

    render() {
      return React.createElement(component, {
        ...this.props,
        history: this.context.history
      })
    }
  }
}複製程式碼

相關文章