序
在傳統頁面中,每個url都是和頁面系結的,即使是單頁面,也應該有這種習慣,因此我們把Page物件作為不同的url,通過頁面的切換來更改url,url的變換代表著用戶在該應用中的探索路徑,用戶可以隨時的后退到上一個頁面,或者前進到后一個頁面,從上面的history篇章中,使用瀏覽器的提供的api可以很好的解決這個問題,然而在實際的使用中存在一個問題:比如用戶從一個頁面跳轉到另外一個頁面,然后后退到前一個頁面,發現上一個頁面并不是他之前訪問的頁面了,原因如下:
- 后退到上一個頁面,頁面重新渲染時得到的資料和上一次的資料不一樣;
- 后退到上一個頁面,無法讓頁面后退到之前瀏覽的那個位置;
- 渲染的頁面有問題,
一般第三點都不會發生,第二點也不是很重要,如果在切換頁面的時候存盤跳轉時的滾動地址,也可以很好的解決,第一點確實是個值得思考的問題,原因如下:
- 第一次頁面進入請求了,第二次是否還需要去請求資料;
- 如果第二次不去請求,直接渲染第一次顯示的資料,那怎么保證頁面的資料是最新的呢;
- 按照2的做法,意味著頁面不是重新渲染,恢復的頁面以及dom系結的事件和最后離開頁面的時候應該是保持一致的;
- 按照3的做法,我們必須要存盤歷史記錄中的頁面相關資料,比如快取dom,快取data等;
- 關于快取data,是否可以把data完全交給history.state,它不就是為了快取頁面資料而存在的么?不過這里并非采用了history.state,因為當我們要任意更改歷史記錄的時候,之前沒有保存在歷史記錄的data就無法被還原,通過記憶體存盤就能增加靈活性,當然history.state可以作為備案,要求快取的資料的可序列化的,
合理的歷史記錄一般不會有太多級,而且把歷史記錄中的頁面儲存起來,可以加快頁面的渲染速度,增強用戶體驗,
需求
通過上面的討論,我們需要實作如下的目標:
- 針對歷史記錄創建一層資料快取,用來實作頁面導航的時候可以快速切換頁面,并將其還原;
- 資料快取中主要存盤Page物件中的DOM,事件和data;
- 要保證快取資料和歷史記錄的資料保持高度一致,當新的頁面添加到歷史記錄的時候,快取記錄就會新增一條記錄,洗掉歷史記錄時,就洗掉快取中對應的頁面資料;
- 為了保證頁面資料和后臺資料的一致性,提供了一個可選的方法,主要是為了進行資料更新以及頁面的區域更新,這個可選方法只有在快速切換的時候才會呼叫,
實作思路
之前的Page物件實作中, 每次渲染一個新頁面的時候,都會new一個指定的頁面,現在我們只要在切換頁面的時候把它快取起來,等到需要的時候,再把它還原回去,只有重新創建一個頁面的時候或者歷史記錄中沒有快取頁面的時候,才會去new一個新Page物件,現在我們在Page的原型物件中新增兩個方法:
- save(), 將資料快取起來;
- restore(dom), 將資料還原到頁面上,
同時頁面的生命周期也產生了變化,因為從歷史記錄中還原頁面不再進行render,getDomObj和beforeInit流程了,通過restore方法,就快速的將頁面還原到瀏覽器了,為了保證資料一致性,添加了一個afterRestore可選方法,進行區域更新,保證前后端資料更新互動,
save方法是對Page物件的淺洗掉操作,將剩余的必要資訊快取起來,之前的destroy是深度洗掉操作,是為了洗掉所有的參考,相對應的,要進行快取的Page物件的生命周期是這樣的:
- restore(dom):將Page還原到頁面上;
- 如果存在afterRestore方法,呼叫之后,通過區域更新從而保證頁面一致;
- 呼叫init方法,初始化該頁面需要引入的插件;
- 日常的業務處理,等待用戶切換頁面;
- 首先呼叫dispose方法,這個方法主要是處理引入的插件的銷毀;
- save(),將資料快取起來, 如果存在beforeSave方法,先呼叫beforeSave,
如下代碼
save: function () {
this.isSave = true; // 設定狀態
this.destroy(false); // 淺洗掉
this.parent.history.save(this); // 保存頁面于歷史快取中
if (typeof this.beforeSave === "function") this.beforeSave();
this.nodes = []; // 把dom全部快取起來
for (var i = 0; i < this.parentDom.childNodes.length; i++) {
this.nodes.push(this.parentDom.childNodes[i]);
}
},
restore: function (dom) {
dom.innerHTML = ''; // 清除
// 保證dom是原來的dom
var fragment = this.template.content;
for (var i = 0; i < this.nodes.length; i++) {
fragment.appendChild(this.nodes[i]);
}
dom.appendChild(fragment);
this.nodes.length = 0;
if (typeof this.afterRestore === "function") this.afterRestore();
this._beforeInit();
// 這方法后緊接著就是系結事件,
},
// 呼叫destroy(true)僅在歷史記錄沒有該頁面的時候呼叫,否則都是淺洗掉
destroy: function (isClean) {
this.dispatchEvent("_dispose");
if (isClean) {
this.eventDispatcher.destroy();
this._removeDom();
this.template = null;
this.nodes.length = 0;
this.parent = null;
this.data = https://www.cnblogs.com/stringWeb/p/{};
this.parentDom = null;
}
},
// _dispose方法放在_init里面定義,僅能被呼叫一次
_init: function () {
this.isSave = false;
this.eventDispatcher.clearListenerByType("_dispose"); // 清除這個事件,然后系結新的
this.attachDiyEvent("_dispose", function () {
if (typeof this.dispose === "function") this.dispose();
this._removeEventListener();
this.http.destroy();
}, true) // 呼叫后銷毀
if (typeof this.init === "function") this.init.apply(this, arguments);
},
新增一個HistoryStorage物件,用于存盤頁面快取,并且修改原先的History物件, 讓History專注于互動,HistoryStorage專注于存盤,并將之前的popstate事件轉到History物件下, 修改如下
function History(app) {
this.app = app;
this.appStorage = new HistoryStorage();
this.skipPop = false; // 是否要過濾popstate事件
this.popBack = null; // 過濾popstate事件時執行的可變方法
// 代表監聽popstate實作
window.addEventListener("popstate", this._popHandler.bind(this));
}
History.prototype = {
constructor: History,
// 設定鎖屏與否
setLock: function (isLock) {
this.appStorage.setLock(isLock);
},
// 獲取是否鎖屏狀態
getLock: function () {
return this.appStorage.isLock
},
// 恢復頁面
restore: function () {
this.appStorage.restore();
},
pushState: function (page, option) {
this.appStorage.pushState(page, option);
},
replaceState: function (page, option) {
this.appStorage.replaceState(page, option);
},
_popHandler: function (ev) {
var app = this.app, that = this;
if (this.getLock()) return this.restore();
if (this.skipPop)
if (typeof this.popBack === "function") return this.popBack();
// 改變hash也會觸發popstate事件
if (location.pathname == app.currentPage.url) return;
// 傳入三個方法,分別代表當前位置是否在首頁,如果是首頁執行第二個方法,不是首頁則執行第三個方法
this.appStorage.popOperation(function (name, nameList) {
return nameList.indexOf(name) === 0;
}, function (component) {
if (typeof app.outofHistory === "function") app.outofHistory();
setTimeout(function () {
that.pushState(component);
app._renderPage(component, true); // 渲染頁面
}, 2000);
}, function (component, config, str) {
that.renderBackComponent(app.component, config, str);
});
}
};
History的實際操作轉向StorageHistory, 為了后期能更好的擴展,
function HistoryStorage() {
this.history = []; // 存放url陣列,對應歷史記錄
this.components = []; // 存放Page物件
this.datas = []; // 存放資料,這里的資料和Page物件,還有包含其它資料
this.index = null; // 指向當前的位置
this.isLock = false;
}
HistoryStorage.prototype = {
constructor: HistoryStorage,
popOperation: function (filter, elseFn, operationFn) {
var name = this._getCurrentHistoryName();
if (filter(name, this.history)) {
elseFn(this.components[this.index]);
} else {
var urlObj = this._getSurroundUrl(),
str, config,
component = this.components[this.index];
component.save(); // 保存頁面
if (urlObj.prev === name) {
config = this.datas[--this.index];
str = "out";
} else {
config = this.datas[++this.index];
str = "in";
}
operationFn(this.components[this.index], config, str);
}
},
// 指向pushState和replaceState的時候,把資料和page物件保存起來
replaceState: function (component, option) {
option = option || {};
var url = option.url || component.url;
// 銷毀頁面
if (this.components[this.index]) this.components[this.index].destroy(true);
if (location.protocol !== "file:") history.replaceState(option, "", url);
this.datas[this.index] = option;
this.components[this.index] = component;
this.history[this.index] = url;
},
pushState: function (component, option) {
option = option || {};
var url = this.type == "app" ?
option.url || component.url :
location.pathname + "?popup=" + component.name,
components = this.components,
datas = this.datas;
if (components[this.index] && !components[this.index].isSave)
components[this.index].save();
if (location.protocol !== "file:") history.pushState(option, "", url);
if (typeof this.index === "number") {
for (var i = components.length - 1; i >= this.index + 1; i--) {
// 移除頁面的時候銷毀頁面
if (components[i]) components[i].destroy(true);
}
var nextIndex = ++this.index,
len = components.length;
components.splice(nextIndex, len - nextIndex, component);
datas.splice(nextIndex, len - nextIndex, option);
this.history.splice(nextIndex, len - nextIndex, url);
} else {
this.index = 0;
this.components.push(component);
this.history.push(url);
this.datas.push(option);
}
},
// 其它操作和之前的類似,這里省略
}
案例地址
結語
通過history和Page配合,讓頁面能夠快速的切換,極大的提升了頁面的切換速度,讓webapp更像原生app,
推廣
底層框架開源地址:https://gitee.com/string-for-100w/string
演示網站: https://www.renxuan.tech/
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/172610.html
標籤:JavaScript
上一篇:6.Page物件詳解
