序
在以往的日常前端开发中,使用最原始的html元素来拼凑整个页面,随着页面慢慢的复杂起来,如果还是用这种方式,就会出现以下问题:
- 页面使用了大量的html元素,导致页面卡顿;
- 维护页面困难,大量的html元素需要手动去更改;
- 大量的业务代码和渲染代码混杂,业务更改导致页面显示不正常,调试会浪费很多的时间。
在市面有很多框架使用了mvvm的架构,通过数据驱动页面的更改,确实能解决部分问题,然而却引入了一大堆的定义数据格式代码,往往控制一个状态需要双倍的代码,且并没有发挥html的本质意义。
之前关于Page对象的介绍,Page对象主要是用于处理业务,如果在渲染层没有简化Page实例对象更改渲染的话,最后的结果是和以往的页面是类似的。因此我想到的解决方案,就是引入一种基于dom思想理念的一种组件方式,将业务逻辑抽离出来,然后通过调用组件的属性和方法对组件进行操作,通过监听组件来触发组件的事件,Page与组件的交互和Page与dom的交互保持一致。
引入Compoent对象的意义在于,分担Page对象处理渲染层的压力。举个简单的例子,就拿原生的时间输入框,我们把它当作一个组件,在使用得时候,我们只需要引入html标签
<input type="date">
关于里面的细节放在组件定义上实现,在业务实现中不需要理解组件的实现原理,如果我们要改变组件的值或者改变它的行为
input.value = "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 class="a-btn-style">
<slot name="name">a</slot>
<slot name="content">b</slot>
</button>
生成的组件是会变成这样
<button class="a-btn-style">
<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/