漫谈作坊,MVC,MVVM及FLUX(1)

篇首语:这将是一系列文章,按心情更新,从作坊开始,简述在前端开发中遇到的一系列问题,以及为了解决类似问题而衍生出的设计模式,看看这些模式是如何解决我们遇到的难题以及相应的好处和不足。

作坊模式

在一个传统的作坊模式下的应用,大概是这样的。

需求:一个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,还可能遇到如下坑爹的问题:

  • 在引用独立组件时,肆意地取其内部节点状态,导致页面强依赖组件的内部实现,而组件的维护者并不知道哪些页面在使用,一旦更新内部实现,页面挂掉。
    原文作者:ssnau
    原文地址: https://segmentfault.com/a/1190000002510316
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞