这是一个前端框架满天飞的时代,你还没有学会其中的一个,新的又出现了。更糟糕的是,你花大力气所学习到的内容可能已经过时了。虽说学习是一辈子的事,但在软件开发领域,你不能太盲目,你必需要有明辨是非的能力以及快速学习的能力。明辨是非有助于淘汰那些不值得学习的目标,快速学习可以避免学完就过时的尴尬。对于大部分人而言,这些能力是难以在短期内习得的,这需要长期的实践、思考与总结。本人使用过不少框架,也写过框架,有一些这方面的心得体会,现分享出来,希望对于众多的初学者能有些许的启发,尽量少走些弯路。当然,这些只是个人的一些认识,如有不当或者争议之处,欢迎探讨。
这篇文章虽说是讲前端框架的,但其中的许多内容具有普适性,对于后端也是适用的,你可以把其中的观点自然地迁移到你所熟悉的后端编程语言中。为了避免陷于介绍繁琐的基础细节,这里假定读者对于面向对象程序设计已经有了一定的认识。
两个事实
在继续进行下面的内容之前,需要先给出两个事实。后面对于一些问题的处理都可以在这两个事实中找到根据或者动机。
-
我们难以认知或者构建无序的复杂的软件系统
-
我们有办法认知或者构建有序的复杂的软件系统
这里有必要对 无序
与 有序
作一些说明。无序的软件系统是指无章法的,没有依赖成熟的理论方法构建的软件系统。而有序的软件系统则相反,它相身是有明显的结构并且是易于理解的。另外,这里的 复杂
主要指的软件系统规模达到一定的水平。
从这两个事实出发,现在我们可以给出一个理想的框架所能实现的目标:可以通过该框架构建有序的复杂的软件系统,或者说通过该框架构建的复杂的软件系统是易于认知的。
传统网页的构建的问题所在
既然讲的是与前端框架相关的内容,那么我们有必要重新审视下,在无框架使用的条件下,对于传统网站页面,我们是如何构建的。我们一般会这么做,在 html
文件中写上众多的标签和内容。然后,一般情况下还会有一些相关 js
文件与 css
文件。js
文件与 css
文件用于操作或者描述 html
文件中的内容。
上面一段讲述的页面构建方案,现在还有许多人在用,对于简单的页面尚可,但如果所要构建的页面足够的复杂,对于页面的维护会是一个不小的负担。这主要体现在下面两个方面:首先,大量存在的 id
属性值或者类属性值容易导致命名冲突;其次,在添加新功能或者扩展新功能时,将难以下手。
从上面的分析,我们可以看出通过传统方式来构建复杂的页面是行不通的,那么问题出在哪里?如果你将上述页面的整体结构与面向对象程序设计中类的结构做个对比,你会发现,该结构有数据对象(html 标签与内容)、有操作(js 代码与 css 描述)。这显然就是一个 类
嘛!所以问题的关键在于它把所有的逻辑都写在一个 类
里了。所以,解决这个问题的思路是分解并重组该 类
,降低构建的复杂度。这样我们就得到理想框架应该有的第一个功能:提供面向对象编程语言里面类的结构来分解复杂的目标系统。在前端,这种结构一般被叫做组件。
组件化
目前,已经有不少框架就上述问题做了尝试,React 是其中的一个代表,它通过 React.createClass 来创建组件。如下面的示例所示:
var Hello = React.createClass({
render: function() {
return <h1 className='hello'>Hello World!</h1>;
}
});
React 的组件化方案并不完美,这主要体现在对样式的处理上,它并没解决全局样式的冲突问题。大部分的人仍然通过命名约定或者使用 less/sass
来实现曲线救国。虽然有一些 CSS in JS
的第三方方案,但解决起来总觉得非常别扭。不过 CSS in JS
的方案却为我们对问题的解决打开了一道窗。下面是 xmlplus 的解决方案,堪称完美。
Hello: {
css: `#hello { color: blue; }`,
xml: `<h1 id='hello'>Hello World</h1>`,
fun: function(sys, items, opts) {
sys.hello.css("font-size", "28px");
}
}
理解该示例的关键在于视图项中 id
标识符。它是局部的,仅在组件内部可访问,在函数项中对其操作与在样式项中对其描述,本质上没什么差别,最终都是对目标对象施加影响。这种处理方式是优点远不止对上述问题的解决,更多内容可以访问文档的相关章节。这一节请注意 局部化
思想的应用,后面内容将反复使用它来解决相关的问题。
组件之间的通信
回到页面的设计,当我们对一个页面进行组件化分割后,原来页面内可以直接通信的对象有可能被割裂开来。一个理想的前端框架应该能够提供修复这种通信关系的能力,这就是组件之间的通信。
按不同组件的关系划分,组件之间的通信可以分为父子组件和任意组件之间的通信。其中父子组件之间主要通过事件的传递和直接可见的接口来通信,这一方面很多主流框架都会提供,所以这里将集中注意力讨论的任意组件之间的通信机制。相对于任意组件之间的通信,比较遗憾的是目前主流的框架都在努力实现数据的绑定来避免操作 DOM 就能更新视图之类非必要的功能。
一个示例
首先,明确一点,一个应用就是一棵组件树。为方便起见,下面以 xmlplus 中的例子来说明,当然,你也可以联系到 React 或者 vue 中的组件树。
Example: {
xml: `<div id='index'>
<Hello id='foo'/>
<World id='bar'/>
</div>`,
fun: function(sys, items, opts) {
console.log("communication test");
}
}
此示例通过两个组件节点 Hello 与 World 之间的通信来演示任意组件之间的通信。上述 Hello 组件与 World 组件的内容如下:
Hello: {
xml: `<button id='hello'>Hello</button>`,
fun: function(sys, items, opts) {
this.on("click", e => this.notify("hello", "msg"));
}
},
World: {
xml: `<h1 id='world'>World</h1>`,
fun: function(sys, items, opts) {
this.watch("hello", (e, msg) => console.log(msg));
}
}
观察此组件 Index,我们可以分解出如下的组件树:
Example/
└── div[index]
├── Hello[foo]
│ └── button[hello]
└── World[bar]
└── h1[world]
通信的局部化
如果只实例化一个 Example 组件上述示例能很好的工作,但如果实例化多个 Example 组件就会出问题了。我们来看看具体的示例:
Index: {
xml: `<div id='index'>
<Example id='foo'/>
<Example id='bar'/>
</div>`,
fun: function(sys, items, opts) {
console.log("two Examples");
}
}
该示例中,点击其中一个 Hello 按钮,将导致所有消息侦听器接收到消息,这显然不是我们想要的。所以我们需要对消息进行局部化。请看下面改进后的 Example 示例,改示例在映射项中配置了一个值为 true
的 msgscope
选项,这将导致当前组件及其子组件消息通信的局部化。局部化的区域可以参考上面给出的组件树视图。
Example: {
xml: `<div id='index'>
<Hello id='foo'/>
<World id='bar'/>
</div>`,
map: { msgscope: true },
fun: function(sys, items, opts) {
console.log("communication test");
}
}
总结
通信的局部化带来两个好处,一个是避免消息污染,另一个是当一个应用足够复杂时,全局的通信将不可避免地陷入混乱。通信的局部化的想法与组件思想如出一辙,都是为了应对构建复杂系统服务的。
目前主流的框架并不提供组件之间的通信功能,同时第三方的通信软件包一般也不会提供通信的局部化功能。为了应对构建复杂的系统,你可以在消息的命名上作文章,尽管麻烦一点。
本文讲了一个理想框架应该包含两个最基本的功能,一个是优秀的组件化能力,另一个局部化的通信功能。前者在很多面向对象编程语言中都会包含,后者一般只能实现全局的通信功能,但读者应该有局部化的通信意识。
下一章我们将讨论命名空间、组件对象共享以及延迟实例化等锦上添花的框架应该提供的功能,敬请期待。