如果要你實現一個前端路由,應該如何實現瀏覽器的前進與后退 ?
首先瀏覽器中主要有這幾個限制,讓前端不能隨意的操作瀏覽器的瀏覽紀錄:
?沒有提供監聽前進后退的事件。?不允許開發者讀取瀏覽紀錄,也就是 js 讀取不了瀏覽紀錄。?用戶可以手動輸入地址,或使用瀏覽器提供的前進后退來改變 url。
所以要實現一個自定義路由,解決方案是自己維護一份路由歷史的記錄,從而區分 前進、刷新、回退。
下面介紹具體的方法。
目前筆者知道的方法有兩種,一種是 在數組后面進行增加與刪除,另外一種是 利用棧的后進先出原理。
我自己是一名從事了多年開發的web前端老程序員,目前辭職在做自己的web前端私人定制課程,今年年初我花了一個月整理了一份最適合2019年學習的web前端學習干貨,各種框架都有整理,送給每一位前端小伙伴,想要獲取的可以關注我的頭條號并在后臺私信我:前端,即可免費獲取。
通過監聽路由的變化事件 hashchange,與路由的第一次加載事件 load ,判斷如下情況:
?url 存在于瀏覽記錄中即為后退,后退時,把當前路由后面的瀏覽記錄刪除。?url 不存在于瀏覽記錄中即為前進,前進時,往數組里面 push 當前的路由。?url 在瀏覽記錄的末端即為刷新,刷新時,不對路由數組做任何操作。
另外,應用的路由路徑中可能允許相同的路由出現多次(例如 A -> B -> A),所以給每個路由添加一個 key 值來區分相同路由的不同實例。
注意:這個瀏覽記錄需要存儲在 sessionStorage 中,這樣用戶刷新后瀏覽記錄也可以恢復。
筆者之前實現的 用原生 js 實現的輕量級路由 ,就是用這種方法實現的,具體代碼如下:
// 路由構造函數
function Router() {
this.routes={}; //保存注冊的所有路由
this.routerViewId="#routerView"; // 路由掛載點
this.stackPages=true; // 多級頁面緩存
this.history=[]; // 路由歷史
}
Router.prototype={
init: function(config) {
var self=this;
//頁面首次加載 匹配路由
window.addEventListener('load', function(event) {
// console.log('load', event);
self.historyChange(event)
}, false)
//路由切換
window.addEventListener('hashchange', function(event) {
// console.log('hashchange', event);
self.historyChange(event)
}, false)
},
// 路由歷史紀錄變化
historyChange: function(event) {
var currentHash=util.getParamsUrl();
var nameStr="router-history"
this.history=window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []
var back=false, // 后退
refresh=false, // 刷新
forward=false, // 前進
index=0,
len=this.history.length;
// 比較當前路由的狀態,得出是后退、前進、刷新的狀態。
for (var i=0; i < len; i++) {
var h=this.history[i];
if (h.hash===currentHash.path && h.key===currentHash.query.key) {
index=i
if (i===len - 1) {
refresh=true
} else {
back=true
}
break;
} else {
forward=true
}
}
if (back) {
// 后退,把歷史紀錄的最后一項刪除
this.historyFlag='back'
this.history.length=index + 1
} else if (refresh) {
// 刷新,不做其他操作
this.historyFlag='refresh'
} else {
// 前進,添加一條歷史紀錄
this.historyFlag='forward'
var item={
key: currentHash.query.key,
hash: currentHash.path,
query: currentHash.query
}
this.history.push(item)
}
// 如果不需要頁面緩存功能,每次都是刷新操作
if (!this.stackPages) {
this.historyFlag='forward'
}
window.sessionStorage[nameStr]=JSON.stringify(this.history)
},
}
在說第二個方法之前,先來弄明白棧的定義與后進者先出,先進者后出原理。
棧的特點:后進者先出,先進者后出。
舉一個生活中的例子說明:就是一摞疊在一起的盤子。我們平時放盤子的時候,都是從下往上一個一個放;取的時候,我們也是從上往下一個一個地依次取,不能從中間任意抽出。
因為棧的后進者先出,先進者后出的特點,所以只能棧一端進行插入和刪除操作。這也和第一個方法的原理有異曲同工之妙。
下面用 JavaScript 來實現一個順序棧:
// 基于數組實現的順序棧
class ArrayStack {
constructor(n) {
this.items=[]; // 數組
this.count=0; // 棧中元素個數
this.n=n; // 棧的大小
}
// 入棧操作
push(item) {
// 數組空間不夠了,直接返回 false,入棧失敗。
if (this.count===this.n) return false;
// 將 item 放到下標為 count 的位置,并且 count 加一
this.items[this.count]=item;
++this.count;
return true;
}
// 出棧操作
pop() {
// 棧為空,則直接返回 null
if (this.count==0) return null;
// 返回下標為 count-1 的數組元素,并且棧中元素個數 count 減一
let tmp=items[this.count-1];
--this.count;
return tmp;
}
}
其實 JavaScript 中,數組是自動擴容的,并不需要指定數組的大小,也就是棧的大小 n 可以不指定的。
棧的經典應用: 函數調用棧
操作系統給每個線程分配了一塊獨立的內存空間,這塊內存被組織成“棧”這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量作為一個棧幀入棧,當被調用函數執行完成,返回之后,將這個函數對應的棧幀出棧。為了讓你更好地理解,我們一塊來看下這段代碼的執行過程。
function add(x, y) {
let sum=0;
sum=x + y;
return sum;
}
function main() {
let a=1;
let ret=0;
let res=0;
ret=add(3, 5);
res=a + ret;
console.log("res: ", res);
reuturn 0;
}
上面代碼也很簡單,就是執行 main 函數求和,main 函數里面又調用了 add 函數,先調用的先進入棧。
執行過程如下:
第二個方法就是:用兩個棧實現瀏覽器的前進、后退功能。
我們使用兩個棧,X 和 Y,我們把首次瀏覽的頁面依次壓入棧 X,當點擊后退按鈕時,再依次從棧 X 中出棧,并將出棧的數據依次放入棧 Y。當我們點擊前進按鈕時,我們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面可以繼續后退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面可以點擊前進按鈕瀏覽了。
比如你順序查看了 a,b,c 三個頁面,我們就依次把 a,b,c 壓入棧,這個時候,兩個棧的數據如下:
當你通過瀏覽器的后退按鈕,從頁面 c 后退到頁面 a 之后,我們就依次把 c 和 b 從棧 X 中彈出,并且依次放入到棧 Y。這個時候,兩個棧的數據就是這個樣子:
這個時候你又想看頁面 b,于是你又點擊前進按鈕回到 b 頁面,我們就把 b 再從棧 Y 中出棧,放入棧 X 中。此時兩個棧的數據是這個樣子:
這個時候,你通過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就無法再通過前進、后退按鈕重復查看了,所以需要清空棧 Y。此時兩個棧的數據這個樣子:
如果用代碼來實現,會是怎樣的呢 ?各位可以想一下。
其實就是在第一個方法的代碼里面, 添加多一份路由歷史紀錄的數組即可,對這兩份歷史紀錄的操作如上面示例圖所示即可,也就是對數組的增加和刪除操作而已, 這里就不展開了。
其中第二個方法與參考了 王爭老師的 數據結構與算法之美。
接:https://blog.csdn.net/u011518749/article/details/102774408
作者:山月行
們知道,在使用瀏覽器的后退和前進按鈕時,瀏覽器會使用緩存來優化您的頁面加載過程。它顯著改善了用戶的瀏覽體驗——尤其是那些網絡或設備較慢的用戶。作為 Web 開發人員,了解如何在所有瀏覽器中針對這種緩存優化頁面至關重要,這樣您的用戶才能獲得收益。
回退前進緩存是一種內存緩存,用于在用戶導航離開時存儲頁面(包括 JavaScript 堆)的完整快照。 整個頁面都在內存中,如果用戶決定返回,瀏覽器可以快速輕松地恢復它。比如當點擊返回時,在那一刻,該緩存可以對上一個頁面的加載速度產生很大的影響:
下面是有緩存和沒緩存的加載回退頁面的過程:
回退前進緩存是如何工作的
回退前進緩存使用的“緩存”不同于 HTTP 緩存(它對于加快重復導航也很有用)。 回退前進緩存是在內存緩存中整個頁面的快照(包括 JavaScript 堆),而 HTTP 緩存僅包含對先前發出的請求的響應。由于加載頁面所需的所有請求都可以從 HTTP 緩存中完成是非常罕見的,因此使用回退前進緩存恢復的重復訪問總是比優化得最好的非回退前進緩存導航更快。
然而,在內存中創建頁面快照涉及到如何最好地保存正在進行的代碼方面的一些復雜性。例如,當頁面在回退前進緩存中時,如何處理超時的 setTimeout() 調用?
答案是瀏覽器暫停運行任何掛起的計時器或未解決的Promise——就是說會掛起所有任務隊列中的任務——并在當從回退前進緩存恢復頁面時恢復處理任務。
在某些情況下,這是相當低的風險(例如,setTimeout或Promise),但在其他情況下,它可能會導致非常混亂或意外的行為。例如,如果瀏覽器暫停了作為 IndexedDB 事務的一部分所需的任務,它可能會影響同一源中的其他打開的選項卡(因為多個選項卡可以同時訪問相同的 IndexedDB 數據庫)。因此,瀏覽器通常不會嘗試在 IndexedDB 事務的中間緩存頁面或使用可能影響其他頁面的 API。
可以觀察回退前進緩存的API
雖然回退前進緩存是瀏覽器自動進行的優化,但對于開發人員來說,知道它何時發生仍然很重要,這樣他們就可以優化他們的頁面并相應地調整任何指標或性能測量。
用于觀察回退前進緩存的主要事件是頁面轉換事件——pageshow 和 pagehide——它們在回退前進緩存存在的時候就已經存在,并且在當今使用的幾乎所有瀏覽器中都得到了支持。較新的頁面生命周期事件(freeze和resume)也會在頁面進出回退前進緩存時以及在某些其他情況下調度。
觀察何時從回退前進緩存恢復頁面:
pageshow 事件在頁面初始加載時以及頁面從緩存恢復時的 load 事件之后立即觸發。 pageshow 事件有一個persisted屬性,如果頁面從緩存恢復,則該屬性為 true(如果不是,則為 false)。 您可以使用持久化屬性來區分常規頁面加載和緩存恢復。 例如:
觀察頁面什么時候進入回退前進緩存
pagehide 是 pageshow 事件的對應事件。 pageshow 事件在頁面正常加載或從緩存恢復時觸發。 pagehide 事件在頁面正常卸載或瀏覽器嘗試將其放入緩存時觸發。
pagehide 事件也有一個persisted屬性,如果它是假的,那么你可以確信頁面不會進入回退前進緩存。 但是,如果persisted 屬性為真,則不能保證頁面會被緩存。 這意味著瀏覽器打算緩存該頁面,但可能存在無法緩存的因素。下面會解釋幾種原因,導致它可能仍然不能進入緩存并被丟棄。
為回退前進緩存優化你的頁面
并非所有頁面都存儲在回退前進緩存中,即使頁面確實存儲在那里,它也不會無限期地留在那里。 開發人員必須了解是什么使頁面符合(和不符合)使用緩存的條件,以最大限度地提高其緩存命中率。以下部分概述了使瀏覽器盡可能緩存您的頁面的最佳實踐。
不要使用 unload 事件
在所有瀏覽器中優化回退前進緩存的最重要方法是永遠不要使用 unload 事件。
unload 事件對瀏覽器來說是有問題的,在 unload 事件觸發后頁面將不會繼續存在。這提出了一個挑戰,因為其中許多頁面也是在假設用戶導航離開時會觸發 unload 事件的假設下構建的,這不再是真的(并且很長一段時間都不是真的)。所以瀏覽器面臨兩難境地,他們必須在可以改善用戶體驗的東西之間做出選擇——但也可能會冒著破壞頁面的風險。
如果 Chrome 和 Firefox 添加了卸載偵聽器,則選擇使頁面不符合回退前進緩存的條件,這樣風險較小,但也會取消許多頁面被緩存的資格。 Safari 將嘗試使用 unload 事件偵聽器緩存某些頁面,但為了減少潛在的損壞,它不會在用戶導航離開時運行 unload 事件,這使得該事件非常不可靠。
不要使用 unload 事件,而是使用 pagehide 事件。 pagehide 事件在當前觸發 unload 事件的所有情況下觸發,并且當頁面放入回退前進緩存時也會觸發。
事實上,Lighthouse v6.2.0 添加了一個 no-unload-listeners 審計,如果他們的頁面上的任何 JavaScript(包括來自第三方庫的 JavaScript)添加了 unload 事件監聽器,它將警告開發人員。
正確的做法:
避免 window.opener 引用
在某些瀏覽器(包括基于 Chromium 的瀏覽器)中,如果頁面是使用 window.open() 或(在 88 版之前基于 Chromium 的瀏覽器中)從帶有 target=_blank 的鏈接打開的——沒有指定 rel="noopener"——那么 打開頁面將引用打開頁面的窗口對象。
除了存在安全風險之外,具有非空 window.opener 引用的頁面不能安全地放入緩存,因為這可能會破壞任何試圖訪問它的頁面。
因此,最好盡可能避免使用 rel="noopener" 創建 window.opener 引用。 如果您的站點需要打開一個窗口并通過 window.postMessage() 或直接引用 window 對象來控制它,則打開的窗口和打開器都沒有資格使用緩存。
始終在用戶導航離開之前關閉打開的連接
如上所述,當頁面被放入緩存時,所有計劃的 JavaScript 任務都會暫停,然后在頁面從緩存中取出時恢復。如果這些計劃好的 JavaScript 任務僅訪問 DOM API(或僅與當前頁面隔離的其他 API),那么在頁面對用戶不可見時暫停這些任務不會導致任何問題。
但是,如果這些任務連接到的 API 也可以從同一來源的其他頁面訪問(例如:IndexedDB、Web Locks、WebSockets 等),這可能會出現問題,因為暫停這些任務可能會阻止其他選項卡中的代碼運行.
因此,某些瀏覽器在以下情況下不會嘗試將頁面放入緩存:
如果您的頁面使用這些 API 中的任何一個,最好在 pagehide 或 freeze 事件期間始終關閉連接并刪除或斷開觀察者。這將允許瀏覽器安全地緩存頁面,而不會影響其他打開的選項卡。然后,如果頁面從緩存恢復,您可以重新打開或重新連接到這些 API(在 pageshow 或 resume 事件中)。
以下示例顯示了如何在使用 IndexedDB 時通過關閉 pagehide 事件偵聽器中的打開連接來確保您的頁面符合緩存條件:
緩存恢復后更新陳舊或敏感數據
如果您的站點保留用戶狀態(尤其是任何敏感的用戶信息),則需要在從回退前進緩存恢復頁面后更新或清除該數據。例如,如果用戶導航到結帳頁面然后更新他們的購物車,如果從緩存恢復過時的頁面,則返回導航可能會顯示過時的信息。
另一個更關鍵的示例是,如果用戶在公共計算機上退出站點,并且下一個用戶單擊后退按鈕。 這可能會暴露用戶在注銷時認為已清除的私人數據。為避免此類情況,如果 event.persisted 為 true,則最好在 pageshow 事件之后始終更新頁面。
以下代碼檢查 pageshow 事件中是否存在特定于站點的 cookie,如果未找到 cookie,則重新加載:
測試一下保證你的頁面會被緩存
Chrome DevTools 可以幫助您測試您的頁面,以確保它們針對回退前進緩存進行了優化,并確定可能阻止它們符合條件的任何問題。
要測試特定頁面,請在 Chrome 中導航到該頁面,然后在 DevTools 中轉到 Application > Back-forward Cache。 接下來單擊 Run Test 按鈕,DevTools 將嘗試導航并返回以確定頁面是否可以從回退前進緩存恢復。
如果成功,則會顯示從緩存恢復成功,失敗也有提示,并告訴你原因。
成功
比如百度使用了unload導致緩存失敗 :)
結語:
回退前進緩存對提高性能,及用戶體驗很有幫助。大家要寫正確的代碼來使自己的頁面盡量被緩存,這樣你們的網站才能大大獲益。
近在工作中遇到一個bug,將word轉換成html,轉換成功之后在瀏覽器中打開其中圖片不顯示,使用img標簽,src指定圖片相對地址又是能顯示的,排除圖片問題。
網上能搜索到的demo
打開轉碼之后的html代碼發現,生成的是vml圖片標簽,這個在IE9以后就不支持了,更別說現在的主流瀏覽器了。
生成的html中使用的是vml標簽
將這個跟大佬分析分析,各種文檔一查,咔咔咔大致分析出問題所在。原來jacob使用的是word本身自帶的功能,相當于把word打開另存為html,于是手動將word轉為html試了一下,果然效果與代碼轉換一致,這時候注意到word另存為時有一個web選項,里面有個使用vml渲染圖片默認是選中的,去掉這個選項,再次生成,圖片正常顯示。
到這里基本已經確定了問題的解決思路,另存為時不勾選這個選項,那么問題來了,怎么利用jacob操作另存為時去掉這個選項呢,想去搜搜看jacob相關的文檔,結果不知道是不是因為這個很老了,網上大多數都是demo,根本沒有相關的文檔可看,Github上也是只言片語,根本無從查起。
jacob github 地址:https://github.com/joval/jacob
微軟官網文檔
官方文檔連接:https://docs.microsoft.com/zh-cn/office/vba/api/word.weboptions.relyonvml
微軟官網查詢相關文檔,發現其實是可以關閉的,于是代碼變成這樣
關閉relyOnVml
再次運行程序,這次轉出來的html就能在瀏覽器打開了。
總結,在這次解決問題的過程中,學會了往更深層次去想問題,找對方向,迎難而上。
記錄一下這個問題解決的經驗,也希望能幫到同樣遇到這個問題的人。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。