React-Router

232 阅读2分钟

HTML5 history

history.pushState

history.pushState 方法向当前浏览器会话的历史堆栈中添加一个状态(不刷新页面)

//state:状态对象是一个JavaScript对象,它与pushState()创建的新历史记录条目相关联,可通过popstate事件监听
//title: 目前浏览器都忽略
history.pushState(state, title, url)
popstate

通过popstate 监听回退

history.pushState({key: Date.now()}, '', '/time')
window.addEventListener('popstate', e => console.log(e), false)

再次载入到time.html url 时可以在console中看到这个 state对象

histoty.replaceState

histoty.replaceState 修改当前历史记录实体(不刷新页面,浏览器不记录该操作)

history.replaceState(state, title, url)

HashHistory

location.hash = 'time'
hashchange

通过hashchange 监听回退

window.addEventListener('hashchange', e => console.log(e),  false)

history.js

history.js 是一个独立的第三方类库,兼容不同的浏览器,统一API,react-router核心依赖history.js

使用例子

import React from 'react';
import {createBrowserHistory} from 'history'

const history = createBrowserHistory()
//监听路由变化
history.listen(data => {
    console.log(data)
})
export default props => {
    return <a onClick={() => history.push('time')}>go</a>
}

history.js暴露3个方法

createBrowserHistory

Browser history 是 React Route 推荐的 history。它使用浏览器中的 History API 用于处理 URL。

Browser history 需要服务端配合设置,例如我们刷新example.com/a,服务器会去寻找根目录下的a.html,实际上不存在这样的文件,我们的单页应用只有一个index.html

nginx配置,访问任何url都指向index.html

server {
  ...
  location / {
    try_files $uri /index.html
  }
}
部分源码

查看 createBrowserHistory 返回的history对象

  var history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref: createHref,
    push: push,
    replace: replace,
    go: go,
    goBack: goBack,
    goForward: goForward,
    block: block,
    listen: listen
  };
  return history;

push方法背后就是调用了HTML5 histoty.pushState

function supportsHistory() {
    var ua = window.navigator.userAgent;
    if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1) return false;
    return window.history && 'pushState' in window.history;
}

function push(path, state) {
    //省略...
    //canUseHistory = supportsHistory()
    if (canUseHistory) {
        //globalHistory = window.history;
        globalHistory.pushState({
            key: key,
            state: state
        }, null, href);
    }
    //省略...
}

通过history.js我们就有了统一的api来操作history

createHashHistory

Hash history 使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由

Hash history 服务端不需要配置

createMemoryHistory

Memory history 不会在地址栏被操作或读取,非常适合测试和其他的渲染环境像React Native

React-Router

React-Router可以实现url和页面的同步,也就是history和component同步,举个例子

import React from 'react';
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
class App extends React.Component{
    render(){
        return (
            <Router>
                <Route exact path="/" component={Index}/>
                <Route path="/list" component={List}/>
            </Router>
        )
    }
}

这里应用了react-router-dom router-react 和 react-router-dom 的区别如下

  • react-router实现了路由的核心功能
  • react-route-dom 将于 react-router 实现了浏览器下的一些功能,例如Link组件、BrowserRouter组件(基于HTML5 history)、HashRouter

那么 history变化 React是如何更新组件的呢

React Context

这里插播一下React Context,先看一个🌰

React Context可以让我们不需要层层传递props又可以获取到父类的数据

//context.js
import React from 'react';
export const data = {
    id: 0
}
export const DataContext = React.createContext(data)

//addKey.js
import React from 'react';
import {DataContext} from './context';

function AddKey() {
    return (
        <DataContext.Consumer>
          {({id, addId}) => (
              <button  onClick={addId}>

              {id}
              </button>
          )}
        </DataContext.Consumer>
  )
}

export default AddKey

//index.js
import React from 'react';
import {render} from 'react-dom'
import {DataContext, data} from './context';
import AddKey from './addKey';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.addId = () => {
      this.setState(state => ({
        id: state.id  + 1
      }))
    }
    this.state = {
      id: data.id,
      addId: this.addId,
    }
  }

  render() {
    return (
      <DataContext.Provider value={this.state}>
         <AddKey />
      </DataContext.Provider>
    );
  }
}



render(<App/>, document.getElementById('root'))

Router的实现

Router组件是一个路由容器

var Router =
/*#__PURE__*/
function (_React$Component) {
  _inheritsLoose(Router, _React$Component);

  Router.computeRootMatch = function computeRootMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  };

  function Router(props) {
    var _this;

    _this = _React$Component.call(this, props) || this;
    _this.state = {
      location: props.history.location
    }; // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.

    _this._isMounted = false;
    _this._pendingLocation = null;

    if (!props.staticContext) {
      _this.unlisten = props.history.listen(function (location) {
        if (_this._isMounted) {
          _this.setState({
            location: location
          });
        } else {
          _this._pendingLocation = location;
        }
      });
    }

    return _this;
  }

  var _proto = Router.prototype;

  _proto.componentDidMount = function componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({
        location: this._pendingLocation
      });
    }
  };

  _proto.componentWillUnmount = function componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  };

  _proto.render = function render() {
    return React.createElement(context.Provider, {
      children: this.props.children || null,
      value: {
        history: this.props.history,
        location: this.state.location,
        match: Router.computeRootMatch(this.state.location.pathname),
        staticContext: this.props.staticContext
      }
    });
  };

  return Router;
}(React.Component);

{
  Router.propTypes = {
    children: PropTypes.node,
    history: PropTypes.object.isRequired,
    staticContext: PropTypes.object
  };

  Router.prototype.componentDidUpdate = function (prevProps) {
     warning(prevProps.history === this.props.history, "You cannot change <Router history>") ;
  };
}

可以看到router利用了react context向其children 共享属性,通过 render 可以看到

 _proto.render = function render() {
    return React.createElement(context.Provider, {
      children: this.props.children || null,
      value: {
        history: this.props.history,
        location: this.state.location,
        match: Router.computeRootMatch(this.state.location.pathname),
        staticContext: this.props.staticContext
      }
    });
  };
  //对应
 _proto.render = function render() {
    return(
        <context.Provider value={{
            history: this.props.history,
            location: this.state.location,
            match: Router.computeRootMatch(this.state.location.pathname),
            staticContext: this.props.staticContext
        }}>
            {this.props.children || null}
        </context.Provider>
    )
};

监听路由变化,调用了history.js listen API, 路由变化时,执行setState, 将当前的路由数据传递到子的Route组件。我们知道父组件执行setState,父组件下的子组件会渲染

 _this.unlisten = props.history.listen(function (location) {
        if (_this._isMounted) {
          _this.setState({
            location: location
          });
        } else {
          _this._pendingLocation = location;
        }
      });

打印看看这个location的数据结构

{
  hash: ""
  key: "ud9ozl"
  pathname: "/state"
  search: ""
  state: undefined
}

Route

Route定义实际的路由,根据Router路由容器传递的location.pathname当前Route的path进行匹配,匹配上了render

var Route =
  /*#__PURE__*/
  function (_React$Component) {
    _inheritsLoose(Route, _React$Component);

    function Route() {
      return _React$Component.apply(this, arguments) || this;
    }

    var _proto = Route.prototype;

    _proto.render = function render() {
      var _this = this;

      return React__default.createElement(context.Consumer, null, function (context$1) {
        !context$1 ?  invariant(false, "You should not use <Route> outside a <Router>")  : void 0;
        var location = _this.props.location || context$1.location;
        var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
        : _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match;

        var props = _extends({}, context$1, {
          location: location,
          match: match
        });

        var _this$props = _this.props,
            children = _this$props.children,
            component = _this$props.component,
            render = _this$props.render; // Preact uses an empty array as children by
        // default, so use null if that's the case.

        if (Array.isArray(children) && children.length === 0) {
          children = null;
        }

        return React__default.createElement(context.Provider, {
          value: props
        }, props.match ? children ? typeof children === "function" ?  evalChildrenDev(children, props, _this.props.path)  : children : component ? React__default.createElement(component, props) : render ? render(props) : null : typeof children === "function" ?  evalChildrenDev(children, props, _this.props.path)  : null);
      });
    };

    return Route;
  }(React__default.Component);