前言
react-router
v6 稳定版已经发布了一段时间了,相比起原来的 v5 版本,其 api 有着很大的变动,代码包体积也减少了一半多(20k => 8k),源码行数缩减到了 1600 行。由于 v5 版本的观念基本不能在 v6 版本使用了,正好这也是个学习源码的好机会,于是作者详细深入了react-router
及其周边生态,大致整理出了两篇文章(其依赖库history
与react-router
核心仓库的源码解析)。
本篇为第一篇文章,将会对history
这一仓库进行深度剖析。
history
本文基于
history v5.2.0
版本进行讲解history
是一个用于管理会话历史(包括浏览器历史)的库。
react-router
目前依赖history
5.x 版本,history
库也是react-router
团队开发的,内部封装了一些系列操作浏览器历史栈的功能,并提供了三种不同性质的history
导航创建方法:
createBrowserHistory
基于浏览器history
对象最新 api。createHashHistory
:基于浏览器 url 的 hash 参数。createMemoryHistory
:基于内存栈,不依赖任何平台。 上面三种方法创建的history
对象在react-router
中作为三种主要路由的导航器使用:BrowserRouter
对应createBrowserHistory
,由react-router-dom
提供。HashRouter
对应createHashHistory
,由react-router-dom
提供。MemoryRouter
对应createMemoryHistory
,由react-router
提供,主要用于react-native
等基于内存的路由系统。 注:- 实际上与
react-native
相对应的包react-router-native
使用的是NativeRouter
,但其实NativeRouter
就是MemoryRouter
的简单封装(改了下名字)。export interface NativeRouterProps extends MemoryRouterProps {} /** * NativeRouter 就是 react-router 里的 MemoryRouter,使用内存做导航 * A <Router> that runs on React Native. */ export function NativeRouter(props: NativeRouterProps) { return <MemoryRouter {...props} />; }
- 在
react-router-dom
中其实还有一种路由StaticRouter
,不过是用在ssr
中的,没有依赖history
库,仅仅是对传入的 props 做了校验而已。
在// 具体引入方式 import { StaticRouter } from 'react-router-dom/server'
react-router-dom
v6.1.1 时还新增了HistoryRouter
,不过该Router
主要是帮助我们手动传入history
实例,在这里就暂不说明了,后续在讲到reacr-router-dom
时会细说。
路由切换时的 Action
history
在每次路由切换时都为其定义了一个特定的action
,这些action
被分为了三大类:
POP
:PUSH
:REPLACE
:
在源码中使用了一个枚举变量来保存,在后续的函数封装中会用到:
export enum Action {
Pop = 'POP',
Push = 'PUSH',
Replace = 'REPLACE'
}
抽象化的 Path 与 Location
对于一次路由跳转的 url,history
将其进行了两层抽象:
-
一层是只基于 url 创建的
Path
对象,该对象是把 url 解析为 path,query 与 hash 三部分后的结果。下面是
Path
对象的定义:// 下面三个分别是对 url 的 path,query 与 hash 部分的类型别名 export type Pathname = string; export type Search = string; export type Hash = string; // 一次跳转 url 对应的对象 export interface Path { pathname: Pathname; search: Search; hash: Hash; }
下面则是
history
内提供的 url 与Path
对象互相转换的方法:/** * pathname + search + hash 创建完整 url */ export function createPath({ pathname = '/', search = '', hash = '' }: Partial<Path>) { if (search && search !== '?') pathname += search.charAt(0) === '?' ? search : '?' + search; if (hash && hash !== '#') pathname += hash.charAt(0) === '#' ? hash : '#' + hash; return pathname; } /** * 解析 url,将其转换为 Path 对象 */ export function parsePath(path: string): Partial<Path> { let parsedPath: Partial<Path> = {}; if (path) { let hashIndex = path.indexOf('#'); if (hashIndex >= 0) { parsedPath.hash = path.substr(hashIndex); path = path.substr(0, hashIndex); } let searchIndex = path.indexOf('?'); if (searchIndex >= 0) { parsedPath.search = path.substr(searchIndex); path = path.substr(0, searchIndex); } if (path) { parsedPath.pathname = path; } } return parsedPath; }
-
还有一层则是扩展自
Path
对象,将路由导航这一行为抽象了出来形成的Location
对象,该对象除了包含有Path
对象的属性外,还拥有与每次导航相关联的上下文信息状态(state)与和这次导航唯一对应的 key 值。下面是
Location
对象的定义:// 唯一字符串,与每次跳转的 location 匹配 export type Key = string; // 路由跳转抽象化的导航对象 export interface Location extends Path { // 与当前 location 关联的 state 值,可以是任意手动传入的值 state: unknown; // 当前 location 的唯一 key,一般都是自动生成 key: Key; }
history
内部创建唯一 key 的方法:/** * 创建唯一 key */ function createKey() { return Math.random().toString(36).substr(2, 8); }
深入理解 History 对象
在之前我们提到了history
内部的三种导航方法会创建出三个有着不同性质的History
对象,但其实这些History
对象向外暴露的 api 大体都是一致的,所以我们可以先从它们共同的 api 定义入手。
一个最基础的history
对象包含:
- 两个属性:当前路由对应的跳转行为(
action
)与导航对象(location
) - 一个工具方法:
createHref
用于用户将history
内部定义的Path
对象转换为原始的 url。history.createHref({ pathname: '/home', search: 'the=query', hash:'hash' }) // 输出: /home?the=query#hash
- 五个路由跳转方法:
push
、replace
、go
、back
与forward
用于在路由栈中进行路由跳转。// 将一个新的历史导航推入历史栈,并且移动当前指针到该历史导航 history.push('/home'); // 将当前的路由使用新传入的历史导航替换 history.replace('/home'); // 此方法可以传入一个 Path 对象,同时也可以接收第二个参数 state,可用于保存在内存中的历史导航上下文信息 history.push({ pathname: '/home', search: '?the=query' }, { some: state }); // replace 方法同上 history.replace({ pathname: '/home', search: '?the=query' }, { some: state }); // 返回上一个历史导航 history.go(-1); history.back(); // 去往下一个历史导航 history.go(1); history.forward();
- 两个路由监听方法:监听路由跳转的钩子(类似后置守卫)
listen
与阻止路由跳转的钩子(如果想正常跳转必须要取消监听,可以封装成类似前置钩子的功能)block
。// 开始监听路由跳转 let unlisten = history.listen(({ action, location }) => { // The current location changed. }); // 取消监听 unlisten(); // 开始阻止路由跳转 let unblock = history.block(({ action, location, retry }) => { // retry 方法可以让我们重新进入被阻止跳转的路由 // 取消监听,如果想要 retry 生效,必须要在先取消掉所有 block 监听,否则 retry 后依然会被拦截然后进入 block 监听中 unblock(); retry(); });
关于
block
监听的阻止路由跳转这里的阻止跳转并不是阻止新的路由的推入,而是监听到新路由跳转后马上返回到前一个路由,也就是强制执行了
history.go(index - nextIndex)
,index
是原来的路由在路由栈的索引,nextIndex
是跳转路由在路由栈的索引。
整个History
对象的接口在源码中的定义如下:
// listen 回调的参数,包含有更新的行为 Action 和 Location 对象
export interface Update {
action: Action; // 上面提到的 Action
location: Location; // 上面提到的 Location
}
// 监听函数 listener 的定义
export interface Listener {
(update: Update): void;
}
// block 回调的参数,除了包含有 listen 回调参数的所有值外还有一个 retry 方法
// 如果阻止了页面跳转(blocker 监听),可以使用 retry 重新进入页面
export interface Transition extends Update {
/**
* 重新进入被 block 的页面
*/
retry(): void;
}
/**
* 页面跳转失败后拿到的 Transition 对象
*/
export interface Blocker {
(tx: Transition): void;
}
// 跳转链接,可以是完整的 url,也可以是 Path 对象
export type To = string | Partial<Path>;
export interface History {
// 最后一次浏览器跳转的行为,可变
readonly action: Action;
// 挂载有当前的 location 可变
readonly location: Location;
// 工具方法,把 to 对象转化为 url 字符串,其实内部就是对之前提到的 createPath 函数的封装
createHref(to: To): string;
// 推入一个新的路由到路由栈中
push(to: To, state?: any): void;
// 替换当前路由
replace(to: To, state?: any): void;
// 将当前路由指向路由栈中第 delta 个位置的路由
go(delta: number): void;
// 将当前路由指向当前路由的前一个路由
back(): void;
// 将当前路由指向当前路由的后一个路由
forward(): void;
// 页面跳转后触发,相当于后置钩子
listen(listener: Listener): () => void;
// 也是监听器,但是会阻止页面跳转,相当于前置钩子,注意只能拦截当前 history 对象的钩子,也就是说如果 history 对象不同,是不能够拦截到的
block(blocker: Blocker): () => void;
}
History 对象的创建
在之前我们提到了三种创建History
对象的方法:createBrowserHistory
、createHashHistory
和createMemoryHistory
。其中:
createBrowserHistory
用于给用户提供的创建基于浏览器 history API 的History
对象,适用于绝大多数现代浏览器(除了少部分不支持 HTML5 新加入的 history API 的浏览器,也就是浏览器的history
对象需要具有pushState
、replaceState
和state
等属性和方法),同时在生产环境需要服务端的重定向配置才能正常使用。createHashHistory
用于给用户提供基于浏览器 url hash 值的History
对象,一般来说使用这种方式可以兼容几乎所有的浏览器,但是考虑到目前浏览器的发展,在5.x
版本内部其实同createBrowserHistory
,也是使用最新的 history API 实现路由跳转的(如果你确实需要兼容旧版本浏览器,应该选择使用4.x
版本),同时由于浏览器不会将 url 的 hash 值发送到服务端,前端发送的路由 url 都是一致的,就不用服务端做额外配置了。createMemoryHistory
用于给用户提供基于内存系统的History
对象,适用于所有可以运行 JavaScript 的环境(包括 Node),内部的路由系统完全由用户支配。
而在history
中,这三种方法内部创建History
对象的过程基本都是一致的,仅在内部依赖了不同的 API 来实现,所以这里就同时分析这三个方法内部的创建流程了。
我们可以先来看下这些方法创建的History
对象的类型:
export interface BrowserHistory extends History {}
export interface HashHistory extends History {}
export interface MemoryHistory extends History {
readonly index: number;
}
BrowserHistory
与HashHistory
的类型其实就是我们之前提到的History
对象的类型,而MemoryHistory
还额外有一个index
属性,因为是基于内存的路由系统,所以我们可以清楚知道当前路由在历史栈中的位置,这个属性就是告诉用户目前的内存历史栈索引的(其余两个路由对象其实内部也有index
,但是这个index
与浏览器中的历史栈索引并不能一一对应,所以没有暴露给用户,只是在内部用作区分)。
下面我们来分别看看三个创建方法本身:
- createBrowserHistory:
// 可以传入指定的 window 对象作为参数,默认为当前 window 对象 export type BrowserHistoryOptions = { window?: Window }; export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { let { window = document.defaultView! } = options; // 拿到浏览器的 history 对象,后续会基于此对象封装方法 let globalHistory = window.history; // 初始化 action 与 location let action = Action.Pop; let [index, location] = getIndexAndLocation(); // 获取当前路由的 index 和 location // 省略其余代码 let history: BrowserHistory = { get action() { return action; }, get location() { return location; }, createHref, push, replace, go, back() { go(-1); }, forward() { go(1); }, listen(listener) { // 省略其余代码 }, block(blocker) { // 省略其余代码 } }; return history; }
- createHashHistory:
// 这里同 BrowserRouter export type HashHistoryOptions = { window?: Window }; export function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { let { window = document.defaultView! } = options; // 浏览器本身就有 history 对象,只是 HTML5 新加入了几个有关 state 的 api let globalHistory = window.history; let action = Action.Pop; let [index, location] = getIndexAndLocation(); // 省略其余代码 let history: HashHistory = { get action() { return action; }, get location() { return location; }, createHref, push, replace, go, back() { go(-1); }, forward() { go(1); }, listen(listener) { // 省略其余代码 }, block(blocker) { // 省略其余代码 } }; return history; }
- createMemoryHistory:
// 这里与 BrowserRouter 和 HashRouter 相比有略微不同,因为没有浏览器的参与,所以我们需要模拟历史栈 // 用户提供的描述历史栈的对象 export type InitialEntry = string | Partial<Location>;// 上面提到的 Location // 因为不是真实的路由,所以不需要 window 对象,取而代之的是 export type MemoryHistoryOptions = { // 初始化的历史栈 initialEntries?: InitialEntry[]; // 初始化的 index initialIndex?: number; }; // 判断上下限值 function clamp(n: number, lowerBound: number, upperBound: number) { return Math.min(Math.max(n, lowerBound), upperBound); } export function createMemoryHistory( options: MemoryHistoryOptions = {} ): MemoryHistory { let { initialEntries = ['/'], initialIndex } = options; // 将用户传入的 initialEntries 转换为包含 Location 对象数组,会在之后用到 let entries: Location[] = initialEntries.map((entry) => { // readOnly 就是调用 Object.freeze 冻结对象,这里做了个开发模式的封装,遇到都可以直接跳过 let location = readOnly<Location>({ pathname: '/', search: '', hash: '', state: null, key: createKey(), ...(typeof entry === 'string' ? parsePath(entry) : entry) }); return location; }); // 这里的 location 与 index 的获取方式不同了,是直接从初始化的 entries 中取的 let action = Action.Pop; let location = entries[index]; // clamp 函数用于取上下限值,如果 没有传 initialIndex 默认索引为最后一个 location // 这这里调用是为了规范初始化的 initialIndex 的值 let index = clamp( initialIndex == null ? entries.length - 1 : initialIndex, 0, entries.length - 1 ); // 省略其余代码 let history: MemoryHistory = { get index() { return index; }, get action() { return action; }, get location() { return location; }, createHref, push, replace, go, back() { go(-1); }, forward() { go(1); }, listen(listener) { // 省略其余代码 }, block(blocker) { // 省略其余代码 } }; return history; }
ok,我们现在应该已经能够看出,这些方法内部就是通过对History
对象方法的封装,最后返回一个符合我们之前定义标准的History
对象,所以现在只需要对其中的每个属性或方法做单独解析就行了。
action、location 与 index(MemoryRouter 独有)
回到我们上面的代码中,可以发现上面三个属性都是通过get
方法设置的,使用这种方式定义的值在获取时都会调用对应的get
方法,这也就意味着这三者的值都是实时获取的,在我们调用History
对象的方法时会间接更改它们的值。
除了createMemoryHistory
,其余两个方法的index
和location
都是通过getIndexAndLocation()
获取的。下面是getIndexAndLocation()
方法的内部逻辑:
- createBrowserHistory:
export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { let { window = document.defaultView! } = options; let globalHistory = window.history; /** * 拿到当前的 state 的 idx 和 location 对象 */ function getIndexAndLocation(): [number, Location] { let { pathname, search, hash } = window.location; // 获取当前浏览器的 state let state = globalHistory.state || {}; // 可以看到下面很多属性都是保存到了 history api 的 state 中 return [ state.idx, readOnly<Location>({ pathname, search, hash, state: state.usr || null, key: state.key || 'default' }) ]; } let action = Action.Pop; let [index, location] = getIndexAndLocation(); // 初始化 index if (index == null) { index = 0; // 调用的是 history api 提供的 replaceState 方法传入 index,这里只是初始化浏览器中保存的 state,没有改变 url globalHistory.replaceState({ ...globalHistory.state, idx: index }, ''); } //... let history: BrowserHistory = { get action() { return action; }, get location() { return location; } // ... }; return history; }
- createHashHistory:
export function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { let { window = document.defaultView! } = options; let globalHistory = window.history; function getIndexAndLocation(): [number, Location] { // 注意这里和 browserHistory 不同了,拿的是 hash,其余逻辑是一样的 // parsePath 方法前面有讲到过,解析 url 为 Path 对象 let { pathname = '/', search = '', hash = '' } = parsePath(window.location.hash.substr(1)); let state = globalHistory.state || {}; return [ state.idx, readOnly<Location>({ pathname, search, hash, state: state.usr || null, key: state.key || 'default' }) ]; } let action = Action.Pop; let [index, location] = getIndexAndLocation(); if (index == null) { index = 0; globalHistory.replaceState({ ...globalHistory.state, idx: index }, ''); } //... let history: HashHistory = { get action() { return action; }, get location() { return location; } // ... }; return history; }
createHref
createHref
方法的功能很简单,它主要用于将history
内部定义的To
对象(type To = string | Partial<Path>
)转回为 url 字符串:
- createBrowserHistory:
export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { // ... // BrowserHistory 只需要简单判断一下类型就可以了 function createHref(to: To) { return typeof to === 'string' ? to : createPath(to); } // ... let history: BrowserHistory = { // ... createHref // ... } return history }
- createHashHistory:
export function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { // ... /** * 查看是否有 base 标签,如果有则取 base 的 url(不是从 base 标签获取,是从 window.location.href 获取) */ function getBaseHref() { let base = document.querySelector('base'); let href = ''; if (base && base.getAttribute('href')) { let url = window.location.href; let hashIndex = url.indexOf('#'); // 拿到去除了 # 的 url href = hashIndex === -1 ? url : url.slice(0, hashIndex); } return href; } // HashHistory 需要额外拿到当前页面的 base url function createHref(to: To) { return getBaseHref() + '#' + (typeof to === 'string' ? to : createPath(to)); } // ... let history: HashHistory = { // ... createHref // ... } return history }
- createMemoryHistory:
export function createMemoryHistory( options: MemoryHistoryOptions = {} ): MemoryHistory { // ... // 同 BrowserHistory function createHref(to: To) { return typeof to === 'string' ? to : createPath(to); } // ... let history: MemoryHistory = { // ... createHref // ... } return history }
listen
listen
方法是一个监听方法,本质上就是实现了一个发布订阅模式。发布订阅模型在源码中实现如下:
/**
* 事件对象
*/
type Events<F> = {
length: number;
push: (fn: F) => () => void;
call: (arg: any) => void;
};
/**
* 内置的发布订阅事件模型
*/
function createEvents<F extends Function>(): Events<F> {
let handlers: F[] = [];
return {
get length() {
return handlers.length;
},
// push 时返回对应的 clear 语句
push(fn: F) {
handlers.push(fn);
return function () {
handlers = handlers.filter((handler) => handler !== fn);
};
},
call(arg) {
handlers.forEach((fn) => fn && fn(arg));
}
};
}
创建完成后,history
会拿到的对应模型对象,在路由改变时依次调用传入的handlers
。
下面是listen
方法的定义:
export interface Update {
action: Action;
location: Location;
}
// Listener 类型在之前有提到过,可以往回看看
export interface Listener {
(update: Update): void;
}
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
// ...
let listeners = createEvents<Listener>();
// ...
let history: BrowserHistory = {
// ...
listen(listener) {
return listeners.push(listener);
},
// ...
};
return history;
}
export function createHashHistory(
options: HashHistoryOptions = {}
): HashHistory {
// ...
let listeners = createEvents<Listener>();
// ...
let history: HashHistory = {
// ...
listen(listener) {
return listeners.push(listener);
},
// ...
};
return history;
}
export function createMemoryHistory(
options: MemoryHistoryOptions = {}
): MemoryHistory {
// ...
let listeners = createEvents<Listener>();
// ...
let history: MemoryHistory = {
// ...
listen(listener) {
return listeners.push(listener);
},
// ...
};
return history;
}
block
block
方法的实现则和listen
方法基本相同,只是BrowserHistory
和HashHistory
内部额外对于浏览器的beforeunload
事件做了监听。
- createBrowserHistory 与 createHashHistory :
export interface Update { action: Action; location: Location; } export interface Transition extends Update { retry(): void; } // Blocker 我们之前也提到过 export interface Blocker { (tx: Transition): void; } const BeforeUnloadEventType = 'beforeunload'; export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { // ... let blockers = createEvents<Blocker>(); // ... let history: BrowserHistory = { // ... block(blocker) { let unblock = blockers.push(blocker); // 当我们需要监听跳转失败时才加入,并且只需要一个事件来阻止页面关闭 if (blockers.length === 1) { window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); } return function () { unblock(); // 当没有 blocker 监听时应该删除 beforeunload 事件的监听 if (!blockers.length) { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); } }; } // ... }; return history; } export function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { // ... let blockers = createEvents<Blocker>(); // ... let history: HashHistory = { // ... block(blocker) { let unblock = blockers.push(blocker); if (blockers.length === 1) { window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); } return function () { unblock(); if (!blockers.length) { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); } }; } // ... }; return history; }
- createMemoryHistory:
// MemoryHistory 这里同 listen 方法一样 export function createMemoryHistory( options: MemoryHistoryOptions = {} ): MemoryHistory { // ... let blockers = createEvents<Blocker>(); // ... let history: MemoryHistory = { // ... // 这里就没有监听浏览器的 beforeunload 事件了 block(blocker) { return blockers.push(blocker); } // ... }; return history; }
push 和 replace
push
和replace
两个方法内部要做的事更多一点,除了需要封装将新的导航推入到历史栈的功能外,还需要同时改变当前的action
与location
,并且判断并调用相应的监听方法。
createBrowserHistory
和createHashHistory
中这两个方法的结构定义是相同的:function push(to: To, state?: any) { let nextAction = Action.Push; let nextLocation = getNextLocation(to, state); /** * 重新执行 push 操作 */ function retry() { push(to, state); } // 当没有 block 监听时 allowTx 返回 true,否则都是返回 false,不会推送新的导航 if (allowTx(nextAction, nextLocation, retry)) { let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1); // try...catch 是因为 ios 限制最多调用 100 次 pushState 方法,否则会报错 try { globalHistory.pushState(historyState, '', url); } catch (error) { // push 失败后就没有 state 了,直接使用 href 跳转 window.location.assign(url); } applyTx(nextAction); } } function replace(to: To, state?: any) { let nextAction = Action.Replace; let nextLocation = getNextLocation(to, state); function retry() { replace(to, state); } // 同 push 函数,否则不会替换新的导航 if (allowTx(nextAction, nextLocation, retry)) { let [historyState, url] = getHistoryStateAndUrl(nextLocation, index); globalHistory.replaceState(historyState, '', url); applyTx(nextAction); } }
createMemoryHistory
中则是改变已定义的entries
数组:function push(to: To, state?: any) { let nextAction = Action.Push; let nextLocation = getNextLocation(to, state); function retry() { push(to, state); } if (allowTx(nextAction, nextLocation, retry)) { // 修改 index 与 entries 历史栈数组 index += 1; // 添加一个新的 location,删除原来 index 往后的栈堆 entries.splice(index, entries.length, nextLocation); applyTx(nextAction, nextLocation); } } function replace(to: To, state?: any) { let nextAction = Action.Replace; let nextLocation = getNextLocation(to, state); function retry() { replace(to, state); } if (allowTx(nextAction, nextLocation, retry)) { // 覆盖掉原来的 location entries[index] = nextLocation; applyTx(nextAction, nextLocation); } }
在上面的代码封装中,我们先定义对应方法的Action
,将用户传入的参数格式化为Location
对象。
其中格式化参数的getNextLocation()
方法的定义如下:
function getNextLocation(to: To, state: any = null): Location {
// 简单的格式化
return readOnly<Location>({
// 这前面三个实际上是默认值
pathname: location.pathname,
hash: '',
search: '',
// 下面胡返回带有 pathname、hash、search 的对象
...(typeof to === 'string' ? parsePath(to) : to),
state,
key: createKey()
});
}
然后定义了一个递归的retry
方法用于在block
回调中传入,最后使用allowTx
方法判断是否满足路由跳转条件,如果满足则调用路由跳转相关代码(包括改变action
、location
属性,以及调用回调监听等)。
其中allowTx()
方法的定义如下:
/**
* 调用所有 blockers,没有 blocker 的监听时才会返回 true,否则都是返回 false
*/
function allowTx(action: Action, location: Location, retry: () => void) {
return (
!blockers.length || (blockers.call({ action, location, retry }), false)
);
}
getHistoryStateAndUrl()
方法(这个只有createBrowserHistory
和createHashHistory
才有):
type HistoryState = {
usr: any;
key?: string;
idx: number;
};
function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {
return [
{
usr: nextLocation.state,
key: nextLocation.key,
idx: index
},
createHref(nextLocation)
];
}
applyTx()
方法:
// createBrowserHistory 与 createHashHistory
function applyTx(nextAction: Action) {
action = nextAction;
// 在这里修改了 index 与 location,getIndexAndLocation 方法我们在前面将 location 属性的时候有提到过
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
// createMemoryHistory
function applyTx(nextAction: Action, nextLocation: Location) {
// 没有在这里改变 index ,和其余 router 不同,将 index 改变操作具体到了 push 和 go 等函数中
action = nextAction;
location = nextLocation;
listeners.call({ action, location });
}
浏览器 popstate 事件的监听
在浏览器环境下,除了手动调用history.push
与history.replace
外,用户还可以通过浏览器的前进和后退按钮改变导航历史,这样的行为在history
中则对应着Action
的POP
,同时浏览器也提供了对应的事件popstate
,所以我们还需要额外处理一下这样的情形。
这里只有createBrowserHistory
和createHashHistory
才会有该事件的监听:
const HashChangeEventType = 'hashchange';
const PopStateEventType = 'popstate';
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
//...
let blockedPopTx: Transition | null = null;
/**
* 事件监听回调函数
* 如果设置了 blocker 的监听器,该函数会执行两次,第一次是跳回到原来的页面,第二次是执行 blockers 的所有回调
* 这个函数用于监听浏览器的前进后退,因为我们封装的 push 函数已经被我们拦截了
*/
function handlePop() {
if (blockedPopTx) {
blockers.call(blockedPopTx);
blockedPopTx = null;
} else {
let nextAction = Action.Pop;
let [nextIndex, nextLocation] = getIndexAndLocation();
// 如果有前置钩子
if (blockers.length) {
if (nextIndex != null) {
// 计算跳转层数
let delta = index - nextIndex;
if (delta) {
// Revert the POP
blockedPopTx = {
action: nextAction,
location: nextLocation,
// 恢复页面栈,也就是 nextIndex 的页面栈
retry() {
go(delta * -1);
}
};
// 跳转回去(index 原本的页面栈)
go(delta);
}
} else {
// asset
// nextIndex 如果为 null 会进入该分支打警告信息,这里就先不管它
}
} else {
// 改变当前 action,调用所有的 listener
applyTx(nextAction);
}
}
}
// 可以看到在创建 History 对象的时候就进行监听了
window.addEventListener(PopStateEventType, handlePop);
//...
}
export function createHashHistory(
options: HashHistoryOptions = {}
): HashHistory {
//...
// 下面和 createBrowserHistory 一样
let blockedPopTx: Transition | null = null;
function handlePop() {
//...
}
// 下面额外监听了 hashchange 事件
window.addEventListener(PopStateEventType, handlePop);
// 低版本兼容,监听 hashchange 事件
// https://developer.mozilla.org/de/docs/Web/API/Window/popstate_event
window.addEventListener(HashChangeEventType, () => {
let [, nextLocation] = getIndexAndLocation();
// 如果支持 popstate 事件这里就会相等,因为会先执行 popstate 的回调
if (createPath(nextLocation) !== createPath(location)) {
handlePop();
}
});
//...
}
go、back 和 forward
比起push
和replace
,这三个方法的封装就简单很多了,在BrowserHistory
和HashHistory
中它们都只是 history
API 的包装而已,而在MemoryHistory
中因为没有路由的 pop 监听事件与来自代码外的历史栈改变,所以直接将监听的回调移动到了这里面。
- createBrowserHistory 与 createHashHistory :
export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory { // ... function go(delta: number) { globalHistory.go(delta); } // ... let history: BrowserHistory = { go, back() { go(-1); }, forward() { go(1); }, // ... }; return history; } export function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { // ... function go(delta: number) { globalHistory.go(delta); } // ... let history: HashHistory = { go, back() { go(-1); }, forward() { go(1); }, // ... }; return history; }
- createMemoryHistory:
export function createMemoryHistory(
options: MemoryHistoryOptions = {}
): HashHistory {
// ...
function go(delta: number) {
// 跳转到原来的 location
let nextIndex = clamp(index + delta, 0, entries.length - 1);
let nextAction = Action.Pop;
let nextLocation = entries[nextIndex];
function retry() {
go(delta);
}
if (allowTx(nextAction, nextLocation, retry)) {
index = nextIndex;
applyTx(nextAction, nextLocation);
}
}
// ...
let history: MemoryHistory = {
go,
back() {
go(-1);
},
forward() {
go(1);
},
// ...
};
return history;
}
总结
history
整体是对浏览器 api 的二次封装,但是并没有太过深入的封装,仅仅是对每次页面跳转时做了抽象处理,并且加入了额外的监听与特殊的阻止跳转功能。
下一篇文章我们将会进入主题,分析react-router v6
中的路由封装思路。
本篇文章相关的源码分析仓库可以在 github 查看。