像react-router
这样的库,核心实现依赖的是一个名为history
的库。
history
库其实就是对于window.history
的一个封装,是基于原生API实现的。
window.history API
window.history
提供了5个方法。
go
,到指定页面,参数为数字,go(1)
前进1个页面,go(-1)
后退1个页面back
,后退,等同于go(-1)
forward
,前进,等同于go(1)
pushState
,添加1条记录replaceState
,替换1条记录
我们主要会是用pushState
和replaceState
。
pushState
pushState
方法接收3个参数
- 第一个参数为
state
对象。 - 第二个参数为标题,不过这个参数浏览器并没有使用,应该传入一个空字符串防止未来API发生变化。
- 第三个参数为
url
,这个参数会实时的显示在浏览器的地址栏上。
state
对象我们可以通过window.history.state
来获取,默认情况下值为null
。
在当前页面打开控制台
输入window.history.pushState({state:0},"","/page")
,可以看到浏览器的地址变成了/page
。
在控制台输入window.history.state
,可以获取到当前的{state: 0}
。
输入window.history.back()
,即可返回之前到上一个页面。
replaceState
replaceState
和pushState
不同的地方在于它会替换掉当前的历史记录。
打开控制台,输入window.history.replaceState({state:1},"","/replace")
,可以看到浏览器的地址变成了/replace
。
在控制台输入window.history.state
,可以获取到当前的{state: 1}
。
输入window.history.back()
,回到的是上上个页面,因为上一个页面被我们替换掉了。
监听历史记录的变化
浏览器提供了一个popstate
的事件来监听历史记录的变化,不过它不能监听pushState
和replaceState
的变化,也无法知道当前是前进还是后退,只能监听go
、back
、forward
,和手动点击浏览器前进和后退按钮发生的历史记录变化。
window.addEventListener("popstate", event => {
console.log(event)
})
history源码浅析
由于原生的监听略有缺陷,所以history
这个库就解决了原生的问题。
它将API统一到一个history
对象,同时自行实现listener
的功能,在调用push
、replace
函数时也会触发事件回调函数,同时会将当前是前进还是后退传入给函数。
// createBrowserHistory
let globalHistory = window.history;
// 原生popstate事件里调用自己的listeners
function handlePop() {
let nextAction = Action.Pop;
let [nextIndex, nextLocation] = getIndexAndLocation();
// 调用自己的listeners
applyTx(nextAction);
}
window.addEventListener('popstate', handlePop);
let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();
// 调用自己的listeners
function applyTx(nextAction: Action, nextLocation: Location) {
action = nextAction;
location = nextLocation;
listeners.call({ action, location });
}
function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
globalHistory.pushState(historyState, '', url);
// push的时候调用listeners
applyTx(nextAction);
}
可以看到,只是创建了一个自己的listeners
,在push
和replace
的时候在手动调用一下,就解决了原生不触发的问题。
createHashHistory
和createBrowserHistory
基本一致,只是额外增加了hashchange
的事件监听。
手写React Router
基于上面的原理,其实我们已经可以简单的写一个路由了。
下面是一个很简单的20行实现的例子。
import React, { useLayoutEffect, useState } from "react";
import { createBrowserHistory } from "history";
const historyRef = React.createRef();
const Router = (props) => {
const { children } = props;
if (!historyRef.current) {
historyRef.current = createBrowserHistory();
}
const [state, setState] = useState({
action: historyRef.current.action,
location: historyRef.current.location,
});
useLayoutEffect(() => historyRef.current.listen(setState), []);
const {
location: { pathname },
} = state;
const routes = React.Children.toArray(children);
return routes.find((route) => route.props.path === pathname) ?? null;
};
const Route = (props) => props.children;
function App() {
return (
<Router>
<Route path="/">
<div onClick={() => historyRef.current.push("/page1")}>index</div>
</Route>
<Route path="/page1">
<div onClick={() => historyRef.current.back()}>page1</div>
</Route>
</Router>
);
}
其实简单来说就是根据不同的pathname
展示不同的元素了,不过在react-router
里没有这么简单,里面有一些复杂的判断,过段时间再对它写一篇源码浅析。