react-Router
通常我们的单页面应用,都是通过前端在项目中配置路由,然后浏览器根据不同的URL
显示不同的视图, 要搞明白这里原理,我们需要先回顾一下浏览器提供的两个API window.location
、window.history
:
window.location
Window
或Document
对象的location
属性均指向一个Location对象,代表当前文档的页面地址,其中href属性包含该文档的完整URL,Location
对象的其他属性都是对该URL
的各部分进行描述
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
对象表示窗口的浏览历史,提供了一些属性和方法可以使得浏览器能够在之前的浏览记录中前进、后退;
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事件。
Location
和History
对象都有各自的方式去改变页面的url但同时不刷新页面;都有对应的事件去监听url的变化;location
对象是通过改变hash
值,然后监听window
对象上的hashchange
事件,history
对象是通过改变状态值,然后通过监听window
对象上的popstate
事件;但不同的是两则触发的条件不同:hashchange事件只能监听到url hash值的变化, 而popstate事件除了能监听到hash值的变化还能监听到url的变化(除了pushState(data, title, ?url), replaceState(data, title, ?url)这两个函数导致的)
hashchange | popstate | |
---|---|---|
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的方式
-
直接通过设置
window.location.hash
的值//设置hash值时 前面不需要带# 也不需要带/, 系统会自动添加; 最终的值就是 #/123 window.location.hash = "123"
-
通过a标签的href属性设置锚点方式
<a href="#123">123</a> <a href="#456">456</a>
-
浏览器的本身自带的前进后退按钮
//浏览器的前进后退按钮操作等同于执行 history.forward() history.back()
根据控制台打印也可以看出上面的这些使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
来监听历史记录的变化
改变历史记录的方式
-
改变hash值不仅能触发
window.onhashchange
事件, 同时也能触发window.onpopstate
事件,因此上面通过Hash实现前端路由原理中所有能改变hash的方式都可以//直接通过设置`window.location.hash`的值 location.hash = "/bar" //通过a标签的href属性设置锚点方式 <a href="#123">123</a>
-
浏览器本身的前进后退按钮
//浏览器的前进后退按钮操作等同于执行 history.forward() history.back()
-
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
是一个路由器,例如常用的HashRouter
、BrowserRouter
;这些路由器通常位于根组件,作为React
组件的最上层容器存在;方便在层级组件中进行值的传递,使内部子组件能够使用来自顶层组件中的属性;说到这里有没有想到什么?Context呀...,不然人家是怎么在深层级的子组件中获取来自顶层组件的属性?当然不是通过props
的层层传递。React-Router
内部是基于History
库开发的, 区别于浏览器为我们提供的window.history
, History
库是在window.history
的基础上进行了一次封装,使其能够兼容不同浏览器, 因此后者也会拥有前者的的一些属性和方法。打印二者区别:
//一个简版的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;
}