Appearance
router/history 模块
这个一个不暴露给开发者的包,主要是作为 react-router 的基座,从而提供三种基础路由方式(MemoryRouter、HistoryRouter、HashRouter)操作浏览器历史栈的核心功能。其将三种方式通过传入不同的处理 location 和 url 的方法,最终提供具有相同操作的实例对象(history),从而磨平了之间的差异。
具体来讲对于我们常用的两种 HistoryRouter、HashRouter 其通过不同的创建方式 createBrowserHistory
、createHashHistory
传入核心的生成 location 和 url 的方法,然后返回具有同一操作方式的实例对象 history,这样就涉及到几个核心的对象
history 对象
ts
/**
* A history is an interface to the navigation stack. The history serves as the
* source of truth for the current location, as well as provides a set of
* methods that may be used to change it.
*
* It is similar to the DOM's `window.history` object, but with a smaller, more
* focused API.
*/
export interface History {
/**
* The last action that modified the current location. This will always be
* Action.Pop when a history instance is first created. This value is mutable.
*/
readonly action: Action;
/**
* The current location. This value is mutable.
*/
readonly location: Location;
/**
* 创建一个不具备location.origin + basename 的相对于react-router 的路由地址
* 区别与 createHref : createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
* createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
* @param to 路径 或者 location对象
* @returns
*/
createHref(to: To): string;
/**
* 创建一个完整url地址的 RL对象
* 区别与 createHref : createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
* createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
* @param to 路径 或者 location对象
* @returns
*/
createURL(to: To): URL;
/**
* Encode a location the same way window.history would do (no-op for memory
* history) so we ensure our PUSH/REPLACE navigations for data routers
* behave the same as POP
*
* @param to Unencoded path
*/
encodeLocation(to: To): Path;
/**
* Pushes a new location onto the history stack, increasing its length by one.
* If there were any entries in the stack after the current one, they are
* lost.
*
* @param to - The new URL
* @param state - Data to associate with the new location
*/
push(to: To, state?: any): void;
/**
* Replaces the current location in the history stack with a new one. The
* location that was replaced will no longer be available.
*
* @param to - The new URL
* @param state - Data to associate with the new location
*/
replace(to: To, state?: any): void;
/**
* Navigates `n` entries backward/forward in the history stack relative to the
* current index. For example, a "back" navigation would use go(-1).
*
* @param delta - The delta in the stack index
*/
go(delta: number): void;
/**
* Sets up a listener that will be called whenever the current location
* changes.
*
* @param listener - A function that will be called when the location changes
* @returns unlisten - A function that may be used to stop listening
*/
listen(listener: Listener): () => void;
}
获取 location 实例对象不同
与 HistoryRouter 相比,HashRouter 的获取 location 实例方法的主要区别是对于 pathname、search、hash 的获取上,例如下面的链接地址
createXXXXLocation
https://www.baidu.com/home/homepage?name=1#/account/logout?age=2#hash2
其在两种 Router 中 location 的结果就是不一样的,具体如下
- HistoryRouter
ts
function createBrowserLocation(
window: Window,
globalHistory: Window["history"]
) {
let { pathname, search, hash } = window.location;
// ...
}
直接通过 window.location 获取,所以其对应的是 pathname = /home/homepage
, search = ?name=1
, hash = #/account/logout?name=2#hash2
- HashRouter
ts
function createHashLocation(window: Window, globalHistory: Window["history"]) {
// 获取的 url中第一个 # 之后的值
let {
pathname = "/",
search = "",
hash = "",
} = parsePath(window.location.hash.substr(1));
}
其通过 window.location.hash.substr(1)
获取三个值,所以会忽略 window.location 自带的 pathname 、hash、search 的值, 具体其值为 pathname = /account/logout
, search = ?age=2
, hash = #/ahash2
。 例外其有一个特殊的方法 parsePath()
处理了结果,这个使用的地方特别多,
parsePath
源码
ts
/**
* Parses a string URL path into its separate pathname, search, and hash components.
* 与 createPath 相对应, createPath 是将 pathname search hash 转换成真正的url
* parsePath 则是将真正的url转换成 { pathname search hash }
* /home/homepage?name=1#a
* =>
* { pathname : "/home/homepage" , search : "name=1" , hash : "#a" }
*/
export function parsePath(path: string): Partial<Path> {
let parsedPath: Partial<Path> = {};
if (path) {
// 提取 hash 的内容
let hashIndex = path.indexOf("#");
if (hashIndex >= 0) {
parsedPath.hash = path.substr(hashIndex);
path = path.substr(0, hashIndex);
}
// 提取 search 的内容
// 注意: search的内容不会转换成对象格式
let searchIndex = path.indexOf("?");
if (searchIndex >= 0) {
parsedPath.search = path.substr(searchIndex);
path = path.substr(0, searchIndex);
}
if (path) {
parsedPath.pathname = path;
}
}
return parsedPath;
}
生成对应跳转 url 地址不同
createXXXXHref
- HistoryRouter
提供 history 模式下 生成路由跳转链接的方法
在 history 模式下 window.location.href = pathname + search + hash
ts
function createBrowserHref(window: Window, to: To) {
return typeof to === "string" ? to : createPath(to);
}
- HashRouter
在 HashRouter 模式下,window.location.href = http://127.0.0.1:3000/index.html#/pathname + search + hash
, 所以其一方面需要通过 pathname + search + hash
生成 react-router 关注的路径,另外一方面需要使用 #
之前的内容生成 url 前缀,从而最终拼凑成 /home/homepage?name=1# ${pathname} ? ${search} # ${hash}
其核心就是获取基础的 href
内容
ts
function createHashHref(window: Window, to: To) {
let base = window.document.querySelector("base");
let href = "";
// 获取 Url 的基础路径 (如果存在# 那么就移除 #及其后面的内容)
// http://127.0.0.1:3000/index.html#/home
// => /home
if (base && base.getAttribute("href")) {
let url = window.location.href;
let hashIndex = url.indexOf("#");
href = hashIndex === -1 ? url : url.slice(0, hashIndex);
}
// 生成包含 pathname + search 和 hash 的 hash地址
return href + "#" + (typeof to === "string" ? to : createPath(to));
}
createPath
相对于 parsePath,此逻辑就比较简单了,就是按照 pathname、search、hash 的顺序转换成真正的 url
ts
/**
* Creates a string URL path from the given pathname, search, and hash components.
* 对象类型的 To 会根据 pathname、search、hash 转换成真正的 url
* { pathname : "/home/homepage" , search : "name=1" , hash : "#a" }
* =>
* /home/homepage?name=1#a
*/
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;
}
action 和 location
ts
// 创建 history 实例对象
let history: History = {
get action() {
return action;
},
get location() {
return getLocation(window, globalHistory);
},
};
action 和 location 属性都是通过 get 方法设置的,使用这种方式定义的值在获取时都会调用对应的 get 方法,这也就意味着这三者的值都是实时获取的,在我们调用 History 对象的方法时会间接更改它们的值。
所以我们可以通过 history.location
和 history.action
去实时获取对应的值
action
主要保存了当前浏览记录是通过何种方式进入的 (POP 的初始化加载, PUSH 的通过 history.push 跳转,REPLACE 的通过 history.replace 跳转)
ts
export enum Action {
// 初始化的方式
Pop = "POP",
// 通过 history.push 的方式进入的
Push = "PUSH",
// 通过 history.replace 的方式进入的
Replace = "REPLACE",
}
对于 action 发现其通过闭包的方式去缓存当前的操作类型,在我们使用 history.push()
、history.replace()
都会修改当前 action 对象,从而我们在 访问 history.action 的时候可以找到当前 url 是通过那种方式进入的
location
缓存了当前 url 下对应的 location 对象信息,其与 window.location 不同,具体如下
- pathname
缓存了当前 url 的路由地址
- search : string
缓存了当前 url 中 ?包含的字符串内容, 需要注意的是 其不是对象类型 而是字符串类型
- hash
缓存了当前 url 中 # 包含的字符串内容
state
key
唯一的字符串 Key,每一个路由对象都不同
路由跳转方法
history.push
作用
将当前导航路由推入到历史栈中,从而形成一个新的历史记录,其 state 的内容为
{ usr : 用户state , idx : 下标 , key : 唯一的Key }
生成新的 location 对象,并修改 history 实例对象中的 action 和 location
兼容 V5 版本的监听方法
源码
ts
/**
* 统一的 push 方法
* @param to 路由地址 /home/homepage
* @param state state 对象
*/
function push(to: To, state?: any) {
action = Action.Push;
// 生成当前路由路径的 location 对象
let location = createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
// 生成 history popstate队列的下标
index = getIndex() + 1;
// 生成state对象
// 目的: 将用户传递的state内容 转换成 { usr : 用户state , idx : 下标 , key : 唯一的Key }
let historyState = getHistoryState(location, index);
// 生成可能包含 hash 的url
let url = history.createHref(location);
// try...catch because iOS limits us to 100 pushState calls :/
// 调用 pushState 去跳转页面
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// If the exception is because `state` can't be serialized, let that throw
// outwards just like a replace call would so the dev knows the cause
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
// https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
if (error instanceof DOMException && error.name === "DataCloneError") {
throw error;
}
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
if (v5Compat && listener) {
listener({ action, location: history.location, delta: 1 });
}
}
history.replace
分析
其逻辑跟 push 很像,只是将修改浏览器历史栈的方法改成了 replaceState
方法,还有 action = Action.Replace
源码
ts
function replace(to: To, state?: any) {
action = Action.Replace;
let location = createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
index = getIndex();
let historyState = getHistoryState(location, index);
let url = history.createHref(location);
globalHistory.replaceState(historyState, "", url);
if (v5Compat && listener) {
listener({ action, location: history.location, delta: 0 });
}
}
history.go
直接调用原生的 go 方法
ts
return globalHistory.go(n);
监听浏览器 Url 切换方法
ReactRouter 跟 VueRouter 在这个方面的区别还是很大的,
在 Vue 中其对于 HashRouter 和 HistoryRouter 两种模式使用了不同的监听方式,如
- history 模式使用的是
window.addEventListener('popstate', function(e) {})
; - Hash 模式则是使用的
window.addEventListener('hashchange', function(e) {})
在 react-router 中对于这两种模式的监听则没有区分,都是统一使用的 popstate
事件去进行监听。
具体好处没有什么大的区别,对于 Vue 来说 hashchange 的兼容性更好,其也符合 Vue2 版本兼容 ie 低版本的能力。而对于 react-router v6 来说其完全没有 ie 兼容的考虑,完全可以统一使用 popstate 从而做到 hash 和 history api 的统一
浏览器地址处理方法
这两个实际上应该放在一起说明,
- history.createHref
创建一个不具备 location.origin + basename 的相对于 react-router 的路由地址
- history.createURL
是创建一个相对于浏览器的完整的 url 地址的 URL 对象
在 history 模式下这两个的差距很小,只是 createURL 比 createHref 多了一个 basename 前缀, 但是在 hash 模式下,createURL 比 createHref 一方面需要多 basename 前缀,另外一方面 还需要生成 hash 的基础 href
例如 : https://www.baidu.com/home/homepage?name=1#/account/logout?age=2#hash2
- history 模式 (basename = "/admin" )
createHref = /home/homepage?name=1#/account/logout?age=2#hash2
createURL = /admin
+ /home/homepage?name=1#/account/logout?age=2#hash2
- hash 模式 (basename = "/admin" )
createHref = /account/logout?age=2#hash2
createURL = /home/homepage?name=1#
+ /admin
+ /account/logout?age=2#hash2
history.createHref
对应上述所说的 生成对应跳转 url 地址不同
history.createURL
ts
/**
* 创建一个完整url地址的 RL对象
* 区别与 createHref : createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
* createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
* @param to 路径 或者 location对象
* @returns
*/
function createURL(to: To): URL {
// window.location.origin is "null" (the literal string value) in Firefox
// under certain conditions, notably when serving from a local HTML file
// See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
let base =
window.location.origin !== "null"
? window.location.origin
: window.location.href;
let href = typeof to === "string" ? to : createPath(to);
invariant(
base,
`No window.location.(origin|href) available to create URL for href: ${href}`
);
return new URL(href, base);
}