為什麼在Iframe中不能使用Vue-Router
1.場景
在進行開發過程中,直接使用了Vue-Router來進行頁面跳轉,但是出現了一些奇奇怪怪的bug,特花時間來進行相關調研並記錄,如有不嚴謹或不正確的地方,歡迎指正探討。
問題
使用Vue-Router來進行頁面跳轉
使用this.$router.push() 位址列的連結不變,Iframe的src不變,但是Iframe的內容發生變化。
使用this.$router.go(-1) 來進行跳轉,位址列連結改變,Iframe的src改變,Iframe的內容也發生變化。
使用this.$router.href()可以進行跳轉,且位址列發生改變
2.路由處理
說到路由跳轉就不得不提Window.history 系列的Api了,常見的Vue-router等路由處理其本質也都是在通過該系列Api來進行頁面切換操作。
本次我們討論的就主要涉及 到Window.history.pushState
和Window.history.go
。
Window.history(下文將直接簡稱為history)指向一個History物件,表示當前視窗的瀏覽歷史,History物件儲存了當前視窗訪問過的所有頁面網址。
2.1History常見屬性與方法
go() 接受一個整數為引數,移動到該整數指定的頁面,比如history.go(1)相當於history.forward(),history.go(-1)相當於history.back(),history.go(0)相當於重新整理當前頁面
back() 移動到上一個訪問頁面,等同於瀏覽器的後退鍵,常見的返回上一頁就可以用back(),是從瀏覽器快取中載入,而不是重新要求伺服器傳送新的網頁
forward() 移動到下一個訪問頁面,等同於瀏覽器的前進鍵
pushState() pushState()
需要三個引數:一個狀態物件(state),一個標題(title)和一個URL。
*注意:pushState會改變url,但是並不會重新整理頁面,也就是說位址列的url會被改變,但是頁面仍保持當前。
總之,pushState()
方法不會觸發頁面重新整理,只是導致 History 物件發生變化,位址列會有反應。
history.pushState({a:1},'page2','2.html')
popState事件
每當同一個文件的瀏覽歷史(即history物件)出現變化時,就會觸發popstate事件。簡單可以理解為,每次我們需要修改url 那麼必定是先出發了popState事件,瀏覽器的位址列隨後才會發生改變。
注意,僅僅呼叫pushState()方法或replaceState()方法 ,並不會觸發該事件,**只有使用者點選瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript 呼叫History.back()、History.forward()、History.go()方法時才會觸發。**另外,該事件只針對同一個文件,如果瀏覽歷史的切換,導致載入不同的文件,該事件也不會觸發。
2.2Vue-Router的實現
mode
#pushsrc/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)
}
functionpushState(url,replace){
saveScrollPosition();
//try...catchthepushStatecalltogetaroundSafari
//DOMException18whereitlimitsto100pushStatecalls
varhistory=window.history;
try{
if(replace){
//preserveexistinghistorystateasitcouldbeoverridenbytheuser
varstateCopy=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);
}
}
#gosrc/history/html5.js
go(n:number){
window.history.go(n)
}
以上是Vue-router再history模式下push和go的原始碼,可見其主要的實現是通過History Api來實現跳轉的。
2.3Vue-Router是如何實現單頁應用的呢?
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巢狀後會有什麼不一樣呢?
3.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也承認這是一個漏洞。
4.實際開發中的應用
1.返回檢測
1.實際開發需求:
使用者填寫表單時,需要監聽瀏覽器返回按鈕,當用戶點選瀏覽器返回時需要提醒使用者是否離開。如果不需要,則需要阻止瀏覽器回退
2.實現原理:監聽 popstate 事件
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('onpopStateinvoke');
console.log(state);
console.log(`locationis${location}`);
varisConfirm=confirm('確認要返回嗎?');
if(isConfirm){
console.log('Iamgoingback');
history.back();
}else{
console.log('pushone');
window.count++;
conststate={
foo:'bar',
count:window.count,
};
history.pushState(
state,
'test'
//`index.html?count=${
//window.count
//}&timeStamp=${newDate().getTime()}`
);
console.log(history.state);
}
});
console.log(`firstlocationis${location}`);
//setTimeout(function(){
window.count++;
conststate={
foo:'bar',
count:window.count,
};
history.pushState(
state,
'test'
//`index.html?count=${window.count}&timeStamp=${newDate().getTime()}`
);
console.log(`afterpushstatelocaitonis${location}`);
//},0);
};
2.Ajax請求後可以後退
在Ajax請求雖然不會造成頁面的重新整理,但是是沒有後退功能的,即點選左上角是無法進行後退的
如果需要進行後退的話 就需要結合PushState了
當執行Ajax操作的時候,往瀏覽器history中塞入一個地址(使用pushState)(這是無重新整理的,只改變URL);於是,返回的時候,通過URL或其他傳參,我們就可以還原到Ajax之前的模樣。
demo參考連結https://www.zhangxinxu.top/wordpress/2013/06/html5-history-api-pushstate-replacestate-ajax/
5.參考資料
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('Notsaved,areyousureyouwanttonavigateaway?')
){
next()
}else{
next(false)//呼叫next(false)就實現了阻止瀏覽器返回,請看下面
}
}
setupListeners(){
//為簡略,省略部分原始碼
consthandleRoutingEvent=()=>{
constcurrent=this.current
//Avoidingfirst`popstate`eventdispatchedinsomebrowsersbutfirst
//historyroutenotupdatedsinceasyncguardatthesametime.
constlocation=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
){
constroute=this.router.match(location,this.current)
this.confirmTransition(//呼叫自身的confirmTransition方法
route,
//為簡略,省略部分原始碼
)
}
confirmTransition(route:Route,onComplete:Function,onAbort?:Function){
constcurrent=this.current
constabort=err=>{
//changedafteraddingerrorswith
//https://github.com/vuejs/vue-router/pull/3047beforethatchange,
//redirectandabortednavigationwouldproduceanerr==null
if(!isRouterError(err)&&isError(err)){
if(this.errorCbs.length){
this.errorCbs.forEach(cb=>{
cb(err)
})
}else{
warn(false,'uncaughterrorduringroutenavigation:')
console.error(err)
}
}
onAbort&&onAbort(err)
}
if(
isSameRoute(route,current)&&
//inthecasetheroutemaphasbeendynamicallyappendedto
route.matched.length===current.matched.length
){
this.ensureURL()
returnabort(createNavigationDuplicatedError(current,route))
}
const{updated,deactivated,activated}=resolveQueue(
this.current.matched,
route.matched
)
constqueue:Array<?NavigationGuard>=[].concat(//定義佇列
//in-componentleaveguards
extractLeaveGuards(deactivated),//先執行當前頁面的beforeRouteLeave
//globalbeforehooks
this.router.beforeHooks,//執行新頁面的beforeRouteUpdate
//in-componentupdatehooks
extractUpdateHooks(updated),
//in-configenterguards
activated.map(m=>m.beforeEnter),
//asynccomponents
resolveAsyncComponents(activated)
)
this.pending=route
constiterator=(hook:NavigationGuard,next)=>{//iterator將會在queue佇列中一次被執行,參見src/utils/async
if(this.pending!==route){
returnabort(createNavigationCancelledError(current,route))
}
try{
hook(route,current,(to:any)=>{
if(to===false){//next(false)執行的是這裡
//next(false)->abortnavigation,ensurecurrentURL
this.ensureURL(true)//關鍵看這裡:請看下面ensureURL的定義,傳true則是pushstate
abort(createNavigationAbortedError(current,route))
}elseif(isError(to)){
this.ensureURL(true)
abort(to)
}elseif(
typeofto==='string'||
(typeofto==='object'&&
(typeofto.path==='string'||typeofto.name==='string'))
){
//next('/')ornext({path:'/'})->redirect
abort(createNavigationRedirectedError(current,route))
if(typeofto==='object'&&to.replace){
this.replace(to)
}else{
this.push(to)
}
}else{
//confirmtransitionandpassonthevalue
next(to)
}
})
}catch(e){
abort(e)
}
}
//為簡略,省略部分原始碼
}
#eusureURL的定義,參見src/history/html5.js
ensureURL(push?:boolean){
if(getLocation(this.base)!==this.current.fullPath){
constcurrent=cleanPath(this.base+this.current.fullPath)
push?pushState(current):replaceState(current)//執行一次pushstate
}
}