React 全家桶之 react-router

传统的多页模式

后端控制路由

在以前我们采用的都是一个 URL 对应一个 html 页面的方式,由后端或者服务器去做路由控制。当一个 URL 找不到对应的页面时就会返回 404.

单页模式(single page application,简称SPA)

单页应用现在是越来越流行了,单页应用和传统的多页应用相比,前端只有一个页面。在不刷新页面的前提下,通过监听 URL 的变化来渲染对应的 UI ,以此来实现多页的功能。

前端控制路由

如果使用的是 browserHistory,这里需要注意一个问题,我们在进入某个路由下的时候,页面刷新就会 404。这是因为服务器上只有一个 index.html,而你当前访问的比如 /home 在服务器上根本就找不到。所以需要让后端或者服务器把所有匹配不到的路径都指向这个 index.html,然后让前端 js 来控制路由。如 nginx 中可以这样配置

server {
  ...
  location / {
    try_files $uri /index.html
  }
}

为了实现 URL 变化而页面不刷新,可以有两种方式:

  1. 通过hash来实现,如 http://www.mysite.com#666。hash 的变化是不会引起浏览器刷新的,可以在页面 onload 的时候获取到location.hash,找到对应的 UI 模块渲染到页面上。另外还要 onhashchange 监听下 hash 的变化,来更新 UI。
  2. 通过 h5 提供的 history 来实现, 如 http://www.mysite.com/666。可以在页面 onload 的时候获取到 location.pathname,根据自己的规则找到对应的 UI 模块渲染到页面上。然后通过 pushState、replaceState 和 popState 实现前进和后退(这两位仁兄也不会引起浏览器刷新),再监听一下 popState 事件来处理 UI 层的更新

history 介绍

history 是一个 JavaScript 库,可让您在 JavaScript 运行的任何地方轻松管理会话历史记录。history 抽象出各种环境中的差异,并提供最小的 API ,使您可以管理历史堆栈,导航,确认导航以及在会话之间保持状态。

history 有三种实现方式:

  1. BrowserHistory:用于支持 HTML5 历史记录 API 的现代 Web 浏览器(请参阅跨浏览器兼容性)
  2. HashHistory:用于旧版Web浏览器
  3. MemoryHistory:用作参考实现,也可用于非 DOM 环境,如 React Native 或测试

react-router 的基本原理

这老兄其实就是一些 react 组件的集合,作用就是实现 URL 和 UI 的同步,内部是基于 history.js 来实现的浏览器历史记录管理。它包括了一下几个核心部分:

  • react-router,路由核心内容,Rrouter、Rroute、Redirect、withRouter都在这里
  • react-router-dom,基于 react-router,针对浏览器做的一些封装,如 Link 组件
  • react-router-native,基于 react-router,针对 ReactNative 做的一些封装 

几个核心的组件的作用

  • Router,包裹着 Route 组件,维护这一张路由与组件映射关系的路由表
  • Route,描述了每条路由与组件的匹配规则
  • Link,最终会被编译成<a>标签,它的 to、query、hash属性会被组合在一起并做为 href 属性

我们通过一个小例子来庖丁解牛一下其中原理

import { browserHistory } from 'react-router'
React.render((
  <Router history={ browserHistory }>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        {/* 使用 /messages/:id 替换 messages/:id */}
        <Route path="/messages/:id" component={Message} />
      </Route>
      <Link to="/about">About</Link>
      <Link to="/inbox">Inbox</Link>
    </Route>
  </Router>
), document.body)

下面我们一个一个看

Router

render() {
    return (
      <RouterContext.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
        }}
      />
    );
  }

Router 组件是基于 RouterContext 来实现的,也就是 react 中的上下文对象 context,只不过这个 context 是利用 'mini-create-react-context' 这个包去创建的,和原生的 context 用法有些不同,但是目的都是让数据可以跨组件纵向传递。

Provider 是生产数据的地方,这里会放入 history、location、match 三个对象,以便子孙组件可以方便的使用

Route

render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");

          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

          const props = { ...context, location, match };

          let { children, component, render } = this.props;

          // 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 (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }

Link

return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const { history } = context;

          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );

          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const method = replace ? history.replace : history.push;

              method(location);
            }
          };

          // React 15 compat
          if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
          } else {
            props.innerRef = innerRef;
          }

          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );

<Link> 组件最终会被转译成 <a> 标签,然后给  <a> 标签加一个 click 事件,并组织 <a> 标签的默认行为(跳转),然后执行 history.push(to) 或 history.replace(to) 来实现跳转。

withRouter

return (
      <RouterContext.Consumer>
        {context => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );

withRouter 是一个高阶组件,它的入参是一个组件假设为 A 。经过它封装之后返回一个新的组件,这个新组件会把 history、location、match 三个对象当做属性传递给组件 A。这也是为什么我们使用 withRouter 包装之后的组件,可以在内部使用 props.xxx 调用这三者的原因。

那么 withRouter 这个高阶组件又是从哪里获取的这三个对象呢?

还记得之前在 Router 里,使用 Provider 提供的这三个对象吗? 这里使用了 Consumer 来消费数据,其实就是通过 react 的上下文对象 context 来跨组件传递数据的。

问题

<Link>标签和<a>标签有什么区别

react-router:只更新变化的部分从而减少DOM性能消耗

而 <a> 标签是整个页面刷新,重新渲染

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