AngularJS components

背景


假设某种Web App的业务场景是这样的:每个页面仅仅只有简单的表单,包括文字提醒label, 单选radio button, 问题列表dropdown menu, 处理页面前后跳转的button, 复杂一点的如价格输入input,时间选择date picker。而且这些可以独立展现业务价值,也就是说任何一个或者多个这样的页面元素可以经过简单组合,甚至无需组合就能反映一个独立的业务提问(具备评估填写人能力的价值),那么我们就可以将这些页面元素及其简单的组合体作为一个独立的component。这些component最大的好处就是恰如其分地反映了它本身的业务,但是这也不能成为封装成component的理由——因为我们完全可以通过一个良好名称的id来反映。独立出来的好处无非两个:可插拔,能复用。

首先,来讨论一下可插拔这个问题。假如BA美女告诉说某个业务提问T不应该再出现在A页面了,它应该出现在B页面。若不是component,问题来了:我们需要删掉本页面的T,连带着需要删除分散的js和css文件。麻烦!若是独立一个component,简单了先解除该页面对T的使用(在AngularJS中是删除对应的指令),然后在B页面导入进来。因为是component,且每个页面的结构都是表单,代码结构也类似,所以改动的量少。方便!

再来讨论一下能复用这个问题。我刚才提到过,因为是反映一个独立的业务提问,所以很可能在其他同类的产品上,这东西也是存在的,可能唯一不同的就是问法不同,以及提示信息有别。换句话说这些问句和提示是可编辑的。有人说那简单呐,component的所有提问和提示信息弄成可编辑的不就行了?component是可变的(可编辑),但是具体在哪个页面给出什么提问和提示总是不变的吧。我原来也是这样想的,但是仔细想想我发现这里有一个矛盾之处:我们想复用业务,而不是组件!这世界上最不可信的就是业务,最可能变的也是业务,但我们还异想天开地复用业务。任何一个独立的component,在具体的业务上,都很可能不是简单地替换显示文本,它完全会增加一小条额外的询问条目!那么原来的component就只能乖乖地被copy/paste成另一个可怜的component——因为它们是不同的业务啊!BA如是说。作为程序员,我讨厌重复的代码,这也是我们不断使用重构的原因。但是就因为业务不同,哪怕代码相似度是99%,我也得重新创建一个,这个实在不值得!

而且,更奇葩地是这些可编辑信息居然被写入了后端的properties文件当中,使用Java这样静态的语言来抽象出一个给这些component提供文本信息的接口。试想一下,哪天某个BA告诉我:我想为某个业务X,添加一个subTitle。我的回答:不行呐,我们的接口只提供了一个label,而且这里已经使用了,我如果加一个新的,会影响其他的所有component的。BA:!&^$%@$. 我接着说:我可以提供一个sub.label的property key,这样前端可以写成 [X.sub].label,如何?BA:呵呵……

探索


简单得出几个结论

  • 不用妄图复用业务,即使它们在UI表现上极其相似;
  • 业务和UI的关系,等同于饮料和瓶子的关系,瓶子一个模子就够了,饮料随便换;
  • 饮料要换个瓶模子了,那换就是,具体是从原来的模子继承,还是组合,饮料不需要关心。

如何做瓶子,如何换饮料,如何组合成卖品

做瓶子简单。作为程序员,我使用AngularJS的Directive构建基础的common components,如radio button, dropdown menu, search box. 这些仅仅和UI相关。当然这些还仅仅是瓶子的组成成分。

那么接下来,如何换饮料呢?饮料有自己的骄傲——它不仅口味各异,还分品牌!每种品牌下的饮料自然不同。那我们只好先做一下分类。先从品牌开始,先分成X, Y, Z这几类,然后开始分口味了,又分成A, B, C这几类。这个季度,老板说我们要推出X类的A, B, C饮料!怎么做?我们把A, B, C写入配置文件(properties),拿A举例子:A有颜色,甜度,气味三个属性,分别对应如下:

1
2
3
4
A:
color: red
sweet: middle
smell: nice

Ok,接下来我们来构造A这款饮料的业务的directive

1
2
3
4
5
6
7
8
9
10
(function(global, angular){
'use strict';
angular.module('a.b.c',[])
.directive('A', function(){
return {
restrict: 'AE',
templateUrl: 'views/A.html'
};
});
})(this, angular);

好了,我们有业务细节A了,又有业务A的directive了。我们理所应当地认为这个directive和A应该是一体的,为什么不直接将A细节写入directive的scope中呢?答案是X, Y, Z在这里是可以复用这个directive的,所以不能硬编码。不能硬编码,我首先的直觉就是变量替换,又要求复用,那么就立马想到了继承或者组合。又因为组合优于继承(更现实的是我不知道如何在AngularJS中实现继承),我决定使用组合。接下来这个过程的产出物就是我们的卖品,构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# define in common module
A:{
color: red,
sweet: middle,
smell: nice
}
##

(function(global, angular){
'use strict';
angular.module('a.b.c', ['common'])
.directive('A', ['me', function(){
return {
restrict: 'AE',
scope: {
self: '='
},
link: function($scope) {
$scope.self = me;
},
templateUrl: 'views/A.html'
};
}]);
})(this, angular);

从代码中很容易看出,我们把A这个业务,组成了一个叫做common的module,可以类比为装着众多饮料的机器,然后对准一个叫做A的瓶子注入,注意A这个瓶子用到了很多common components。这整个过程就是Angular完成这个A的directive的过程,也就是饮料和瓶子组装的过程。然后这个A的directive就是我们的卖品。

实际成效


事实验证,上述的方式是不合理的。理由有下面三点:

  • 依赖过剩。我们将会有很多的components,如果每次都得手动注入common依赖的话,量实在是太大了。
  • 非独立。一个纯净的业务组件不应该有第三方的依赖,有依赖,必然导致无法独立使用。
  • 不可扩展设备。这个最大的怪兽就是我们将业务的东西全部放到了前端,如果要兼容其他设备,我们后端的这块业务功能是缺失的,不能给其他设备提供支持。