像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里没有这么简单,里面有一些复杂的判断,过段时间再对它写一篇源码浅析。