篇首语:这将是一系列文章,按心情更新,从作坊开始,简述在前端开发中遇到的一系列问题,以及为了解决类似问题而衍生出的设计模式,看看这些模式是如何解决我们遇到的难题以及相应的好处和不足。
作坊模式
在一个传统的作坊模式下的应用,大概是这样的。
需求:一个TodoList。
- 可以增加和删除条目
Stage 1
代码大致是如下,具体结果可以参照我创建的JsFiddle http://jsfiddle.net/xykhzwq8/
var ndContent = $("#content");
var ndList = $("#todoList");
// 增加一个item
$("#add").click(function(){
var content = ndContent.val();
$(['<li class="item">',
'<span class="item-content">',
content,
'</span>',
'<button class="del">x</button>',
'</li>'
].join('')
).appendTo(ndList);
});
// 删除一个item
ndList.on('click', '.del', function(){
$(this).parents('.item').remove();
});
这是极简单的,而且也看不到作坊模式的不好的地方。相反,它看上去很易懂,很直接,我们都很喜欢。
但是由于它实在太简单,满足不了用户的需求,用户希望可以:
- 勾选已完成的和未完成的
- 显示已完成的数量和未完成的数量
于是我们开始修改Dom结构,大致如下:
<li class="item">
<input type="checkbox" class="check"/>
<span> {content} </span>
<button class="del">x</button>
</li>
以及一个显示总数量,已完成数量, 未完成数量的统计框。
<div id="stat">
<span>已完成: <i class="finished">{finished}</i></span>
<span>未完成: <i class="unfinished">{unfinished}</i></span>
<span>总量: <i class="total">{total}</i></span>
</div>
新增了一个checkbox
用于让用户勾选,同时JS代码扩展如下:
http://jsfiddle.net/b7s2z2jw/2/
ndList.on("click", ".check", function(){
updateStat();
});
function updateStat(){
var finished = 0;
var totalCount = 0;
var unfinished = 0
ndList.find('.check').each(function(index, checkbox) {
totalCount++;
if (checkbox.checked) finished++;
});
unfinished = totalCount - finished;
$(".finished").html(finished);
$(".unfinished").html(unfinished);
$(".total").html(totalCount);
}
依然非常容易实现。但注意了,我们增加了一个updateStat
的方法,并且需要在四个地方显调用它:
- 点击勾选时(正如上面代码所示)
- 应用启动时
- 点击增加按钮时
- 点击删除按钮时
除了点击勾选
是新增的需求外,其余三项均是原有的代码和逻辑。这里展示了作坊模式的一个问题是:
当有新业务(本例是统计数量)进入时,需要修改已有的业务(启动,增加,删除)的代码
以上是我们遇到的第一个问题。注意,在这个例子中或许还看不出太大的不便,当一个应用有着更多的交互和数据时:
- 修改任何已有的代码都是危险的。
- 容易忘记或忽略在相应的场景增加新来业务的代码。
- 业务之间存在强耦合,将导致难以维护。
Stage 2
许多用户希望:
- 每一个item都可以弹出窗口编辑
于是我们遇到了不同UI块间交互的问题,一个简陋的解决方式是这样的:
首先升级一下item节点的内容,增加一个edit
的按钮:
<li class="item">
<input type="checkbox" class="check"/>
<span class="item-content"> tv </span>
<button class="del">x</button>
<button class="edit">edit</button>
</li>
接下来处理交互,代码大致为:
http://jsfiddle.net/dht27Lqe/2/
ndList.on('click', '.edit', function(){
var item = $(this).parents('.item').find('.item-content');
var content = item.html();
ndMain.hide();
$("#edit-dialog .content").val(content);
$("#edit-dialog").show();
$("#edit-dialog .ok").one('click', function(){
var content = $("#edit-dialog .content").val();
item.html(content);
$("#edit-dialog").hide();
ndMain.show();
});
});
$("#edit-dialog .cancel").click(function(){
ndMain.show();
$("#edit-dialog").hide();
});
上述代码有点丑陋,但是能工作,即便是在作坊模式下,我们也可以将edit-dialog
封装代组件以减少对其的dom操作,如:
ndList.on('click', '.edit', function(){
var item = $(this).parents('.item').find('.item-content');
var content = item.html();
$dialog.open({
content: content,
onOk: function (content) {
item.html(content);
}
});
});
但是在上面两个代码片段中,我们都能看到一段无比丑陋的代码:
var item = $(this).parents('.item').find('.item-content');
它的作用是,根据当前点击的edit
按钮,获取其对应item
的内容。这段代码强耦合了HTML的片段,其带来的灾难是另每个前端在开发业务时都极为头疼:
当UI变化时,如PM希望改变item的外观,很多时候会无可避免地改变一个item
的HTML,导致你:
- 不敢轻易修改一个标签的class
- 不敢轻易的调整内部结构
- 一旦有新的需求改变外观,将产生极大的调试包袱,实际工程中,你甚至不知道你的改动会产生bug
这便是我们遇到的第二个问题:
业务逻辑与HTML结构强耦合,修改其中之一,必须小心翼翼地调试看上去与其完全不相干的另一个。(很多情况下你甚至找不到另一个在哪…
分离展示和交互似乎成了笑话,因为它带来更大的不确定性和不稳定的依赖耦合。
Stage 3
公司业务增长了,TodoList将全面升级,针对不同用户推出了如下不同的功能:
- 高级用户可以点击
全选
按钮,将所有事务设置为已读
对于全选按钮,其HTML为:
<input type='checkbox' class='checkall' />
这段HTML仅在用户是高级用户时,才会从服务器渲染下来。于是在JS中需要:
- 判断这个节点是否存在
- 处理全选时产生的操作
代码如下:
var ndCheckAll = $('.checkall');
// 如果这个节点存在
if (ndCheckAll[0]) {
ndCheckAll.click(function(){
// 将所有的checkbox设为true
ndList.find('item .check').attr("checked",true);
// 更新底部的统计状态
updateStat();
});
}
// 修改点击每个item的checkbox的代码
ndList.on('.check', 'click', function(){
// 保留原来的代码
.....
// 新增的代码
// 1. 如果点击后,列表状态不是全选,那么将.checkedAll设为false
// 2. 如果点击后,列表状态为全选,那么将.checkedAll设为true
// 3. 还需要判断.checkedAll这个节点是否存在
});
上面的代码存在的问题是:
- 新增业务在完成本职任务外,还需要调用
updateStat
,它本不应该关注这个的。 - 需要修改已有代码来适配新增的业务逻辑。
- 页面通过判断节点的存在来选择性执行某段代码,这是灾难性的。
>如果有n个用户状态的话,那么产生的页面分支组合就是n*(n-1)/2种,其相互依赖耦合几乎是不可维护的。
> 设想在本例中,如果底部状态栏也是由页面根据用户等级来判断是否显示的时候,那么在全选
业务代码中调用updateStat
是什么意思?每新增一个需求,都要对已有的情况有完全掌控,这负担是极重的。
这种情况存在很普遍,以电商网站下单为例,一个订单根据其不同属性在下单时可能需要展示:
- 物流信息
- 积分选择
- 优惠券选择
- 满xxx减yy
- 验证手机号
- ……
这些都将产生不同形态的组合和依赖,改变任何一段的信息,都将可能改变其它地方的展示。
阶段性总结
作坊模式存在的问题:
- 没有数据模型,一切操作都在对DOM节点的读取,不能轻易修改DOM节点结构和属性
- 没有职责分离,每一个操作都需要自己负责更新潜在影响的地方,给可维护性带来了问题
- 新增业务需要修改已有代码
- 新增业务需要插入与其间接相关的已有代码
- 每一个区块盘根错节的依赖,导致代码无法进行测试
在实际的作坊工程中,由于人们习惯于操作DOM,还可能遇到如下坑爹的问题:
- 在引用独立组件时,肆意地取其内部节点状态,导致页面强依赖组件的内部实现,而组件的维护者并不知道哪些页面在使用,一旦更新内部实现,页面挂掉。