自浏览器问世以来,前端的开发就一直是一个困难的问题。 一部分原因在于早些年的JS被设计为类似Lua一样的精简脚本语言,另一部分原因在于Web是一个极其开放的平台。 几年前阻挡前端发展的主要问题是没有模块化的方案,也没有组件化的方案。 近年来模块化问题被RequireJS,CommonJS,Webpack解决了。 组件化问题也有了诸多方案,例如ReactJS,AngularJS,VueJS。 看似已经全面迈入现代化? 实则不然,组件化之后的前端工程仍然面临着许多挑战,这些挑战也催生了很多新的方案。

本文以探讨为主,将讲解从Jquery时代到React/Redux时代的组件化的演变和挑战。

组件是什么?

从一开始就开始叨叨叨组件,组件是什么呢? 组件可以看做带有一定逻辑执行能力,可嵌套,代码可以复用的用户界面单元,比如一个按钮,一个菜单。下面有几个例子:

原生

原生的<button> Submit </button>就可以看做简单的组件,代码可复用,可以嵌套,也是UI单元,然而此类组件的问题在于没有逻辑执行能力,并且通常都非常的简单。 因此原生的组件就不包含菜单,日历之类的复杂组件。

jQuery

jQuery时代里,一段一段的HTML代码被直接运用为组件,也就是说,需要运用组件的时候,用jQuery强行把一段HTML代码插入相应DOM,就可以了。 这种方案简单易行,也可以制作出复杂组件。 然而,这个方法不能允许嵌套组件,因为组件是以HTML的形式存储的,HTML里只能包含上文提及的初级简单组件。 (除此之外,逻辑代码也很难和HTML绑定在一块,当然这个要求是可以简单克服的。) 总之这样一来,灵活度就大大降低了,比如想实现这样一个需求:

两个菜单A和菜单B,A的菜单里有一个动画天气图标,B的菜单里有一篇文章。

用jQuery的方案,就只好写两套HTML了,然而这两套HTML会有很大的相似性,浪费了工作量。

现代方案 React/Vue/Angular

为了克服jQuery方案的不便,React之类的工具就逐渐占据了主流,这类方案可以实现带有逻辑执行能力,可复用,可嵌套的组件单元。 简单来说,这些方案都用特殊的方案来处理了模板(HTML),而不是简单的操纵DOM,这样就可以让模板超越HTML的局限,变得复杂起来。那么这类方案的问题是什么呢?

挑战:状态流

上文提及Web是很开放的平台,按照我的理解,是由于事件多样导致的。 一个普通的Web应用可以接收来自许多方面的事件:

  1. 用户输入,点击按钮,输入文字
  2. URL和Cookies
  3. 时间(时钟类应用)
  4. 网络请求 (比如通知消息)

这些事件都会对组件产生影响,有的事件影响的组件多,有的事件相对局部, 有的事件是由组件产生的,有的事件是由外部因素导致的。

根据上文组件的定义,我们可以画出一个普通的组件化项目的图如下。

Vanilla React

这个图中,实线箭头表示从属关系,带颜色的箭头表示状态、事件流。 可以看到事件来源多样,影响分布不同,不同状态被存在不同组件里,比如登录状态层层传递至叶子层。天气状态储存于Menu,仅仅影响一个子组件。 这种情况下应用随着组件数量的上升维护成本会随之急剧上升。

考虑随便添加一个组件,如果菜单下面还有一个登出(Logout)的组件,那这个叶子层的组件就会释放出一个影响组件根(甚至影响Cookies)的登出事件,该事件的传播范围很广,很难控制。

另一方面,此方案的测试也相对困难,因为组件包含的业务逻辑往往细碎分散,很难组织起有效的单元测试。

状态处理中心

有了问题就有了方案,很多人想,事件如此复杂,干脆给他来个标准吧。 然后就有了Flux,Redux,VueX之类的工具,这些工具都主张使用一个中心的信息处理机制来接收,分发所有的事件。 可以下面这一张图片来理解。

Improved Components

图中红线代表事件,黑线与虚线代表状态,虚线是可选的,取决于使用者的看法。 可以看到,事件中心接收所有的事件,然后把生成的状态分发给许多组件。 可以看到这种方法有几个优势:

  1. 组件可以专注于界面,减少逻辑,代码变得简洁,更复用。
  2. 状态处理,例如网络操作之类的,被状态处理中心标准化,代码变得更有组织。
  3. 更容易测试,因为逻辑单元被抽离出了组件
  4. 若是忽略虚线,状态就不需要在组件之间传递了。(优点?)

当然相应的也有劣势:

  1. 标准总是对应着一定的负担,写一个组件要写一系列的boilerplates。
  2. 不同的实现各有优劣,尤其是在状态分发上,有许多争议。
  3. 最佳实践也存在争议。

这第三点,也是我个人认为这个方案不理想的最主要原因。 可以看见,如果图中多出了虚线,状态流会变得显著复杂了起来。 然而这是一个难以定夺的决定,一些局部的状态是否该被放在存储中心呢? 例如,鼠标飘过了一个按钮,按钮变成别的文字,这个事件明显非常独立,放置于中心不合理。 但又例如,一个组件以及它的子组件们专门管理所有的文章,其他的组件都不会对该状态有涉及,这种时候要不要把文章状态放置在中心呢? 原本很严格的树形结构若是强行被打散,到底是好还是不好呢? 这个答案怕是只有团队讨论才能有结论了。

除此之外,如果状态处理中心被严格(过度严格)执行了,那实际上这和jQuery方案的结果相似之处能达到80%,到了这个份上,怕是有点本末倒置。

反思

在微博上总是有人争吵前端的这些新发明是否有益,其实争论是有道理的。 因为当前的方案(至少在组件化上)的确不尽如人意。

虽然无法轻易决断哪中方案的优劣,不过可以确定的一点是如果放弃了前端的开放灵活性而追求所谓的“正确”,那肯定是不对的。