前置知识:浏览器记录历史和跳转
这部分大多数人都有了解,这里简单总结一下勾起你们的记忆:
- 无刷新跳转有两种模式:window.history.pushState(replaceState)和location.hash;
- pushState和replaceState区别:pushState可以增加历史记录,而replaceState自会替换当前记录;
- pushState(replaceState)第一个参数是需要传入的状态,可以通过
history.state
来获取。内部会使用结构化拷贝算法
进行序列化存储,会将拷贝后的结果记录在历史栈的记录中; - 如果当前不处于历史栈顶时,再次进行pushState,当前历史以后的记录都会被覆盖重写;
- window.history.go 可以通过数字参数来实现前进后退,注意:
history.go(0)
,其意义为刷新当前页面,与location.reload方法行为一致;history.go只是控制栈指针在栈内移动,不会有进栈和出栈的操作; - 在设置location.hash时要注意,如果设置的location.hash值与浏览器URL地址的hash值相同,就不会触发任何事件,也不会添加任何历史记录。或者如果前后两次对location.hash设置了相同的值,则仅第一次location.hash设置生效,第二次相同的设置不会产生任何事件和历史记录;
- 如果希望在改变地址栏hash的同时,也不进行入栈操作,则可通过
window.location.replace
实现(但在设置绝对路径时,其会刷新页面); - 单击浏览器的“后退”或“前进”按钮,或者调用history的go、back、forward等方法,或者更改部分浏览器的hash,都会触发
popstate
事件,但使用history.pushState或history.replaceState不会触发; hashchange
事件可以通过设置location.hash、在地址栏中手动修改hash、调用window.history.go、在浏览器中单击“前进”或“后退”按钮等方式触发;- 注意,window.history.pushState不会触发hashchange事件,即使前后导航的URL仅hash部分不同,也是如此;
- 可以通过
window.dispatchEvent(new PopStateEvent('popstate'))
来手动触发popstate事件;同理可以通过window.dispatchEvent(new HashChangeEvent('hashchange'))
来手动触发hashchange事件。
history库
history库是React Router重要的依赖库,扮演着导航执行者与监听者的重要角色,提供了3类历史对象:browserHistory(浏览器历史对象)、hashHistory(哈希历史对象)和memoryHistory(内存历史对象)。
history库提供了3类历史对象的创建工厂函数,分别是createBrowserHistory、createHashHistory和createMemoryHistory。
上面的工厂函数都可以传入配置对象,配置对应的历史对象的行为。其中有一个getUserConfirmation
跳转确认函数。
getUserConfirmation跳转确认函数通常用于在用户操作流程没有结束又产生导航时提醒用户,需要配合history.back
使用。history.block接受不传入参数,或者传入一个string、boolean类型的参数或一个prompt 函数。当调用history.block时,任何导航包括浏览器的前进或后退行为都将被阻止,或者以某种形式提示用户确认导航。
当prompt为string类型时,默认弹出系统的prompt,一般为window.confirm。如果创建history时传入了getUserConfirmation,则使用传入的prompt。
browserHistory
browserHistory也叫浏览器历史对象,与浏览器中的window.location对象的各属性完全兼容。可以传入以下配置:
interface BrowserHistory {
basename?: string;
forceRefresh?: boolean;
getUserConfirmation?: typeof getUserConfirmation;
keyLength?: number;
}
复制代码
解析如下:
- basename: 基准路径,当使用createHref、history.push和history.replace等方法都会得到basename与path的拼接;
- forceRefresh: 对于browserHistory,默认的跳转不会造成页面刷新,如果设置forceRefresh为true,则在跳转过程中会强制刷新页面;
- getUserConfirmation:跳转确认函数;
- keyLength:每次push调用,都将产生一个随机的key值,该key值可从browserHistory.location中获取,并持久化存储于window.history.state中。这个随机值的字符串长度由创建history时的keyLength配置进行控制,默认为6,其作用为标识本次导航。
browserHistory.push
browserHistory.push
方法底层调用history.pushState,但是跟原生的pushState又有几点不同:
- 第一个参数除了是字符串地址,还可以传入路径描述对象,如下所示:
browserHistory.push({
pathname:'/index',
search:'?id=1',
hash:'#test',
state:{pid=11}
})
复制代码
- 跟原生的不同,可以在
back
方法被调用后改变行为,如下所示:
history.back()
history.push('/about') // 在调用back()方法后,该push调用没有效果
history.back('确定跳转到about?')
history.push('/about') // 此时调用push会弹出系统默认弹窗,点击确定跳转页面
复制代码
- pushState不会触发popState事件,而push在状态更新后,会触发
history.listen
监听的回调函数,回调函数的参数为当前最新的地址对象和值为“PUSH”的导航行为标识;
注意:当创建history的forceRefresh为true时,框架将不使用pushState原生方法,而是直接调用window.location.href=href刷新页面
browserHistory.replace
browserHistory.replace
方法底层调用history.replaceState,由于replaceState的调用不会触发popstate事件,所以replace的调用也不会触发popstate事件。同样要注意的是,如果forceRefresh
为true,将使用window.location.replace(href)进行页面更新,强制刷新页面。
browserHistory.listen
browserHistory.listen
可以监听hisotry.push、history.replace、history.go等方法改变location,但是在页面初始化的时候不会触发,同时这里监听location“变化”并不表示前后导航过程中location的值不一致发生变化,当前后导航过程中产生的location一致时,同样会触发history.listen的回调函数。
history的设计风格都是在调用订阅函数之后返回一个取消订阅的函数,可调用取消订阅函数取消history的监听
hashHistory
当浏览器不支持pushState等原生接口时,可以使用hashHistory,如果不希望在页面切换时刷新页面,同时希望将页面的URL存储在浏览器地址的hash中,也可使用hashHistory。
在创建hashHistory时,可以传入配置对象,如下所示:
interface HashHistory {
basename?:string;
hashType?: HasnType;
getUserConfirmation?: typeof getUserConfirmation;
}
复制代码
其他参数在上面已经讲述过了,这里重点讲解hashType
。hashType支持hashbang
、noslash
及slash
3种类型,如果在创建时不指明hashType,则默认的hashType为slash,这三种类型的区别如下所示:
- hashbang: 对应的hash路径“#”后会接上“!”与“/;
- noslash: 对应的hash路径“#”没有“/”;
- slash: 对应的hash路径“#”接上“/”;
注意:在创建hashHistory时,即使没有任何操作,路径也会改变,比如hashType=slash时,路径会自动带上#/,这是因为在初始化时,会调用replaceHashPath(‘/’)来初始化hashType。
hashHistory.push
hashHistory.push
方法底层调用的是location.hash
,其参数既可以接收字符串形式的路径,也可以接收location的描述对象,如下所示:
hashHistory.push({
pathname:'/index',
search:'?id=1',
hash:'#test',
state:{pid=11}
})
复制代码
hashHistory会把浏览器地址的hash部分进行路由储存,每次调用push仅会改变hash值。
跟browserHistory一样,hashHistory.push一样会被back
方法改变行为,具体看上面的列子。
下面有几点值得注意的地方:
- 如果在push方法中传入的字符中带有
#
号, 那么就会产生两个#号,第一个是hashHistory所标记的地址,第二个是hashHistory中location的hash值,如下所示:
location.href = 'http://www.xxx.com/#/index'
hashHistory.push('/about#id')
console.log(window.location.href) // http://www.xxx.com/#/about#id
console.log(hashHistory.location.hash) // #id
console.log(window.location.hash) // #/about#id
复制代码
- 如果hashHistory.push设置了state参数,低版本的history库会发出警告不支持,为了兼容性,不建议直接在第二个参数设置state,可以在location对象里面设置state,但是只能在hashHistory.location.state中读取,不能在window.history.state中读取到,而且不具备持久化state的能力。
hashHistory.replace 和 hashHistory.go方法跟browerHistory的基本一致,暂不展开讲述了-。-
history.createHref
createHref方法可将location对象转换为对应的URL字符串,在Link
组件内部使用createHref方法创建a标签的href属性值。
在使用hashHistory.createHref的时候,要注意下面两点:
- createHref不会对原字符串做任何编解码处理;
- createHref会判断HTML文档流中是否有href属性的base元素,有会进行特殊处理,得到完全的路径;
memoryHistory
memoryHistory的运行环境通常不在浏览器内,一般作为测试使用或如ReactNative原生环境,创建的时候,可以传入下面的对象配置:
interface MemoryHistory {
getUserConfirmation?: typeof getUserConfirmation;
initialEntries?: string[];
initialIndex?: number;
keyLength?: number;
}
复制代码
解析如下:
initialEntries
类似于Browser Router或Hash Router的历史栈,通过它可以初始化栈内容,默认是[‘/’];initialIndex
表示初始的栈指针位置,默认是0;
对于memoryHistory,其除了通用的history的属性,还多出index、entries和canGo属性:
- index是当前历史栈指针的指针位置;
- entries为历史栈数组;
- canGo用来判断跳转位置n是否可以跳转;
memoryHistory.push
调用memoryHistory.push,可以把location存储在内存中,通过memoryHistory.location
访问,或者entries
访问;
与browserHistory、hashHistory一样,memoryHistory不仅支持调用block阻止跳转,还支持相对路径导航、保存state等,但是它的state是通过自身维护的,不是存在于window.history.state中进行持久化存储。
history库原理
运行流程
无论是哪种形式的history,其内部的流程都是通用的。首先history统一维护了回调数组,用于存储listen监听的函数;当调用push,replace等方法时,会确认是否调用过back方法,如果调用过并且入参不为false,则会在导航前进行判定,有两种情况:1.参数是字符串,则会将字符串内容用于提示用户,由用户判定是否执行导航操作;2. 参数是false,则会取消导航并且没有提示。当导航成功后,history会在内部收集所有的变动,并且更新到history的各个属性,当history的状态更新完,会触发listen里的回调函数。注意,由于memoryHistory不运行在浏览器中,所以不会原生事件监听和阻止导航。
history.back解析
history.back是如何做到拦截导航的呢,关键是内部调用了transitionManager.confirmTransitionTo
,其主要作用是确认是否应该阻止跳转。
confirmTransitionTo的关键地方在于设置prompt
函数:首先判断prompt是否为null,如果不为null,就进行拦截。但是如果通过go方法或者点击导航栏的前进后退,是怎么实现拦截的呢,实质上点击前进后退的时候,地址已经改变的了,这个时候就需要用到人工恢复
。
人工恢复的思路是:通过模拟的内存栈(比如browserHistory的allKeys),已经发生跳转的地址,以及希望恢复的地址来计算出恢复所需要的距离delta
,最后通过go(delta)来跳转地址。
参考文献
- 深入理解React Router: 从原理到实现