阅读 1524

不看看react-router源码?真的懂路由咩

前言

被业务追赶着的,一下子停更了一个月(不会有人认为是偷懒没写吧不会吧),最近的重构工作到了整理路由的这一块,用到了router4整体,就整理了一下路由相关内容,路由可以说是我们接触到最多的东西,当然不能只会用了。

脑图🔽

路由是啥捏

司尘(抢先回答):就是实现浏览器在不刷新界面的情况下,切换界面。

机智的读者:这只是前端路由实现的效果,特地整理当然要详细咯

路由概念

路由就是url到界面的一个映射,这样一想我们每个页面去请求也可以实现路由,所以路由分两种

  • 服务器端路由
    类似这种:https://www.abc.com/index.html,直接给你返回一个页面,他的过程当然就是网络请求页面的过程了:输入url浏览器干了啥?

  • 客户端路由(前端路由)
    要解决后端路由的请求频繁问题那么我们就要实现

    • 改变url浏览器不发请求
    • 在不刷新界面的情况下改变url
    • 可以监听到url的变化

    司尘:这么复杂咋解决嘛?

    机智的读者:前端这不给了两种模式嘛都给你实现了

    司尘:哦那没事了

前端路由方式

hash模式

跳转实现

location对象中的hash值可以让帮助实现

  • 直接改变location.hash的值

  • 通过a标签实现

<a href="#login">edit</a>
复制代码
监听hash

js提供给我们两种方式监听hash变化

window.onhashchange = function(hash) {
   console.log(hash)
}
window.addEventListener('hashchange', function(hash) {
   console.log(hash)
})
复制代码
hash劣势

为啥早期使用hash而现在基本都没有地方使用了呢?能想到的他的优点也就只有兼容性好了

  • 最直观的,多个#看着就是难受,那我不管真的感觉丑(把肤浅打在公屏上)

  • 配合后端困难 对于部分需要重定向的操作,后端无法获取hash部分内容,导致后台无法取得url中的数据

  • 服务器端无法准确跟踪前端路由信息

  • 对于需要锚点功能的需求会与目前路由机制冲突

history模式

这个模式就是基于HTML5的History接口,理所当然兼容性会差一点。 我们路由场景就只有:前进、后退、指定到某个页面。那么对应到的方法我们就落列一下:

  • History.forward()
  • History.back()
  • History.go()
  • History.pushState()
  • History.replaceState()

这三种方法看起来就好像是在操作一个已有的路由对象,我们初次进来怎么会有嘛,所以有history.pushState方法,点击路由跳转就会去往堆栈里面添加路由信息,History.forward()、History.back()、History.go() 这三个方法使用时会调用popState方法,在堆栈中进行操作,所以也不会去刷新界面

React-router4.0

到今天的正题至于他的使用以及API这边就不做介绍了,官方文档里都有,我们直接来通过源码看一下,冲!

React-router中将上面的History类的构建方法,独立成一个node包,包名为history,路由的实现history完全继承了History接口,因此拥有History中的所有的属性和方法。

先来一个简单的demo

class Square extends React.Component {
    render() {
        return (
            <BrowserRouter>
            <div>
              <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/page">page</Link></li>
              </ul>

              <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page" component={Page}/>
              </Switch>
            </div>
          </BrowserRouter>
        )
    }
}
复制代码

路由相关信息方法肯定是在props上的我们打印看一下,这是我的两次操作

  • 点击跳转
  • 浏览器回退键
{
    history: {...}, // history库提供的方法
    match: {
        isExact:true // 是否为严格匹配
        path: "/page", // 用来匹配的
        url: "/page", // 当前的URL
        params: {},  // 路径中的参数
    },
    location: {
        hash: "" // hash
        key: "lm9hdk" // 唯一的key
        pathname: "/page" // URL 中路径部分
        search: "" // URL 参数
        state: undefined // 路由跳转时传递的参数 state
    }
    staticContext: undefined  // 用于服务端渲染
}
复制代码

BrowserRouter和HashRouter

代码看来BrowserRouter就是注册了history然后能够让子路由内容全部能获取到history,HashRouter其实也大同小异。

import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";


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

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
复制代码

这样看来我们要追溯到Router组件了

Router组件

比作是一个监听器我们这样去理解会好一些,每一次改变我们通过Router组件的监听然后去改变props里面的值

function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };
    this._isMounted = false;
    this._pendingLocation = null;

    // staticContext为true时,为服务器端渲染那就不需要前端去操作路由咯
    if (!props.staticContext) {
      // 监听listen,location改变触发
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          // 更新 location
          this.setState({ location });
        } else {
          // 否则存储到_pendingLocation, 等到didmount再setState避免可能报错
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    // 赋值为true,且不会再改变
    this._isMounted = true;
    // 更新location
    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
  // 取消监听
    if (this.unlisten) this.unlisten();
  }
  
  render() {
    const context = getContext(this.props, this.state);
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={context}
      />
    );
  }
}
复制代码

Route组件

当url改变的时候,将path属性与改变后的url做对比,如果匹配成功,则渲染该组件的componet或者children属性所赋值的那个组件

class Route extends React.Component {
  ...
  componentWillReceiveProps(nextProps, nextContext) {
    ...
    this.setState({
      match: this.computeMatch(nextProps, nextContext.ro         uter)
    });
  }
  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;
  }
}
复制代码

Switch

前面的这几个组件好像已经是实现了路由,那我们为啥要在Route组件外面包裹一个Switch呢?有点累了看一眼女友

按照路由匹配规则,URL是/page 那么Home组件也会被渲染出来。

Switch 是用来嵌套在 Route 的外面,当 Switch 中的第一个 Route 匹配之后就不会再渲染其他的 Route 了,我们直接把他理解为一个精准匹配路由的工具这样比较好理解

  render() {
    const { route } = this.context.router;
    const { children } = this.props;
    const location = this.props.location || route.location;

    let match, child;
    React.Children.forEach(children, element => {
      if (match == null && React.isValidElement(element)) {
        const {
          path: pathProp,
          exact,
          strict,
          sensitive,
          from
        } = element.props;
        const path = pathProp || from;

        child = element;
        match = matchPath(
          location.pathname,
          { path, exact, strict, sensitive },
          route.match
        );
      }
    });

    return match
      ? React.cloneElement(child, { location, computedMatch: match })
      : null;
  }
复制代码

Switch是通过matchPath这个函数来判断是否匹配成功,这个方法就是做了判断 location 是否符合 path。

Link

最后那就是实现我们路由跳转的Link组件了,想想他就不会复杂,就是去调了一下history的方法嘛,我们一起康康

class Link extends React.Component {
  // 定义一个点击方法
  handleClick = event => {
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented && // 组织违约
      event.button === 0 && // 忽略一切,除了左键点击
      !this.props.target && // 让浏览器处理"target=_blank"等
      !isModifiedEvent(event) // 忽略点击修改键
    ) {
      event.preventDefault();

      const { history } = this.context.router;
      const { replace, to } = this.props;

      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };
  
  render() {
    const { replace, to, innerRef, ...props } = this.props;
    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} />
    );
  }
}
复制代码

总结

自己之前还只是会去用,甚至switch这种组件都不知道是干嘛的就想着要写在那里,看了具体实现之后其实才懂得他的整个流程,走了一遍还是清晰了很多。最近碰到的一个登陆加密问题下一篇想写一下https为啥不会被攻击,加油肝!