前端路由及React-Router解读

1,563 阅读5分钟

前前言

本文来自推啊前端团队 刘爽 同学,主要介绍一下 前端路由相关内容和解读一下React-Router源码。欢迎在评论区讨论哦!!!

什么是路由

通常会在网络工程里面听到路由这个词,前端工程化后路由的概念用语页面的跳转,浏览器监测到路由的变化在页面显示路由所对应的页面。在早期,路由的概念时根据URL的变更重新渲染页面布局和内容的过程,而且这个过程时由服务器端实现的。他所描述的是URL 和 函数之间的映射关系。 image.png 在web前端的单页面应用,路由描述的是URL 与 UI 之间的映射关系。这种映射关系显著的特点是URL的改变不会引起页面的刷新。 **

两种路由方式

在前面也说了web前端路由的特点,URL 改变的目的是为了更新UI,与此同时不能够刷新页面,想要更新页面视图UI,我们就需要监听 URL 的变化。所以在前端实现路由引擎需要注意两点

  1. URL 的改变不刷页面,
  2. 如何监听 URL 的改变

在前端领域有两种路由方式能够实现以上标准

hash 路由

hash 路由就是我们常说的锚点。即在URL后面添加#,# 号后面就是hash路由部分。可以通过监听事件监听hash路由的变化

window.addEventListener("hashchange", function(){
	console.log("路由改变")
})

window.onhashchange = function(){
	console.log("路由改变")
}

history 路由

history对象表示,当前窗口用户的导航记录,该对象不会向外暴露用户访问过的URL,但是可以通过方法实现前进和后退。HTML5 添加了新方法 pushState和replaceState,表示添加和替换历史记录的条目,语法如下。

history.pushState(state, title[, url])
history.replaceState(state, title[, url])
- state: 一个于指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数中。如果不需要这个对象,此处可以填null
- title: 当前大多数浏览器都忽略此参数
- url: 新历史记录条目的URL由此参数指定,新网址必须与当前网址同源

通过popstate监听路由改变

window.addEventListener('popstate',function(){
	console.log("路由改变")
})

React-Router

这一栏我们只讨论源码,对应的版本是5.2 history:负责浏览器页面,链接改变通知当前页面location对象发生了改变,开发者根据变化渲染内容。 Router:负责监听页面对象发生了改变,并开始重新渲染页面 Route:页面开始渲染后,根据具体的页面location信息展示具体路由地址对应的内容。

BrowserRouter 和 Router

最开始使用 react-router 的时候需要的方式如下:

import { createBrowserHistory } from 'history'
import { Router } from 'react-router'

const BrowserRouter = React.cloneElement(Router, { history: createBrowserHistory() })

export default () => (
	<BrowserRouter>
  	...
  </BrowserRouter>
)
// 或者

export default () => (
	<Router history={createBrowserHistory()}>
  ...
  </Router>
)

react-router V4 版本之后可以直接使用 BrowserRouter 如下所示

import { BrowserRouter } from 'react-router-dom'

export default () => (
	<BrowserRouter>
  	...
  </BrowserRouter>
)

BrowserRouter 相关源码如下: 可以看到 BrowserRouter 就是对 Router 组件的一层封装,传入history 属性。其主要部分还是Router的源码

// packages/react-router-dom/modules/BrowserRouter.js

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
// packages/react-router/modules/Router.js
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      // BrowserRouter 传入的 history
      location: props.history.location
    };

    if (!props.staticContext) {
      // 监听URL是否改变
      this.unlisten = props.history.listen(location => {
        // 这里的内容不影响整体逻辑
      });
    }
  }

  // 使用上下文把history location 等信息传入到children
  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

**总结:**根据源码可以看到 Router 处理的内容并不多,1. 定义一个上下文把相关信息传入children;2. 监听URL的变化改变当前的state。

Route

Route 的源码也十分简单,根据传入的信息匹配要显示的页面,如下所示

// packages/react-router/modules/Route.js
class Route extends React.Component {
  // Consumer 上下文,用于接手上文传入信息
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");
					// location 信息 如果用户传入使用用户传入信息,否则使用Provider 传入
          const location = this.props.location || context.location;
    			// 核心内容:根据URL路径匹配
    			// match: {path,url,isExact,params}
    			// 如果没有匹配到 就是上文的 computeRootMatch 内容
          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;
    			
    			// 这里略过了一些判断... 

			    let { children, component, render } = this.props;
					// 开始渲染 children, 一共有三种渲染的方式
    			// 1. <Route exact path="/" component={Home} />
    			// 2. <Route exact path="/" render={props=>{return <Home />}} />
    			// 3. <Route exact path="/"><Home /> </Route>
          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

**总结:**Route 就是用来渲染组件的,根据匹配的URL 即 Route 的path 属性,匹配出需要渲染的内容。在渲染时有三种方式 render component children 。在里面使用三目运算,写了很长,其实并不难理解。先判断有没有children,在判断chidren 是不是一个函数,这是渲染children的逻辑,如果没有children 判断有没有component ,最后判断有没有render方法。

Switch

如果 Route 组件被 Switch 包裹,那么匹配到的URL 会返回 被包裹的Route 的第一个匹配到的元素。核心代码如下:

// packages/react-router/modules/Switch.js
React.Children.forEach(this.props.children, child => {
  if (match == null && React.isValidElement(child)) {
    element = child;

    const path = child.props.path || child.props.from;

    match = path
      ? matchPath(location.pathname, { ...child.props, path })
    : context.match;
  }
});

return match
  ? React.cloneElement(element, { location, computedMatch: match })
: null;

**总结:**Switch 里面的内容并不多,就是把包裹的Route做了一个遍历,返回第一个匹配到的内容

总结:

整体来讲react-router 的源码并不是很难理解,都是一些简单的判断。相对于这一篇内容,对于react-router的如何使用并没有介绍,只是对源码做一个简单的解读。下一篇就实现一个简易版react-router。实现前文所介绍的api。