序
就好像用戶看到的都是由dom表現出來的,所有的業務處理都是在Page物件中處理的,如果業務越簡單,創建的Page物件數量就會越少;如果業務越復雜,那么相對而言Page物件數量就越多(或Page實體物件就會越復雜),
Page物件主要做以下事情:
- 渲染頁面,保證dom元素的合理安排,以及事件的處理;
- 資料互動,將資料正確的放在dom元素中,并進行合理的前后端資料互動,
無論是在哪個時候,這兩點確實是前端開發的重中之重,換一句話說這就是前端核心開發內容,
為了讓Page物件更加專注于上面所提的兩件事情,將處理業務的細節轉移到復雜services的檔案中,讓它成為資料樞紐,安排資料的走向,弄清楚資料到底是渲染到頁面上還是保存起來,從而做進一步的前后端資料互動,另一方面,把復雜的渲染部分,封裝成組件,將渲染邏輯以及資料渲染頁面的邏輯交給組件內部處理,減少Page代碼量,讓業務處理更加清晰,
我們在Page對dom進行原子性的操作,而不是另外抽出一層作為單獨渲染層,從分離上看似更加合理,然而在日常開發中,瀏覽器對于dom的處理已經夠全面了,大部分是可以通過一句代碼來實作的,對于復雜點的,通過簡單的封裝或者組件的處理就能實作(組件對dom也是直接操作),在Page物件中處理代碼也不會太多,如果為了封裝而失去修改的方便性,其實是得不償失的,而且不同層之間的互動,會讓代碼更加的難以理解(因為dom也算是一層渲染層,額外加一次就顯得比較多余),這也是為什么堅持使用最原始的html的原因之一,
通過上面的分析,我們更傾向于把Page物件的主要任務作為資料的樞紐,負責資料的運輸,把資料讓給渲染層顯示,或將資料做處理保存,或將資料進行前后端資料互動等事情, 相當于MVC架構中的Controller部分,html渲染出來的dom層代表著View層,Page物件實際上沒有保存長期資料的習慣,如果需要長期保存的資料,可以把它放在App物件中,或者把它放在services的某個檔案中,
需求
因為頁面和history有很大的關系,并且當前顯示的頁面必須顯示在瀏覽器端的標簽欄中,秉承著異步按需加載的特點,將頁面的配置項固定設定為 { title: "頁面標題", url: "頁面url", js: "頁面的定義js檔案", name: "頁面的名稱" }對于Page物件,我希望能滿足以下情況:
- 它具有一般模塊的方法,還應該擁有對渲染層事件系結等處理;
- 擁有傳統的方式,將dom快取起來,下次使用獲取時加快速度,擁有事件管理能力(不然頁面切換無法解綁dom事件);
- 只有它擁有與后端互動的能力,僅有Page物件才擁有與后端互動的所有能力,前后端互動是業務的核心之一,
這里要特別注意,在異步操作中,有時候頁面切換的時候,回呼函式中處理dom的時候,會因為dom已被銷毀而出現錯誤;因此我們對頁面切換會對所有該頁面發起的ajax做中斷處理,在別的異步操作中,要確保異步操作完成后,再做頁面切換作業,
實作思路
每個Page物件從加載到銷毀, 定義為一個生命周期,程序如下,用圖表示:

- 獲取Page實體物件的js,加載js;
- 呼叫render方法,將html獲取到加載到某個dom中,這里處理的方式是放在fragment中;
- 接著呼叫getDomObj方法,目的就是快取dom,并且系結事件;
- 將fragment加載到瀏覽器的dom中,展示頁面,如果存在beforeInit方法,先執行beforeInit方法;
- 呼叫init方法,初始化該頁面需要引入的插件;
- 日常的業務處理,等待用戶切換頁面;
- 首先呼叫dispose方法,這個方法主要是處理引入的插件的銷毀;
- 銷毀,主要是移除dom事件的注冊,移除dom的參考,洗掉臨時資料, 與后端ajax互動中斷等,
我們創建一個Page物件
function Page(name, title, url) {
BaseProto.call(this); // 繼承自定義事件能力
this.domList = {}; // 快取dom
this.eventList = {}; // 快取事件和dom的關系
this.parent = null; // 現在指App物件
this.parentDom = null; // 指向Page物件放置的dom
this.template = document.createElement("template");
this.http = new Http(this); // 用于AJAX互動,后續介紹
this.data = https://www.cnblogs.com/stringWeb/p/{}; // 放置私有物件
}
Page.prototype = Object.create(BaseProto.prototype, {
// 實體化的Page物件必須要重寫這兩個方法, 對應的步驟2
render: function (next) {
throw new Error("render方法必須繼承重寫")
},
// 使用attachDom和attachEvent用來快取dom和快取事件
getDomObj: function (next) {
throw new Error("getDomObj方法必須繼承重寫")
},
// render方法獲取html后,將html放在dom里面,bk代表初始化后的回呼呼叫,對應步驟2, 3, 4
initialize: function (dom, html, bk) {
this.template.innerHTML = html;
var fragment = this.template.content;
this.getDomObj();
dom.appendChild(fragment);
this._beforeInit(bk);
},
_beforeInit: function (next) {
var that = this;
if (typeof this.beforeInit === "function") {
this.beforeInit(function () {
that._init.apply(that, arguments); // 可以傳參
if (typeof next === "function") next();
})
}
else {
this._init();
if (typeof next === "function") next();
}
},
// 步驟5,6交給用戶處理
_init: function () {
this._addEventListeners(); // 系結事件
if (typeof this.init === "function") this.init.apply(this, arguments); // 開始處理業務
},
// 代表銷毀物件,對應步驟7, 8
destroy: function () {
// 清除外部參考
if (typeof this.dispose === "function") this.dispose();
this._removeEventListeners(); // 事件移除
this.eventDispatcher.destroy(); // 自定義事件銷毀
this.eventList.length = 0; // 事件快取清除
this._removeDom(); // 移除dom
this.template = null;
this.parent = null;
this.data = https://www.cnblogs.com/stringWeb/p/{};
this.parentDom = null;
this.http.destroy(); // 銷毀物件,阻止未結束的請求,
},
// 快取dom
attachDom: function (cssQuery, key) {
this.domList[key] = this.template.content.querySelector(cssQuery);
return this;
},
// 快取事件
attachEvent: function (key, eventStr, fn, passive, doFn) {
passive = passive || false;
var eventList = this.eventList;
doFn = doFn || fn.bind(this);
// 獲取對應key的dom系結事件陣列描述物件
var eventObj = getEvent(eventList, { key: key });
if (eventObj) {
var eventArray = eventObj.eventArray;
// 找到該事件的系結方法陣列
var methodEventObj = getEvent(eventArray, { method: eventStr });
if (methodEventObj) {
var fnArray = methodEventObj.fnArray;
// 是否已經系結,防止重復
var obj = getEvent(fnArray, { backFn: fn, passive: passive });
if (!obj) fnArray.push({ backFn: fn, passive: passive, doFn: doFn });
} else {
eventArray.push({ method: eventStr,
fnArray: [{ backFn: fn, passive: passive, doFn: doFn }]
})
}
} else {
eventList.push({
key: key,
eventArray: [{
method: eventStr,
fnArray: [{ backFn: fn, passive: passive, doFn: doFn }]
}]
})
}
return this;
},
// 剩下的就是_addEventListeners,_removeEventListeners就是決議eventList的資料格式
// 系結事件和移除事件, 程序略過
});
下面來實作AJAX實作細節,代碼如下
function Http(target) {
this.target = target;
this.list = [];
}
Http.prototype = {
constructor: Http,
// ajax方法
ajax: function (option, bk) {
var list = this.list, target = this.target,
useType = false, commonHeader = Http.commonHeader;
var xhr = new XMLHttpRequest();
list.push(xhr);
if (option.username)
xhr.open(option.method, option.url, option.async,
option.username, option.password);
else
xhr.open(option.method, option.url, option.async);
var header = option.header || {};
// 作為公共header
for (var key in commonHeader) {
xhr.setRequestHeader(key, commonHeader[key])
}
// 特殊化header
for (var i in header) {
if (header["Content-Type"] !== "multipart/form-data") {
xhr.setRequestHeader(i, header[i]);
}
}
// 配置二進制流請求
if ("type" in option) {
xhr.responseType = option.type;
useType = true;
}
xhr.onload = function () {
target.dispatchEvent("xhrload", { xhr: xhr });
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
var index = list.indexOf(xhr);
list.splice(index, 1);
var result = useType ?
xhr.response || xhr.responseText :
xhr.responseText;
option.success.call(option.target, result);
if (typeof bk === "function") bk(result);
} else {
if (typeof option.error === "function") option.error.call(target, xhr);
}
if (typeof option.complete === "function") option.complete.call(target, xhr);
target.dispatchEvent("xhrcomplete", { xhr: xhr });
};
xhr.onerror = function () {
target.dispatchEvent("xhrerror", { xhr: xhr });
if (typeof option.error === "function") option.error.call(target, xhr);
if (typeof option.complete === "function") option.complete.call(target, xhr);
target.dispatchEvent("xhrcomplete", { xhr: xhr });
};
if ("onabort" in option) {
xhr.onabort = option.onabort;
}
var data = https://www.cnblogs.com/stringWeb/p/null;
// 默認以url-encode
if (option.data) {
if ( header["Content-Type"] == "application/json") data = https://www.cnblogs.com/stringWeb/p/JSON.stringify(option.data);
else if (header["Content-Type"] == "multipart/form-data") {
data = https://www.cnblogs.com/stringWeb/p/new FormData();
for (var key in option.data) {
data.append(key, option.data[key]);
}
}
else data = serialize(option.data);
}
xhr.send(data);
target.dispatchEvent("xhrstart", { xhr: xhr });
return xhr;
},
// 銷毀
destroy: function () {
var list = this.list;
for (var i = list.length - 1; i >= 0; i--) {
// 中斷請求,防止切換頁面導致回呼函式中操作dom造成錯誤
list[i].abort();
list.splice(i, 1);
}
}
};
接下來在Page的原型物件中加入post,get方法,
post: function (url, data, fn) {
var obj = createRequest(this, url, data, feeback, option, "POST");
this.http.ajax(obj);
},
get: function (url, fn) {
var obj = createRequest(this, url, undefined, feeback, option, "GET");
this.http.ajax(obj);
},
公共函式,創建請求引數的統一方式,
function createRequest(target, url, data, feeback, option, method) {
option = option || {};
option.header = option.header || {};
option.header["x-request-with"] = "XMLHttpRequest";
if (!("Content-Type" in option.header)) option.header["Content-Type"] = "application/json";
option.method = method || "GET";
option.success = feeback.bind(target);
if ("onabort" in option) option.onabort = option.onabort.bind(target);
option.target = target;
option.url = url;
option.data = https://www.cnblogs.com/stringWeb/p/data;
option.async = typeof option.async ==="undefined" ? true : option.async;
return option;
}
案例地址
總結
主要對Page物件的用途做了簡要的介紹,以及它的生命周期,并且著重對ajax做了簡要的封裝, 下一章針對Page與history的綜合
應用進行介紹,
推廣
底層框架開源地址:https://gitee.com/string-for-100w/string
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/172609.html
標籤:JavaScript
上一篇:4.App物件的介紹和影片管理
