#yyds干货盘点#组件化漫谈

今天前端生态里面,​React​​Angular​​Vue​三分天下。虽然这三个框架的定位各有不同,但是它们有一个核心的共同点,那就是提供了组件化的能力。​W3C​也有​Web Component​的相关草案,也是为了提供组件化能力。今天我们就来聊聊组件化是什么,以及它为什么这么重要。

其实组件化思想是一种前端技术非常自然的延伸,如果你使用过​​HTML​​​,相信你一定有过“我要是能定义一个标签就好了”这样的想法。​​HTML​​虽然提供了一百多个标签,但是它们都只能实现一些非常初级的功能。

​HTML​​本身的目标,是标准化的语义,既然是标准化,跟自定义标签名就有一定的冲突。所以从前端最早出现的2005年,到现在2022年,我们一直没有等到自定义标签这个功能,至今仍然是​​Draft​​状态。

但是,前端组件化的需求一直都存在,历史长流中工程师们提出过很多组件化的解决方案。

ExtJS

​Ext JS​是一个流行的​JavaScript​框架,它为使用跨浏览器功能构建​Web​应用程序提供了丰富的​UI​。我们来看看它的组件定义:

MainPanel = function() {
this.preview = new Ext.Panel({
id: "preview",
region: "south"
// ...
});
MainPanel.superclass.constructor.call(this, {
id: "main-tabs",
activeTab: 0,
region: "center"
// ...
});

this.gsm = this.grid.getSelectionModel();

this.gsm.on(
"rowselect", function(sm, index, record) {
// ...
}, this, { buffer: 250 }
);

this.grid.store.on("beforeload", this.preview.clear, this.preview);
this.grid.store.on("load", this.gsm.selectFirstRow, this.gsm);

this.grid.on("rowdbclick", this.openTab, this);
};

Ext.extend(MainPanel, Ext.TabPanel, {
loadFeed: function(feed) {
// ...
},
// ...
movePreview: function(m, pressed) {
// ...
}
});

你可以看到​​ExtJS​​​将组件设计成一个函数容器,接受组件配置参数​​options​​​,​​append​​​到指定​​DOM​​​上。这是一个完全使用​​JS​​​来实现组件的体系,它定义了严格的继承关系,以及初始化、渲染、销毁的生命周期,这样的方案很好地支撑了​​ExtJS​​的前端架构。

HTML Component

搞前端时间比较长的同学都会知道一个东西,那就是​​HTC​​(​​HTML Components​​),这个东西名字很现在流行的​​Web Components​​很像,但却是不同的两个东西,它们的思路有很多相似点,但是前者已是昨日黄花,后者方兴未艾,是什么造成了它们的这种差距呢?

因为主流浏览器里面只有​​IE​​支持过​​HTC​​,所以很多人潜意识都认为它不标准,但其实它也是有标准文档的,而且到现在还有链接,注意它的时间!

在​​MSDN online​​对​​HTC​​的定义仅如下几句:

HTML Components (HTCs) provide a mechanism to implement components in script as Dynamic HTML (DHTML) behaviors. Saved with an .htc extension, an HTC is an HTML file that contains script and a set of HTC-specific elements that define the component.
(​​HTC​​是由​​HTML​​标记、特殊标记和脚本组成的定义了​​DHTML​​特性的组件.)

作为组件,它也有属性、方法、事件,下面简要说明其定义方式:

  • ​<PUBLIC:COMPONENT></PUBLIC:COMPONENT>​​:定义​​HTC​​,这个标签是其他定义的父元素。
  • ​<PUBLIC:PROPERTY NAME=”pName” GET=”getMethod” PUT=”putMethod” />​​: 定义​​HTC​​的属性,里面三个定义分别代表属性名、读取属性、设置属性时​​HTC​​所调用的方法。
  • ​<PUBLIC:METHOD NAME=”mName” />​​:定义​​HTC​​的方法,​​NAME​​定义了方法名。
  • ​<PUBLIC:EVENT NAME=”eName” ID=”eId” />​​:定义了​​HTC​​的事件,​​NAME​​定义了事件名,​​ID​​是个可选属性,在​​HTC​​中唯一标识这个事件。
  • ​<PUBLID:ATTACH EVENT=”sEvent” ONEVENT=”doEvent” />​​:定义了浏览器传给​​HTC​​事件的相应方法,其中​​EVENT​​是浏览器传入的事件,​​ONEVENT​​是处理事件的方法。

我们来看看它主要能做什么呢?

它可以以两种方式被引入到​​HTML​​页面中,一种是作为“行为”被附加到元素,使用​​CSS​​引入,一种是作为“组件”,扩展​​HTML​​的标签体系

行为为脚本封装和代码重用提供了一种手段

通过行为,可以轻松地将交互效果添加为可跨多个页面重用的封装组件。例如,考虑在 ​​Internet Explorer 4.0​​​ 中实现​​onmouseover highlight​​​的效果,通过使用 ​​CSS​​​ 规则,以及动态更改样式的能力,很容易在页面上实现这种效果。在 ​​Internet Explorer 4.0​​​ 中,实现在列表元素​​li​​​上实现 ​​onmouseover​​​ 高亮可以使用​​onmouseover​​​和​​onmouseout​​​事件动态更改​​li​​元素样式:

<HEAD>
<STYLE>
.HILITE
{ color:red;letter-spacing:2; }
</STYLE>
</HEAD>

<BODY>
<UL>
<LI onmouseover="this.className='HILITE'"
onmouseout ="this.className=''">HTML Authoring</LI>
</UL>
</BODY>

从 ​​Internet Explorer 5​​​ 开始,可以通过 ​​DHTML​​​ 行为来实现此效果。当将​​DHTML​​​行为应用于​​li​​元素时,此行为扩展了列表项的默认行为,在用户将鼠标移到其上时更改其颜色。

下面的示例以 ​​HTML​​​ 组件 (​​HTC​​​) 文件的形式实现一个行为,该文件包含在​​hilite.htc​​​文件中,以实现鼠标悬停高亮效果。使用 ​​CSS​​​ 行为属性将行为应用到元素​​li ​​​上。上述代码在 ​​Internet Explorer 5​​ 及更高版本中可能如下所示:

// hilite.htc
<HTML xmlns:PUBLIC="urn:HTMLComponent">
// <ATTACH> 元素定义了浏览器传给HTC事件的相应方法,其中EVENT是浏览器传入的事件,ONEVENT是处理事件的方法
<PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="Hilite()" />
<PUBLIC:ATTACH EVENT="onmouseout" ONEVENT="Restore()" />
<SCRIPT LANGUAGE="JScript">
var normalColor;

function Hilite()
{
if (event.srcElement == element)
{
normalColor = style.color;
runtimeStyle.color = "red";
runtimeStyle.cursor = "hand";
}
}

function Restore()
{
if (event.srcElement == element)
{
runtimeStyle.color = normalColor;
runtimeStyle.cursor = "";
}
}
</SCRIPT>

通过​CSS behavior​属性将​DHTML​行为附加到页面上的元素

<HEAD>
<STYLE>
LI {behavior:url(hilite.htc)}
</STYLE>
</HEAD>

<BODY>
<UL>
<LI>HTML Authoring</LI>
</UL>
</BODY>

HTC自定义标记

我们经常看到某些网页上有这样的效果:用户点击一个按钮,文本显示,再次点击这个按钮,文本消失,但浏览器并不刷新。下面我就用​​HTC​​来实现这个简单效果。编程思路是这样的:用​​HTC​​模拟一个开关,它有​​”on”​​和​​”off”​​两种状态(可读/写属性​​status​​);用户可以设置这两种状态下开关所显示的文本(设置属性 ​​turnOffText​​和​​turnOnText​​);用户点击开关时,开关状态被反置,并触发一个事件(​​onStatusChanged​​)通知用户,用户可以自己写代码来响应这个事件;该​​HTC​​还定义了一个方法(​​reverseStatus​​),用来反置开关的状态。下面是这个​​HTC​​的代码:

<!—switch.htc定义 -->  
<PUBLIC:COMPONENT TAGNAME="Switch">
<!--属性定义-->
<PUBLIC:PROPERTY NAME="turnOnText" PUT="setTurnOnText" VALUE="Turn on" />
<PUBLIC:PROPERTY NAME="turnOffText" PUT="setTurnOffText" VALUE="Turn off" />
<PUBLIC:PROPERTY NAME="status" GET="getStatus" PUT="setStatus" VALUE="on" />

<!--定义事件-->
<PUBLIC:EVENT NAME="onStatusChanged" ID="changedEvent" />

<!--定义方法-->
<PUBLIC:METHOD NAME="reverseStatus" />

<!--关联客户端事件-->
<PUBLIC:ATTACH EVENT="oncontentready" ONEVENT="initialize()"/>
<PUBLIC:ATTACH EVENT="onclick" ONEVENT="expandCollapse()"/>

</PUBLIC:COMPONENT>

<!-- htc脚本 -->
<script language="javascript">
var sTurnOnText; //关闭状态所显示的文本
var sTurnOffText; //开启状态所显示的文本
var sStatus; //开关状态
var innerHTML //使用开关时包含在开关中的HTML

//设置开关关闭状态所显示的文本
function setTurnOnText(value)
{
sTurnOnText = value;
}

//设置开关开启状态所显示的文本
function setTurnOffText(value)
{
sTurnOffText = value;
}

//设置开关状态
function setStatus(value)
{
sStatus = value;
}

//读取开关状态
function getStatus()
{
return sStatus;
}

//反向开关的状态
function reverseStatus()
{
sStatus = (sStatus == "on") ? "off" : "on";
}

//获取htc控制界面html文本
function getTitle()
{
var text = (sStatus == "on") ? sTurnOffText : sTurnOnText;
text = "<div id='innerDiv'>" + text + "</div>";
return text;

}

//htc初始化代码
function initialize()
{
//back up innerHTML
innerHTML = element.innerHTML;
element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();
}

//响应用户鼠标事件的方法
function expandCollapse()
{
reverseStatus();
//触发事件
var oEvent = createEventObject();
changedEvent.fire(oEvent);
var srcElem = element.document.parentWindow.event.srcElement;
if(srcElem.id == "innerDiv")
{
element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();
}
}
</script>

​html​页面引入自定义标记

html页面引入自定义标记

<!--learnhtc.html-->
<html xmlns:frogone><!--定义一个新的命名空间-->
<head>
<!--告诉浏览器命名空间是由哪个HTC实现的-->
<?IMPORT namespace="frogone" implementation="switch.htc">
</head>
<body>
<!--设置开关的各个属性及内部包含的内容-->
<frogone:Switch id="mySwitch"
TurnOffText="off"
TurnOnText="on"
status="off"
onStatusChanged="confirmChange()">
<div id="dBody">文本内容...... </div>
</frogone:Switch>
</body>
<script language="javascript">
//相应开关事件
function confirmChange()
{
if(!confirm("是否改变开关状态?"))
mySwitch.reverseStatus();
}
</script>
</html>

这项技术提供了事件绑定和属性、方法定义,以及一些生命周期相关的事件,应该说已经是一个比较完整的组件化方案了。但是我们可以看到后来的结果,它没有能够进入标准,默默地消失了。用我们今天的角度来看,它可以说是生不逢时。

如何定义一个组件

​ExtJS​​基于面向对象的思想,将组件设计成函数容器,拥有严格的继承关系和组件生命周期钩子。​​HTC​​利用​​IE​​浏览器内置的一种脚本封装机制,将行为从文档结构中分离,通过类似样式或者自定义标识的方式为​​HTML​​页面引入高级的自定义行为(​​behavior​​)。从历史上组件化的尝试来看,我们应该如何来定义一个组件呢?

首先应该清楚组件化的设想为了解决什么问题?不言而喻,组件化最直接的目的就是复用,提高开发效率,作为一个组件应该满足下面几个条件:

  • 封装:组件屏蔽了内部的细节,组件的使用者可以只关心组件的属性、事件和方法。
  • 解耦:组件本身隔离了变化,组件开发者和业务开发者可以根据组件的约定各自独立开发和测试。
  • 复用:组件将会作为一种复用单元,被用在多处。
  • 抽象:组件通过属性和事件、方法等基础设施,提供了一种描述UI的统一模式,降低了使用者学习的心智成本。

接下来我们深入具体的技术细节,看看组件化的基本思路。首先,最基础的语义化标签就能看作成一个个组件,通过​​DOM API​​可以直接挂载到对应的元素上:

var element = document.createElement('div')
document.getElementById('container').appendChild(element)

但是实际上我们的组件不可能这么简单,涵盖场景比较多的组件都比较复杂,工程师就想到将组件定义为原生​JS​中的​Function​或者​Class​容器(其实​Object​也是一种思路,比如​Vue​),因为在​JavaScript​语法中它们天生就是提供了一个闭关的空间,形如:

function MyComponent(){
this.prop1;
this.method1;
……
}

不过,要想挂载又成了难题,普通的​JS​对象没法被用于​appendChild​,所以前端工程师就有了两种思路,第一种是反过来,设计一个​appendTo​方法,让组件把自己挂到​DOM​树上去。

function MyComponent(){
this.root = document.createElement("div");
this.appendTo = function(node){
node.appendChild(this._root)
}
}

第二种比较有意思,是让组件直接返回一个​DOM​元素,把方法和自定义属性挂到这个元素上:

function MyComponent(){
var _root = document.createElement("div");
root.prop1 // = ...
root.method1 = function(){
/*....*/
}
return root;
}

document.getElementById("container").appendChild(new MyComponent());
发表评论

相关文章