React-Router知识理解总结

831 阅读6分钟

react-Router

通常我们的单页面应用,都是通过前端在项目中配置路由,然后浏览器根据不同的URL显示不同的视图, 要搞明白这里原理,我们需要先回顾一下浏览器提供的两个API window.locationwindow.history:

window.location

WindowDocument对象的location属性均指向一个Location对象,代表当前文档的页面地址,其中href属性包含该文档的完整URL,Location对象的其他属性都是对该URL的各部分进行描述

image-20201207192119816

Location对象的属性

  • hash: 哈希值,开头有一个“#”。
  • host: 当前URL的主机名和端口,一些默认端口不显示(http: 80、https:443、ftp:21、ssh:22、telnet:23)
  • hostname: 当前URL的主机名
  • href: 返回当前页面的URL的完整文本
  • **origin: **当前URL的域名包括协议、主机、端口号
  • pathname: URL的路径名部分,开头有一个“/”。
  • search: 请求参数,开头有一个“?”。

Location对象的方法

  • assign(string url): 加载并显示指定url的内容,等同于为location对象的href属性设置url值
  • reload(boolean cache): 重新加载当前url显示的文档; 函数参数默认为false,走正常的缓存机制,如果缓存规则命中生效,则直接取缓存中的内容; 当参数传入true时,则会绕过缓存 重新向服务器加载资源
  • replace(string url): 替换当前url,与assign()不同的是用replace()改变页面不会在History中产生历史记录,也就意味着用户无法通过前进/后退来到该页面
  • toString(): 返回当前Location对象的url值

window.history

History对象表示窗口的浏览历史,提供了一些属性和方法可以使得浏览器能够在之前的浏览记录中前进、后退;

image-20201209204106002

history对象提供的属性不多,就一个state、一个length;而且浏览器为了保护隐私也没有对外暴露浏览记录的真实url,不过为我们提供了前进后退的方法;go()back()forward();

HTML5开始history还引入了pushState()replaceState()方法,使用户可以添加和修改历史记录中的条目;

history.pushState(stateObj, title, ?url)

stateObj: 状态对象; 当一个应用进入一个新的状态的时候(例如hash值的改变),它会调用history.pushState(stateObj, )方法将该状态添加到浏览器的浏览历史记录中,该方法的第一个参数是一个对象, 该对象包含用于恢复当前文档状态所需的所有信息; 该方法的第二个参数是一个可选的标题,浏览器可以使用它来表识浏览记录中保存的状态;该方法的第三个参数是一个可选的URL,表示当前状态的位置,就是URL最后面的那一部分(最后一个/后面的部分);

除了pushState()还有replaceState(),该方法和pushState接受一样的参数,但不同的是,它不是将状态添加到浏览历史记录中,而是在历史记录中替换当前的状态, history对象的length值不会改变

当用户通过history.go()history.forward()history.back()这些前进后退方法,改变当前的状态时,浏览器会触发window对象上的一个popstate事件。

LocationHistory对象都有各自的方式去改变页面的url但同时不刷新页面;都有对应的事件去监听url的变化;location对象是通过改变hash值,然后监听window对象上的hashchange事件,history对象是通过改变状态值,然后通过监听window对象上的popstate事件;但不同的是两则触发的条件不同:hashchange事件只能监听到url hash值的变化, 而popstate事件除了能监听到hash值的变化还能监听到url的变化(除了pushState(data, title, ?url), replaceState(data, title, ?url)这两个函数导致的)

hashchangepopstate
location.hash
history.pushState()××
history.popState()××
history.go()×
history.back()×
history.forward()×

回想一下我们应用中的路由,要么是通过hash实现,将整个pathname部分作为hash

通过hash实现前端路由原理

hash值是Location对象属性的一部分,可以通过window.location.hash来设置、获取hash值;注意hash值的变化,虽然导致页面URL整体的变化,但是并不会引起页面的刷新。

改变hash的方式

  1. 直接通过设置window.location.hash的值

    //设置hash值时 前面不需要带# 也不需要带/, 系统会自动添加; 最终的值就是 #/123
    window.location.hash = "123" 
    
  2. 通过a标签的href属性设置锚点方式

    <a href="#123">123</a>
    <a href="#456">456</a>
    
  3. 浏览器的本身自带的前进后退按钮

    //浏览器的前进后退按钮操作等同于执行
    history.forward()
    history.back()
    
image-20201231115943485

根据控制台打印也可以看出上面的这些使hash值改变的方法,虽然会导致页面整体url的变化,但都不会导致浏览器刷新,我们可以通过监听浏览器的hashchange的事件来捕获,进而才能实现路由与视图对应;

console.log("Initial Page") //只在页面初始化时打印了一次
//监听hash值变化的事件
window.onhashchange = function(event) {
  console.log(event)
}
//或者
window.addEventListener('hashchange', (event) => {
  console.log(event)
})

通过History实现前端路由原理

上面也提到过History接口从HTML5开始,引入history.pushState()history.replaceState()添加和修改历史记录中的条目;通过window.onpopstate来监听历史记录的变化

改变历史记录的方式

  1. 改变hash值不仅能触发window.onhashchange事件, 同时也能触发window.onpopstate事件,因此上面通过Hash实现前端路由原理中所有能改变hash的方式都可以

    //直接通过设置`window.location.hash`的值
    location.hash = "/bar"
    //通过a标签的href属性设置锚点方式
    <a href="#123">123</a>
    
    
  2. 浏览器本身的前进后退按钮

    //浏览器的前进后退按钮操作等同于执行
    history.forward()
    history.back()
    
  3. Html5中为history新添加的方法

    history.pushState()
    history.replaceState()
    // 注意这些方法虽然可以改变history的历史记录,但是并不会触发popstate事件
    

监听方式

// 为window对象的onpopstate属性设置监听函数
window.onpopstate = (e) => {
  console.log("onpopstate", e)
}
//添加监听器函数
window.addEventListener('popstate', (e) => {
  console.log("popstate", e)
})

React Router

Router是一个路由器,例如常用的HashRouterBrowserRouter;这些路由器通常位于根组件,作为React组件的最上层容器存在;方便在层级组件中进行值的传递,使内部子组件能够使用来自顶层组件中的属性;说到这里有没有想到什么?Context呀...,不然人家是怎么在深层级的子组件中获取来自顶层组件的属性?当然不是通过props的层层传递。React-Router内部是基于History库开发的, 区别于浏览器为我们提供的window.historyHistory库是在window.history的基础上进行了一次封装,使其能够兼容不同浏览器, 因此后者也会拥有前者的的一些属性和方法。打印二者区别:

image-20210108162426981
//一个简版的BrowserRouter
import { Component } from "react";
import { createBrowserHistory } from "history";

const RouterContext = React.createContext();

class BrowserRouter extends Component {
  constructor(props) {
    super(props);
    this.history = createBrowserHistory(this.props);
    this.state = {
      location: this.history.location,
    };
    this.unlisten = this.history.listen((location) => {
      this.setState({ location });
    });
  }
  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }
  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.history,
          location: this.state.location,
        }}
      >
        {this.props.children}
      </RouterContext.Provider>
    );
  }
}

Route是一个路由组件,用于定义路由与组件之间的映射关系;

const RouterContext = React.createContext();
function Route(props) {
    const ctx = useContext(RouterContext);
    const { path, component: Cmp } = props;
    const { location } = ctx;
    let match = path === location.pathname;
    return match ? <Cmp /> : null;
}