序
在以往的日常前端開發中,使用最原始的html元素來拼湊整個頁面,隨著頁面慢慢的復雜起來,如果還是用這種方式,就會出現以下問題:
- 頁面使用了大量的html元素,導致頁面卡頓;
- 維護頁面困難,大量的html元素需要手動去更改;
- 大量的業務代碼和渲染代碼混雜,業務更改導致頁面顯示不正常,除錯會浪費很多的時間,
在市面有很多框架使用了mvvm的架構,通過資料驅動頁面的更改,確實能解區域分問題,然而卻引入了一大堆的定義資料格式代碼,往往控制一個狀態需要雙倍的代碼,且并沒有發揮html的本質意義,
之前關于Page物件的介紹,Page物件主要是用于處理業務,如果在渲染層沒有簡化Page實體物件更改渲染的話,最后的結果是和以往的頁面是類似的,因此我想到的解決方案,就是引入一種基于dom思想理念的一種組件方式,將業務邏輯抽離出來,然后通過呼叫組件的屬性和方法對組件進行操作,通過監聽組件來觸發組件的事件,Page與組件的互動和Page與dom的互動保持一致,
引入Compoent物件的意義在于,分擔Page物件處理渲染層的壓力,舉個簡單的例子,就拿原生的時間輸入框,我們把它當作一個組件,在使用得時候,我們只需要引入html標簽
<input type="date">
關于里面的細節放在組件定義上實作,在業務實作中不需要理解組件的實作原理,如果我們要改變組件的值或者改變它的行為
input.value = "https://www.cnblogs.com/stringWeb/p/2014-10-01";
input.focus();
如果組件進行了更改,我們可以監聽事件來進行互動
input.addEventListener("change", fn, false);
與其它框架不同,這里引入的組件是一個符合標準的xml格式的標簽,它的屬性值只能是string或數值,不能包含陣列或物件,因為保持標準的html結構才能讓html可以跨框架,即使放在原生瀏覽器中,也能保持良好的表現形式,即使不依賴任何js,
這一篇主要介紹組件的原理,處理組件的生命周期,以及如何和Page物件的互動,
下一篇主要介紹組件的定義,怎么樣創建一個標準的組件,包括一些技巧,
需求
類似的,我們需要組件和Page物件、service一樣,可以按需引入,我們希望組件有以下的功能:
- 和其它物件一樣,擁有初始化,銷毀等方法,在不需要的時候減少記憶體的使用;
- 使用方式盡量要和原生dom要一致;
- 具有自定義組件功能,可以根據不同的場景填充不同的內容,類似slot;
- 配合Page在history上的操作,擁有保存和快速還原等功能,
實作思路
首先,引入了組件之后,我們可以把頁面渲染結構看作下面的圖,

每個頁面都包含若干個組件,同時每個組件也有若干個組件,在每個頁面中,應該是一棵組件樹,
在上一篇中,我們抽出了HtmlProto,Page和Component都是和HTML有關的,將它們都繼承于HtmlProto物件,因此他就實作了初始化,銷毀等方法,在HtmlProto上加入一個陣列,專門存放Component物件陣列,同時加一個屬性uid,代表唯一的值,
其次,我們在頁面上插入html的時候,我們要做的處理應該在html放在fragment上時候,然后進行如下操作:
- 組件篩選,將它們用臨時dom替換并標記,決議標簽內的屬性和內容,作為資料保存起來;
- 通過標簽引入組件的js,然后實體化組件物件,在組件初始化的時候,可能其中內容包含其它組件,將會是一個遞回的程序,直到初始化完畢;
- 將生成的fragment替換之前的臨時dom,
這三個程序就是組件的初始化程序,無論是在頁面上還是組件上,都是同樣的,
最后,在業務處理后,如果頁面是保存操作,那每個組件都要進行保存,如果頁面是直接銷毀,每個組件也就銷毀了,另外的,如果該頁面是從歷史中取出來的,那么組件執行的是restore程序,restore中的組件執行和頁面邏輯是一樣的,快速還原組件狀態,
最終代碼如下
HtmlProto的initialize方法
initialize: function (dom, html, option, feeback) {
option = option || {};
this.parentDom = dom;
this.template.innerHTML = html;
var fragment = this.template.content;
// 第一步在fragment篩選符合條件的元素
var components = this._beforeInitComponent(fragment, html);
var that = this;
// 初始化組件
this._initComponent(components, function () {
// 組件因為是替換操作,沒有parentDom,所以保存nodes,后續進行相同的操作,
// 在組件的操作中,要保持nodes的排序一致性
that.beforeInitialize(fragment);
that.getDomObj();
// staticPage做插入檔案操作
if (option.method === "insert") dom.insertBefore(fragment, dom.firstChild);
// 組件做replace
else if (option.method === "replace") dom.parentNode.replaceChild(fragment, dom);
else if (option.method === "before") dom.parentNode.insertBefore(fragment, dom);
else { // Page做默認覆寫操作
dom.innerHTML = "";
dom.appendChild(fragment);
}
that._beforeInit(feeback);
});
},
其中_beforeInitComponent使用了TreeWalker物件, 使用了過濾方法
var tree = document.createTreeWalker(dom, NodeFilter.SHOW_ELEMENT, componentfilter.filter, false);
var component = tree.firstChild();
while (component) {
var obj = {
component: component, // 元素
id: component.id, // id
name: component.tagName.toLowerCase(), // tagName
slot: component.innerHTML, // 插槽
dataset: component.dataset, // 自定義資料
property: {}, // 屬性
originHTML: originHTML // 原始html
};
var attributes = component.attributes;
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i];
obj.property[attribute.name] = attribute.value; // 將屬性放在property上
}
components.push(obj);
component = tree.nextNode();
}
其中的componentfilter
var componentfilter = (function () {
var preNode = null, pattern = /^(\w+-)+\w+$/;
return {
destroy: function () {
preNode = null;
},
filter: function (node) {
if (node.constructor.name === "HTMLElement" && pattern.test(node.tagName)) {
if (preNode && preNode.contains(node)) return NodeFilter.FILTER_SKIP;
preNode = node;
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
}
})();
這里_initComponent方法主要是把上面的components生成Component物件,最終的是_addComponent方法
_initComponent: function (components, next) {
if (components.length === 0) return next();
var len = components.length;
var feeback = function () {
if (--len === 0) {
next();
}
}
for (var i = 0; i < components.length; i++) {
this._addComponent(components[i], feeback);
}
},
_addComponent: function (component, feeback) {
var that = this;
var app = this._getApp();
// 加載組件定義js,
app.getComponentByName(component.name, function (com, config) {
if (com) {
var newComponent = new com(), id = component.id;
newComponent.baseUrl = getBaseUrl(config.js);
if (id) newComponent.id = id;
newComponent.parent = that;
extend(newComponent.data, component.dataset); // 宣告在html上的data賦值
extend(newComponent.dataset, component.dataset); // 保持html的一致
extend(newComponent.property, component.property); // 把屬性附加到組件中
newComponent.slot = component.slot;
newComponent.render(function (html) {
// 這是將定義的slot和html上的標記結合起來
var nextHtml = newComponent.initSlot(html, newComponent.slot);
// 如果組件中包含其它組件,這是一個遞回的程序
newComponent.initialize(component.div, nextHtml, {
method: "replace"
}, function () {
that.components.push(newComponent);
if (typeof feeback === "function") feeback();
});
})
} else {
if (typeof feeback === "function") feeback();
}
})
},
下面是initSlot方法,這個方法在Component的原型物件中宣告
這里的難點在于相同的組件可以多層嵌套,和模板渲染引擎的情況有點像,無法用正則匹配,
對于html中使用
<a-btn>
<div slot="content">content</div>
<div slot="name">name</div>
</a-btn>
組件定義
<button >
<slot name="name">a</slot>
<slot name="content">b</slot>
</button>
生成的組件是會變成這樣
<button >
<div>name</div>
<div>content</div>
</button>
如果組件定義中slot沒有被html中定義,將會默認以slot的元素的形式渲染,如果是包含多個元素,比如將content的slot變成多個元素,可以用以下
<template slot="content"><div>1</div><div>2</div></template>
在Component實體物件中,都有一個slots陣列,他們是保存插槽的資訊,插槽的進一步使用,需要組件中實作,具體initSlot的方法實作,參見源代碼
組件和原生dom一樣,都可以通過定義事件,監聽事件來進行互動,因為組件也是通過html元素方式嵌入頁面的,它也支持dom事件,通過自定義事件可以讓事件進行冒泡,以下是觸發自定義事件的方法
dispatchCustomEvent: function (name, data) {
if (this.nodes.length > 0) {
var event = document.createEvent("CustomEvent");
event.initCustomEvent(name, true, true, data);
this.nodes[0].dispatchEvent(event);
}
return this;
},
在最后一步,組件的save和restore,是一個決議樹的程序,統一先將元素組components先save或restore,如下代碼
save: function () {
HtmlProto.prototype.save.call(this);
if (typeof this.beforeSave === "function") {
this.beforeSave();
}
for (var i = 0; i < this.components.length; i++) {
this.components[i].save();
}
var div = document.createElement("div");
div.id = this.uid;
div.className = "loadinghidden";
if (this.nodes.length > 0 && this.nodes[0].parentNode) this.nodes[0].parentNode.insertBefore(div, this.nodes[0]);
},
restore: function () {
var fragment = this.template.content;
var div = document.getElementById(this.uid);
for (var i = 0; i < this.nodes.length; i++) {
fragment.appendChild(this.nodes[i]);
}
if (div && div.parentNode) {
div.parentNode.replaceChild(fragment, div);
}
for (var i = 0; i < this.components.length; i++) {
this.components[i].restore();
}
if (typeof this.afterRestore === "function") {
this.afterRestore();
}
this._init();
},
與Page的restore和save執行流程類似,
案例地址
結語
這里主要介紹了Component的原理,如何將Component物件內嵌到頁面中,將Component的使用方式盡量和原生的元素保持一致.
推廣
底層框架開源地址:https://gitee.com/string-for-100w/string
演示網站: https://www.renxuan.tech/
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/172612.html
標籤:JavaScript
下一篇:10.組件實戰
