在進行開發過程當中,直接使用了Vue-Router來進行頁面跳轉,可是出現了一些奇奇怪怪的bug,特花時間來進行相關調研並記錄,若有不嚴謹或不正確的地方,歡迎指正探討。javascript
使用Vue-Router來進行頁面跳轉html
使用this.$router.push() 地址欄的連接不變,Iframe的src不變,可是Iframe的內容發生變化。vue
使用this.$router.go(-1) 來進行跳轉,地址欄連接改變,Iframe的src改變,Iframe的內容也發生變化。html5
使用this.$router.href()能夠進行跳轉,且地址欄發生改變java
說到路由跳轉就不得不提Window.history 系列的Api了,常見的Vue-router等路由處理其本質也都是在經過該系列Api來進行頁面切換操做。git
本次咱們討論的就主要涉及 到Window.history.pushState
和Window.history.go
。github
Window.history(下文將直接簡稱爲history)指向一個History對象,表示當前窗口的瀏覽歷史,History對象保存了當前窗口訪問過的全部頁面網址。web
go() 接受一個整數爲參數,移動到該整數指定的頁面,好比history.go(1)至關於history.forward(),history.go(-1)至關於history.back(),history.go(0)至關於刷新當前頁面ajax
back() 移動到上一個訪問頁面,等同於瀏覽器的後退鍵,常見的返回上一頁就能夠用back(),是從瀏覽器緩存中加載,而不是從新要求服務器發送新的網頁vue-router
forward() 移動到下一個訪問頁面,等同於瀏覽器的前進鍵
pushState() pushState()
須要三個參數:一個狀態對象(state),一個標題(title)和一個URL。
*注意:pushState會改變url,可是並不會刷新頁面,也就是說地址欄的url會被改變,可是頁面仍保持當前。
總之,pushState()
方法不會觸發頁面刷新,只是致使 History 對象發生變化,地址欄會有反應。
history.pushState({a:1},'page 2','2.html')
popState事件
每當同一個文檔的瀏覽歷史(即history對象)出現變化時,就會觸發popstate事件。簡單能夠理解爲,每次咱們須要修改url 那麼一定是先出發了popState事件,瀏覽器的地址欄隨後纔會發生改變。
注意,僅僅調用pushState()方法或replaceState()方法 ,並不會觸發該事件,**只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript 調用History.back()、History.forward()、History.go()方法時纔會觸發。**另外,該事件只針對同一個文檔,若是瀏覽歷史的切換,致使加載不一樣的文檔,該事件也不會觸發。
mode
#push src/history/html5.js
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
function pushState (url, replace) {
saveScrollPosition();
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
var history = window.history;
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
var stateCopy = extend({}, history.state);
stateCopy.key = getStateKey();
history.replaceState(stateCopy, '', url);
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url);
}
}
#go src/history/html5.js
go (n: number) {
window.history.go(n)
}
以上是Vue-router再history模式下push和go的源碼,可見其主要的實現是經過History Api來實現跳轉的。
vue-router 主要用來作單頁面,即更改 url 無需刷新可以渲染部分組件達到渲染不一樣頁面的效果,其中 history 模式監聽 url 的變化的也是由 popstate 實現的,而後監聽瀏覽器返回的方法也是大同小異。
原理是,A url-> B url,此時用戶點擊返回時,url 先回退到 A url,此時觸發 popstate 回調,vuerouter 根據 next 回調傳參是 false 判斷須要修成 A url 成 B url,此時須要將進行 pushstate(B url),則此時就實現了阻止瀏覽器回退的效果
Ps:篇幅緣由,源碼在文章底部附上。
那麼在進行了Iframe嵌套後會有什麼不同呢?
The sequence of
Document
s in a browsing context is its session history. Each browsing context, including child browsing contexts, has a distinct session history. A browsing context's session history consists of a flat list of session history entries.Each
Document
object in a browsing context's session history is associated with a uniqueHistory
object which must all model the same underlying session history.The
history
getter steps are to return this's associatedDocument
'sHistory
instance.-https://html.spec.whatwg.org/multipage/history.html#joint-session-history
簡單來講不一樣的documents在建立的時候都有本身的history ,同時內部的document在進行初始化時候具備相同的基礎HIstory。
如上,當咱們從頁面A進行跳轉之後,Top層,和內嵌Iframe層初始時是具備相同的history,所以,當咱們進入頁面後,不管是在頁面B 仍是頁面C中使用window.history.go(-1)都可以實現相同的效果,即返回頁面A,且瀏覽器的URl欄也會隨之發生改變。
當咱們從hybrid頁面跳向hybrid的時候
以下,此時若是在新的頁面內使用go(-1),則可能會出現問題【當頁面A和頁面B的History不一致時】,可是除了咱們手動去pushState改變,大部分狀況頁面A和頁面B的history是徹底一致的所以也就不會出現History不一致的問題了。
那麼來看一下咱們一開始遇到的問題:
注意:如下僅僅針對Chrome瀏覽器,不一樣瀏覽器對於Iframe中的HIstory Api處理方式可能會存在不同。
1.使用this.$router.push() 地址欄的連接不變,Iframe的src不變,可是Iframe的內容發生變化。
2.使用this.$router.go(-1) 來進行跳轉,地址欄連接改變,Iframe的src改變,Iframe的內容也發生變化。
3.使用this.$router.href()能夠進行跳轉,且地址欄發生改變
1.直接調用Router.push 至關於咱們在Iframe中調用了pushState,可是因爲pushState是不會主動觸發popstate的,因此外層的popstate是沒有被觸發,所以外層的url並沒有改變,可是內層因爲VueRouter經過對pushState的callBack事件來進行的後續操做,所以能夠實現對popState事件的觸發,從而實現了在將新的url push到history中之後,並進行了頁面的跳轉。
2.使用this.$router(-1) 能夠實現跳轉的緣由在於,在咱們進入一個hybrid頁面的時候,iframe的history會被初始化和window徹底相同,也就是說,這個時候咱們在Iframe中執行window.go(-1)取到的url 是和直接在Top執行Window。因此這個時候執行Router.go(-1)是能夠正常運行且返回上一個頁面的。
3.本質仍是對remote方法進行封裝 。
關於頁面IFrame中history Api的應用仍是存在着一些爭議和問題,在W3C的TPAC會議上也都有在進行相關的討論
雖然最後有了一些共識,可是對於各個瀏覽器來講,兼容性仍是不太一致。所以,建議你們在Iframe中使用history系列api時,務必當心並增強測試。
從上來看,是很是不科學的,iframe中能夠影響到Window的history,Chorme也認可這是一個漏洞。
用戶填寫表單時,須要監聽瀏覽器返回按鈕,當用戶點擊瀏覽器返回時須要提醒用戶是否離開。若是不須要,則須要阻止瀏覽器回退
popstate,MDN 的解釋是:當瀏覽器的活動歷史記錄條目更改時,將觸發 popstate 事件。
觸發條件:當用戶點擊瀏覽器回退或者前進按鈕時、當 js 調用 history.back,history.go, history.forward 時
但要特別注意:當 js 中 pushState, replaceState 並不會觸發 popstate 事件
window.addEventListener('popstate', function(state) {
console.log(state) // history.back()調用後會觸發這一行
})
history.back()
原理是進入頁面時,手動 pushState 一次,此時瀏覽器記錄條目會自動生成一個記錄,history 的 length 加 1。接着,監聽 popstate 事件,被觸發時,出彈窗給用戶確認,點取消,則須要再次 pushState 一次以恢復成沒有點擊前的狀態,點肯定,則能夠手動調用 history.back 便可實現效果
20200607233903
window.onload = (event) => {
window.count = 0;
window.addEventListener('popstate', (state) => {
console.log('onpopState invoke');
console.log(state);
console.log(`location is ${location}`);
var isConfirm = confirm('確認要返回嗎?');
if (isConfirm) {
console.log('I am going back');
history.back();
} else {
console.log('push one');
window.count++;
const state = {
foo: 'bar',
count: window.count,
};
history.pushState(
state,
'test'
// `index.html?count=${
// window.count
// }&timeStamp=${new Date().getTime()}`
);
console.log(history.state);
}
});
console.log(`first location is ${location}`);
// setTimeout(function () {
window.count++;
const state = {
foo: 'bar',
count: window.count,
};
history.pushState(
state,
'test'
// `index.html?count=${window.count}&timeStamp=${new Date().getTime()}`
);
console.log(`after push state locaiton is ${location}`);
// }, 0);
};
在Ajax請求雖然不會形成頁面的刷新,可是是沒有後退功能的,即點擊左上角是沒法進行後退的
若是須要進行後退的話 就須要結合PushState了
當執行Ajax操做的時候,往瀏覽器history中塞入一個地址(使用pushState)(這是無刷新的,只改變URL);因而,返回的時候,經過URL或其餘傳參,咱們就能夠還原到Ajax以前的模樣。
demo參考連接https://www.zhangxinxu.top/wordpress/2013/06/html5-history-api-pushstate-replacestate-ajax/
HIstory APi 學習 :
https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event
https://wangdoc.com/javascript/bom/history.html
https://www.cnblogs.com/jehorn/p/8119062.html
Vue-Router源碼
https://liyucang-git.github.io/2019/08/15/vue-router%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
https://zhuanlan.zhihu.com/p/27588422
Iframe相關問題學習:
https://github.com/WICG/webcomponents/issues/184
https://www.cnblogs.com/ranran/p/iframe_history.html
https://www.coder.work/article/6694188
http://www.yuanmacha.com/12211080140.html
開發應用:
https://www.codenong.com/cs106610163/
Vue-Router實現源碼:
#src/history/html5.js
beforeRouteLeave (to, from, next) { // url離開時調用的鉤子函數
if (
this.saved ||
window.confirm('Not saved, are you sure you want to navigate away?')
) {
next()
} else {
next(false) // 調用next(false) 就實現了阻止瀏覽器返回,請看下面
}
}
setupListeners () {
// 爲簡略,省略部分源碼
const handleRoutingEvent = () => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route => { // 這裏調用自定義的transitionTo方法,其實就是去執行一些隊列,包括各類鉤子函數
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent) // 在這裏添加popstate監聽函數
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
#下面看 transitionTo 的定義,參見 src/history/base.js
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
const route = this.router.match(location, this.current)
this.confirmTransition( // 調用自身的confirmTransition方法
route,
// 爲簡略,省略部分源碼
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
// changed after adding errors with
// https://github.com/vuejs/vue-router/pull/3047 before that change,
// redirect and aborted navigation would produce an err == null
if (!isRouterError(err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(createNavigationDuplicatedError(current, route))
}
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = [].concat( // 定義隊列
// in-component leave guards
extractLeaveGuards(deactivated), // 先執行當前頁面的beforeRouteLeave
// global before hooks
this.router.beforeHooks, // 執行新頁面的beforeRouteUpdate
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
this.pending = route
const iterator = (hook: NavigationGuard, next) => { // iterator將會在queue隊列中一次被執行,參見src/utils/async
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) { // next(false) 執行的是這裏
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true) // 關鍵看這裏:請看下面ensureURL的定義,傳true則是pushstate
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 爲簡略,省略部分源碼
}
#eusureURL 的定義,參見 src/history/html5.js
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current) // 執行一次pushstate } }