美文网首页
一起学react(5) 史上最全react-router4底层源

一起学react(5) 史上最全react-router4底层源

作者: fangkyi03 | 来源:发表于2018-05-27 05:45 被阅读1445次

    本篇文章主要分析react-router-redux 与react-router 这两个插件的底层源码部分

    首先来看一下简单的react-router-redux 来了解一下它的运行机制

    如果有什么问题的话 可以加我QQ:469373256

    示例

    import React from 'react'
    import { render } from 'react-dom'
    import { connect, Provider } from 'react-redux'
    import {
      ConnectedRouter,
      routerReducer,
      routerMiddleware,
      push
    } from 'react-router-redux'
    
    import { createStore, applyMiddleware, combineReducers } from 'redux'
    import createHistory from 'history/createBrowserHistory'
    
    import { Route, Switch } from 'react-router'
    import { Redirect } from 'react-router-dom'
    
    const history = createHistory()
    
    const authSuccess = () => ({
      type: 'AUTH_SUCCESS'
    })
    
    const authFail = () => ({
      type: 'AUTH_FAIL'
    })
    
    const initialState = {
      isAuthenticated: false
    }
    
    const authReducer = (state = initialState , action) => {
      switch (action.type) {
        case 'AUTH_SUCCESS':
          return {
            ...state,
            isAuthenticated: true
          }
        case 'AUTH_FAIL':
          return {
            ...state,
            isAuthenticated: false
          }
        default:
          return state
      }
    }
    
    const store = createStore(
      combineReducers({ routerReducer, authReducer }),
      applyMiddleware(routerMiddleware(history)),
    )
    
    class LoginContainer extends React.Component {
      render() {
        return <button onClick={this.props.login}>Login Here!</button>
      }
    }
    
    class HomeContainer extends React.Component {
      componentWillMount() {
        alert('Private home is at: ' + this.props.location.pathname)
      }
    
      render() {
        return <button onClick={this.props.logout}>Logout Here!</button>
      }
    }
    
    class PrivateRouteContainer extends React.Component {
      render() {
        const {
          isAuthenticated,
          component: Component,
          ...props
        } = this.props
    
        return (
          <Route
            {...props}
            render={props =>
              isAuthenticated
                ? <Component {...props} />
                : (
                <Redirect to={{
                  pathname: '/login',
                  state: { from: props.location }
                }} />
              )
            }
          />
        )
      }
    }
    
    const PrivateRoute = connect(state => ({
      isAuthenticated: state.authReducer.isAuthenticated
    }))(PrivateRouteContainer)
    
    const Login = connect(null, dispatch => ({
      login: () => {
        dispatch(authSuccess())
        dispatch(push('/'))
      }
    }))(LoginContainer)
    
    const Home = connect(null, dispatch => ({
      logout: () => {
        dispatch(authFail())
        dispatch(push('/login'))
      }
    }))(HomeContainer)
    
    render(
      <Provider store={store}>
        <ConnectedRouter history={history}>
          <Switch>
            <Route path="/login" component={Login} />
            <PrivateRoute exact path="/" component={Home} />
          </Switch>
        </ConnectedRouter>
      </Provider>,
      document.getElementById('root'),
    )
    

    先看一下初始化部分

      const history = createHistory()
      const store = createStore(
      combineReducers({ routerReducer, authReducer }),
      applyMiddleware(routerMiddleware(history)),
    )
    首先在这里定义你想要的history
    然后将react-router-redux中对应的reduce以及这个中间件进行一下注入
    然后在这里将个刚才注册的history传递给ConnectedRouter用来发起一次dispatch
     <Provider store={store}>
        <ConnectedRouter history={history}>
          <Switch>
            <Route path="/login" component={Login} />
            <PrivateRoute exact path="/" component={Home} />
          </Switch>
        </ConnectedRouter>
      </Provider>,
    
    //绑定组件对应的dispatch用来触发一次action
    const Login = connect(null, dispatch => ({
      login: () => {
        dispatch(authSuccess())
        dispatch(push('/'))
      }
    }))(LoginContainer)
    
    const Home = connect(null, dispatch => ({
      logout: () => {
        dispatch(authFail())
        dispatch(push('/login'))
      }
    }))(HomeContainer)
    现在开始一点点来进行一下分析
    

    先来看一下ConnectedRouter部分

    import React, { Component } from "react";
    import PropTypes from "prop-types";
    import { Router } from "react-router";
    
    import { LOCATION_CHANGE } from "./reducer";
    
    class ConnectedRouter extends Component {
      static propTypes = {
        store: PropTypes.object,
        history: PropTypes.object.isRequired,
        children: PropTypes.node,
        isSSR: PropTypes.bool
      };
    
      static contextTypes = {
        store: PropTypes.object
      };
    
      handleLocationChange = (location, action) => {
        this.store.dispatch({
          type: LOCATION_CHANGE,
          payload: {
            location,
            action
          }
        });
      };
    
      componentWillMount() {
        const { store: propsStore, history, isSSR } = this.props;
        this.store = propsStore || this.context.store;
    
        if (!isSSR)
          this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
    
        this.handleLocationChange(history.location);
      }
    
      componentWillUnmount() {
        if (this.unsubscribeFromHistory) this.unsubscribeFromHistory();
      }
    
      render() {
        return <Router {...this.props} />;
      }
    }
    
    export default ConnectedRouter;
    
    

    重点来看一下ConnectedRouter的componentWillMount部分

     componentWillMount() {
        const { store: propsStore, history, isSSR } = this.props;
        this.store = propsStore || this.context.store;
    
        if (!isSSR)
          this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
    
        this.handleLocationChange(history.location);
      }
    
    从这里可以看到 如果当前不是isSSR服务端渲染的话 那么就会发起一个监听 用来监听当前路由的变化 如果history发生变化时 触发handleLocationChange事件
    

    handleLocationChange事件

    handleLocationChange = (location, action) => {
        this.store.dispatch({
          type: LOCATION_CHANGE,
          payload: {
            location,
            action
          }
        });
      };
    这个事件的作用很简单 当路由发生变化 就发起一次dispatch用来重新刷新页面 在这里得来先屡一下这个调用的先后顺序
    

    1.react-router-redux中间件的实现部分

      export default function routerMiddleware(history) {
      return () => next => action => {
        if (action.type !== CALL_HISTORY_METHOD) {
          return next(action);
        }
    
        const { payload: { method, args } } = action;
        history[method](...args);
      };
    }
    从这里可以看到 如果action的类型不为CALL_HISTORY_METHOD就直接放行 让下一个中间件去处理 如果当前类型等于CALL_HISTORY_METHOD则触发history
    下面来看一下CALL_HISTORY_METHOD这个究竟是个什么东西
    
    

    2.Action定义

    export const CALL_HISTORY_METHOD = "@@router/CALL_HISTORY_METHOD";
    
    function updateLocation(method) {
      return (...args) => ({
        type: CALL_HISTORY_METHOD,
        payload: { method, args }
      });
    }
    
    /**
     * These actions correspond to the history API.
     * The associated routerMiddleware will capture these events before they get to
     * your reducer and reissue them as the matching function on your history.
     */
    export const push = updateLocation("push");
    export const replace = updateLocation("replace");
    export const go = updateLocation("go");
    export const goBack = updateLocation("goBack");
    export const goForward = updateLocation("goForward");
    
    export const routerActions = { push, replace, go, goBack, goForward };
    
    链接上文就可以发现 我们所有调用的push replace的type都是CALL_HISTORY_METHOD
    举个简单的例子
    this.props.dispatch(push('./'))
    那么实际上我们发送的是一个这样的一个action
      return (...args) => ({
        type: ‘@@router/CALL_HISTORY_METHOD’,
        payload: { method:'push', args }
      });
    
    
    在连接上文的代码来看一下
    export default function routerMiddleware(history) {
      return () => next => action => {
        if (action.type !== CALL_HISTORY_METHOD) {
          return next(action);
        }
    
        const { payload: { method, args } } = action;
        history[method](...args);
      };
    }
    这时候 你会发现 这里实际上就变成了
    history.push(...args)这种方式去进行了调用
    
    聪明的你 我想应该已经发现这个react-router-redux的运行机制了
    当你用push或者replace进行任何操作的时候
    最终都会被转换成history中对应的方法
    然后因为我们对history进行了操作 所以会触发他对应的回调
    通过这种方式来做到了页面的跳转
    但是从现有的代码里面 你会发现 貌似没有任何一个地方会导致页面被重新渲染 别急 继续往下看
    

    react-router分析
    这里主要介绍Prompt Router Route Redirect
    其他的都是一些衍生的产物 就不过多介绍了
    先来看一下Router

      class Router extends React.Component {
      static propTypes = {
        history: PropTypes.object.isRequired,
        children: PropTypes.node
      };
    
      static contextTypes = {
        router: PropTypes.object
      };
    
      static childContextTypes = {
        router: PropTypes.object.isRequired
      };
    
      getChildContext() {
        return {
          router: {
            ...this.context.router,
            history: this.props.history,
            route: {
              location: this.props.history.location,
              match: this.state.match
            }
          }
        };
      }
    
      state = {
        match: this.computeMatch(this.props.history.location.pathname)
      };
    
      computeMatch(pathname) {
        return {
          path: "/",
          url: "/",
          params: {},
          isExact: pathname === "/"
        };
      }
    
      componentWillMount() {
        const { children, history } = this.props;
    
        invariant(
          children == null || React.Children.count(children) === 1,
          "A <Router> may have only one child element"
        );
    
        // Do this here so we can setState when a <Redirect> changes the
        // location in componentWillMount. This happens e.g. when doing
        // server rendering using a <StaticRouter>.
        this.unlisten = history.listen(() => {
          this.setState({
            match: this.computeMatch(history.location.pathname)
          });
        });
      }
    
      componentWillReceiveProps(nextProps) {
        warning(
          this.props.history === nextProps.history,
          "You cannot change <Router history>"
        );
      }
    
      componentWillUnmount() {
        this.unlisten();
      }
    
      render() {
        const { children } = this.props;
        return children ? React.Children.only(children) : null;
      }
    }
    
    export default Router;
    
    接我们上文讲到的话题 当你发起一个dispatch的时候 为什么页面就会发生变化呢
    这里来看一下关键代码
    
      componentWillMount() {
        const { children, history } = this.props;
    
        invariant(
          children == null || React.Children.count(children) === 1,
          "A <Router> may have only one child element"
        );
    
        // Do this here so we can setState when a <Redirect> changes the
        // location in componentWillMount. This happens e.g. when doing
        // server rendering using a <StaticRouter>.
        this.unlisten = history.listen(() => {
          this.setState({
            match: this.computeMatch(history.location.pathname)
          });
        });
      }
    看到这里 我想你应该就明白了吧
    你会发现 原来Router在这里也对history进行了一个监听
    只要你发起了一个dispatch并且正常调用了history以后 这边就会接收到这个更新 并且触发一次setState
    这里我们知道 如果父级刷新的时候 所有的children都会进行一次render计算 所以 页面的刷新 其实就是这么来的
    是不是比你想象的要简单很多呢
    
    

    再来看看route部分

    class Route extends React.Component {
      static propTypes = {
        computedMatch: PropTypes.object, // private, from <Switch>
        path: PropTypes.string,
        exact: PropTypes.bool,
        strict: PropTypes.bool,
        sensitive: PropTypes.bool,
        component: PropTypes.func,
        render: PropTypes.func,
        children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
        location: PropTypes.object
      };
    
      static contextTypes = {
        router: PropTypes.shape({
          history: PropTypes.object.isRequired,
          route: PropTypes.object.isRequired,
          staticContext: PropTypes.object
        })
      };
    
      static childContextTypes = {
        router: PropTypes.object.isRequired
      };
    
      getChildContext() {
        return {
          router: {
            ...this.context.router,
            route: {
              location: this.props.location || this.context.router.route.location,
              match: this.state.match
            }
          }
        };
      }
    
      state = {
        match: this.computeMatch(this.props, this.context.router)
      };
    
      computeMatch(
        { computedMatch, location, path, strict, exact, sensitive },
        router
      ) {
        if (computedMatch) return computedMatch; // <Switch> already computed the match for us
    
        invariant(
          router,
          "You should not use <Route> or withRouter() outside a <Router>"
        );
    
        const { route } = router;
        const pathname = (location || route.location).pathname;
    
        return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
      }
    
      componentWillMount() {
        warning(
          !(this.props.component && this.props.render),
          "You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
        );
    
        warning(
          !(
            this.props.component &&
            this.props.children &&
            !isEmptyChildren(this.props.children)
          ),
          "You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
        );
    
        warning(
          !(
            this.props.render &&
            this.props.children &&
            !isEmptyChildren(this.props.children)
          ),
          "You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
        );
      }
    
      componentWillReceiveProps(nextProps, nextContext) {
        warning(
          !(nextProps.location && !this.props.location),
          '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
        );
    
        warning(
          !(!nextProps.location && this.props.location),
          '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
        );
    
        this.setState({
          match: this.computeMatch(nextProps, nextContext.router)
        });
      }
    
      render() {
        const { match } = this.state;
        const { children, component, render } = this.props;
        const { history, route, staticContext } = this.context.router;
        const location = this.props.location || route.location;
        const props = { match, location, history, staticContext };
    
        if (component) return match ? React.createElement(component, props) : null;
    
        if (render) return match ? render(props) : null;
    
        if (typeof children === "function") return children(props);
    
        if (children && !isEmptyChildren(children))
          return React.Children.only(children);
    
        return null;
      }
    }
    
    export default Route;
    
    我们在上面已经知道了一个大概的dispatch刷新页面的流程以后 
    我们这边要继续深入一下 来了解一下大概的刷新逻辑
    这里主要是关注几点
    1.
        componentWillReceiveProps(nextProps, nextContext) {
        warning(
          !(nextProps.location && !this.props.location),
          '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
        );
    
        warning(
          !(!nextProps.location && this.props.location),
          '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
        );
    
        this.setState({
          match: this.computeMatch(nextProps, nextContext.router)
        });
      }
    
    这里你会发现 当我们刚才发起一个dispatch的时候 因为父执行了setState以后 导致所有的children都触发了一个更新
    这里子就会重新执行computeMatch来判断当前Route这个组件对应的children或者component render等函数是否要执行并且显示对应的页面
    

    Route computeMatch部分

      computeMatch(
        { computedMatch, location, path, strict, exact, sensitive },
        router
      ) {
        if (computedMatch) return computedMatch; // <Switch> already computed the match for us
    
        invariant(
          router,
          "You should not use <Route> or withRouter() outside a <Router>"
        );
    
        const { route } = router;
        const pathname = (location || route.location).pathname;
    
        return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
      }
    

    Route matchPath路径匹配规则

    const matchPath = (pathname, options = {}, parent) => {
      //如果options类型为string类型的话 则path变成options
      if (typeof options === "string") options = { path: options };
      
      const { path, exact = false, strict = false, sensitive = false } = options;
      //如果path为空的时候 则使用this.context的内容
     //这里就是404的关键所在
      if (path == null) return parent;
    
      const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
      const match = re.exec(pathname);
      //如果不匹配则直接返回null表示你当前这个组件的route不符合也就不会刷新出来
      if (!match) return null;
      
      //分解url
      const [url, ...values] = match;
      const isExact = pathname === url;
      //如果设置为强制匹配 但是实际结果不强制的话 也直接null不刷新显示
      if (exact && !isExact) return null;
      //一切正常的时候 返回对应的match来刷新页面
      return {
        path, // the path pattern used to match
        url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
        isExact, // whether or not we matched exactly
        params: keys.reduce((memo, key, index) => {
          memo[key.name] = values[index];
          return memo;
        }, {})
      };
    };
    
    export default matchPath;
    

    compilePath 部分代码

    const patternCache = {};
    const cacheLimit = 10000;
    let cacheCount = 0;
    
    const compilePath = (pattern, options) => {
     // 这里会将你的参数变成一个文本
      const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
       //这里会根据传进来的end strict sensitive来进行分类
      //如有两条数据
      1.end:true,strict:false, sensitive:true
      2.end:true,strict:false, sensitive:false
      那么就会在patternCache里面保存两条这个数据 并且将这个对应
      进行返回
      const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
      这里返回的这个cache也就是指定类的集合 里面放的都是同等类型的
      如果cache中有相同的话 就直接返回相同的 不进行其他多余的运算
      if (cache[pattern]) return cache[pattern];
      
      首次初始化的时候 这边肯定是为空的 所以这边进行运算 生成一个最新的值
      const keys = [];
      const re = pathToRegexp(pattern, keys, options);
      const compiledPattern = { re, keys };
      // 这里要注意 你的每次路由跳转以及初始化都会使用cacheCount
    最大值是10000 也就是说 如果超过了10000 则下次进行不会使用cache里面的值 而是每次都进行计算返回最新的数据
      if (cacheCount < cacheLimit) {
        cache[pattern] = compiledPattern;
        cacheCount++;
      }
     //返回最新的计算结果
      return compiledPattern;
    };
    
    image.png
    image.png

    Route render

    我们已经了解了整个router的更新机制 现在来看一下这个是如何被render的
      render() {
        const { match } = this.state;
        const { children, component, render } = this.props;
        const { history, route, staticContext } = this.context.router;
        const location = this.props.location || route.location;
        const props = { match, location, history, staticContext };
        只要你的match为true 就会显示出来
       但是这里比较特殊的是404那种为匹配到的页面
        如果你的props中没有path的话 会返回parent的match
        这个时候只要你有component就会直接给你显示出来
        
        if (component) return match ? React.createElement(component, props) : null;
    
        if (render) return match ? render(props) : null;
    
        if (typeof children === "function") return children(props);
    
        if (children && !isEmptyChildren(children))
          return React.Children.only(children);
    
        return null;
      }
    

    Redirect 部分源码讲解

    class Redirect extends React.Component {
      static propTypes = {
        computedMatch: PropTypes.object, // private, from <Switch>
        push: PropTypes.bool,
        from: PropTypes.string,
        to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
      };
    
      static defaultProps = {
        push: false
      };
    
      static contextTypes = {
        router: PropTypes.shape({
          history: PropTypes.shape({
            push: PropTypes.func.isRequired,
            replace: PropTypes.func.isRequired
          }).isRequired,
          staticContext: PropTypes.object
        }).isRequired
      };
      //只有你的父级为Route的时候 才会有staticContext
      isStatic() {
        return this.context.router && this.context.router.staticContext;
      }
    
      componentWillMount() {
        invariant(
          this.context.router,
          "You should not use <Redirect> outside a <Router>"
        );
        if (this.isStatic()) this.perform();
      }
    
      componentDidMount() {
        if (!this.isStatic()) this.perform();
      }
    
      componentDidUpdate(prevProps) {
        const prevTo = createLocation(prevProps.to);
        const nextTo = createLocation(this.props.to);
    
        if (locationsAreEqual(prevTo, nextTo)) {
          warning(
            false,
            `You tried to redirect to the same route you're currently on: ` +
              `"${nextTo.pathname}${nextTo.search}"`
          );
          return;
        }
    
        this.perform();
      }
    
      computeTo({ computedMatch, to }) {
       // 跳转的时候 分为两种
      如果有computedMatch的话 说明你有参数要传递
     如果没有的话直接使用to的数据
        if (computedMatch) {
          if (typeof to === "string") {
            return generatePath(to, computedMatch.params);
          } else {
            return {
              ...to,
              pathname: generatePath(to.pathname, computedMatch.params)
            };
          }
        }
    
        return to;
      }
    
      perform() {
        const { history } = this.context.router;
        const { push } = this.props;
        const to = this.computeTo(this.props);
       //如果push为真的话就push否则替换
        if (push) {
          history.push(to);
        } else {
          history.replace(to);
        }
      }
    
      render() {
        return null;
      }
    }
    
    export default Redirect;
    
    这个比较简单 就不细讲了 看一遍应该就明白了
    

    Prompt 部分源码

    这个组件唯一的作用就是在页面改变的时候 去给个提醒
    class Prompt extends React.Component {
      static propTypes = {
        when: PropTypes.bool,
        message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
      };
    
      static defaultProps = {
        when: true
      };
     //这句话意味着 你这个组件 永远不能是顶层组件 因为如果自己是顶层的话 是不会有context的
      static contextTypes = {
        router: PropTypes.shape({
          history: PropTypes.shape({
            block: PropTypes.func.isRequired
          }).isRequired
        }).isRequired
      };
    
      enable(message) {
        if (this.unblock) this.unblock();
    
        this.unblock = this.context.router.history.block(message);
      }
    
      disable() {
        if (this.unblock) {
          this.unblock();
          this.unblock = null;
        }
      }
    
      componentWillMount() {
        invariant(
          this.context.router,
          "You should not use <Prompt> outside a <Router>"
        );
    
        if (this.props.when) this.enable(this.props.message);
      }
    
      componentWillReceiveProps(nextProps) {
       只有当this.props.when不为空
       并且 前后两次显示的message都不一样的时候 才会开启
        if (nextProps.when) {
          if (!this.props.when || this.props.message !== nextProps.message)
            this.enable(nextProps.message);
        } else {
          this.disable();
        }
      }
    
      componentWillUnmount() {
        this.disable();
      }
    
      render() {
        return null;
      }
    }
    
    export default Prompt;
    

    ok 到这里 整个react-router源码就分析完毕 如果有什么问题的话 可以加我QQ:469373256

    相关文章

      网友评论

          本文标题:一起学react(5) 史上最全react-router4底层源

          本文链接:https://www.haomeiwen.com/subject/xtnvjftx.html