你好,前端路由

1,643 阅读7分钟
作者:QP

1、什么是前端路由?

路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。 在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化 引起 UI 更新(无需刷新页面)。

2、如何实现前端路由?

a.hash 实现

hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新。通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:

  • 通过浏览器前进后退改变 URL、
  • 通过a标签改变 URL
  • 通过window.location改变URL。

如上这几种情况改变 URL 都会触发 hashchange 事件

b.history 实现

history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。

  • history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或a标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和a标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

3、hash 与 history 模式的对比

hash路由优缺点

优点

  • 实现简单,兼容性好(兼容到ie8)
  • 绝大多数前端框架均提供了给予hash的路由实现
  • 不需要服务器端进行任何设置和开发
  • 除了资源加载和ajax请求以外,不会发起其他请求

缺点

  • 对于部分需要重定向的操作,后端无法获取hash部分内容,导致后台无法取得url中的数据,典型的例子就是微信公众号的oauth验证
  • 服务器端无法准确跟踪前端路由信息
  • 对于需要锚点功能的需求会与目前路由机制冲突

History(browser)路由 优缺点

优点

  • 对于重定向过程中不会丢失url中的参数。后端可以拿到这部分数据
  • 绝大多数前段框架均提供了browser的路由实现
  • 后端可以准确跟踪路由信息
  • 可以使用history.state来获取当前url对应的状态信息

缺点

  • 兼容性不如hash路由(只兼容到IE10)
  • 需要后端支持,每次返回html文档

4、react-router 实现原理

a.首先,我们先来了解一下Router组件

Router 组件是所有路由组件的父级组件,为子组件提供当前路由状态并监听路由改变并触发重新渲染。 history.listen 能够监听路由的变化并执行回调事件。 每次的路由变化,触发回调事件 this.computeMatch, 相比于在 setState 里做的操作,setState 本身的意义更大 —— 每次路由变化 -> 触发顶层 Router 的回调事件 -> Router 进行 setState -> 向下传递 nextContext(context 中含有最新的 location)-> 下面的 Route 获取新的 nextContext 判断是否进行渲染。

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)

      });

    });

  }

b.再来看下 Route ,Route 的作用是匹配路由,并传递给要渲染的组件 props。

Route 接受上层的 Router 传入的 context,Router 中的 history 监听着整个页面的路由变化,当页面发生跳转时,history 触发监听事件,Router 向下传递 nextContext,就会更新 Route 的 props 和 context 来判断当前 Route 的 path 是否匹配 location,如果匹配则渲染,否则不渲染。是否匹配的依据就是 computeMatch 这个函数,在下文会有分析,这里只需要知道匹配失败则 match 为 null,如果匹配成功则将 match 的结果作为 props 的一部分,在 render 中传递给传进来的要渲染的组件。

 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)

    });

  }

c.接下来看一下 Route 的 render 部分。

提供了三种渲染组件的方法:component props,render props 和 children props,渲染的优先级也是依次按照顺序,如果前面的已经渲染后了,将会直接 return。

  • component (props) —— 由于使用 React.createElement 创建,所以可以传入一个 class component。
  • render (props) —— 直接调用 render() 展开子元素,所以需要传入 stateless function component。
  • children (props) —— 其实和 render 差不多,区别是不判断 match,总是会被渲染。 children(子元素)—— 如果以上都没有,那么会默认渲染子元素,但是只能有一个子元素。
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;

  }

d. 再来看下 matchPath,matchPath 返回的是一个如下结构的对象,这些信息将作为匹配的参数传递给 Route

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;
    }, {})
  };

e. 最后,我们来看下 Link 标签代码

render() {
    const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars

    invariant(
      this.context.router,
      "You should not use <Link> outside a <Router>"
    );

    invariant(to !== undefined, 'You must specify the "to" property');

    const { history } = this.context.router;
    const location =
      typeof to === "string"
        ? createLocation(to, null, null, history.location)
        : to;

    const href = history.createHref(location);
    // 最终创建的是一个 a 标签
    return (
      <a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
    );
  }

可以看到 Link 最终还是创建一个 a 标签来包裹住要跳转的元素,但是如果只是一个普通的带 href 的 a 标签,那么就会直接跳转到一个新的页面而不是 SPA 了,所以在这个 a 标签的 handleClick 中会 preventDefault 禁止默认的跳转,所以这里的 href 并没有实际的作用,但仍然可以标示出要跳转到的页面的 URL 并且有更好的 html 语义。

整理一,整个路由跳转的过程有两件事需要完成:

  • 路由的改变
  • 页面的渲染部分的改变

1、在最一开始 mount Router 的时候,Router 在 componentWillMount 中 listen 了一个回调函数,由 history 库管理,路由每次改变的时候触发这个回调函数。这个回调函数会触发 setState。
2、当点击 Link 标签的时候,实际上点击的是页面上渲染出来的 a 标签,然后通过 preventDefault 阻止 a 标签的页面跳转。
3、Link 中也能拿到 Router -> Route 中通过 context 传递的 history,执行 hitsory.push(to),这个函数实际上就是包装了一下 window.history.pushState(),是 HTML5 history 的 API,但是 pushState 之后除了地址栏有变化其他没有任何影响,到这一步已经完成了目标1:路由的改变。
4、第1步中,路由改变是会触发 Router 的 setState 的,在 Router 那章有写道:每次路由变化 -> 触发顶层 Router 的监听事件 -> Router 触发 setState -> 向下传递新的 nextContext(nextContext 中含有最新的 location)
5、下层的 Route 拿到新的 nextContext 通过 matchPath 函数来判断 path 是否与 location 匹配,如果匹配则渲染,不匹配则不渲染,完成目标2:页面的渲染部分的改变

5、备注:

react-router 主要是利用底层 history 模块的机制,通过结合 react 的架构机制做一层包装,实际自身的内容并不多,但其包装的思想很值得学习

history 库借鉴了浏览器 history 的概念,对其进行封装或实现,使得开发者可以在任何js运行环境中实现历史会话操作,react-router 使用 history 库对其路由状态进行监听和管理,使得他能在非浏览器环境下运行。