【blade的UI设计】理解前端MVC与分层思想

  1. 云栖社区>
  2. 博客>
  3. 正文

【blade的UI设计】理解前端MVC与分层思想

范大脚脚 2017-11-10 10:15:00 浏览1041
展开阅读全文
前言

最近校招要来了,很多大三的同学一定按捺不住心中的焦躁,其中有期待也有彷徨,或许更多的是些许担忧,最近在开始疯狂的复习了吧

这里小钗有几点建议给各位:

① 不要看得太重,关心则乱,太紧张反而表现不好

② 好的选择比坚持更重要

这点小钗便深有体会了,因为当年我是搞.net的,凭着这项技能想进bat简直就是妄想,于是当时我就非常机智的转了前端,另一个同学也非常机智的转了安卓

所以各位想进大公司,还需要提前关注各个公司最大的缺口是什么,找不准缺口基本无望进大公司的

③ 积累最重要/没有积累现在就专精

想进大公司除了运气,最重要的还是平时积累,若是大三之前没有5W行代码量的同学便需要专精一门,不要东搞西搞,最后什么都不会,那就完了

④ 不要扯设计模式

这里虽然有点偏激,但是就我的观察,我原来的leader、我身边的同事,我们公司的架构师,真的没有几个完全理解了设计模式,我们很多架构师甚至连个依赖图,时序图都搞不出来

所以各位面试的同学除非真的对设计模式有深入了解,或者代码量达到了10-20w行,除此之外不要在面试扯设计模式,肯定会中招的

PS:尼玛,这里差点忘了加之前,万一被leader看见了就完了!

最后,进得了大公司的同学一般之前大学时光没有怎么浪费,或者说天资本来就好,进不了的同学便先积累吧,搞不好哪天就中彩票不用工作了

好了,我们进入今天的正题,这是与blade框架有关的第三篇博客,第二篇是关于webapp seo难题的解决方案

由于该方案本身具有一定难度还在实现之中,所以这里就先出第三篇了......

PS:此文只是个人粗浅的认识,有误请指正

前端的发展

我原来常与leader讨论,javascript之前不适合做大规模应用的一个主要原因是其无法分模块,这个就带来了多人维护一个文件的难题

程序员往往都是2B,我们可能不会觉得自己的代码写得多好,但是一般会觉得别人的代码写得很烂!

于是你会看见一个有趣的现象便是两个有一定差距的程序员维护了一个js文件后,可能以后他们都不能愉快的玩耍了

让2个程序员维护一个文件都非常困难了,何况是多个?所以分模块、分文件是javascript或者说是前端一个极大的进步

他让前端合作变成了可能,由此才出现了几万甚至几十万代码量的前端应用,这里requireJS相关的模块库功不可没

这里我们先来回顾下之前我们是怎么写代码的

混杂编程

最初,我们的应用一般都不复杂,我们会这样写前端代码:

复制代码
<body>
  <div>
    <span onclick="test()">测试</span>
  </div>
  <script type="text/javascript">
    function test() {
      alert('do something');
    }
  </script>
</body>
复制代码
慢慢的我们感觉好像不对,因为B同事好像也有个test方法,由于B同事比我们早来几年我们也不好喷他,于是只好寻求解决方案,于是出现了命名空间

复制代码
<div>
  <span onclick="yexiaochai.test()">测试</span>
</div>
<script type="text/javascript">
  var yexiaochai = {
    test: function () {
      alert('do something');
    }
  };
</script>
复制代码
这里解决了B同事冲刷我代码的问题后,突然B同事便离职了,B同事的代码交给了我们,于是我们非常看不惯其以B开头的命名空间!

此类情况也出现与了最初的ASP、JSP甚至现今的PHP,他们有一些共同的特点便是:

① web中的asp代码与javascript代码交替,代码杂乱

② 界面设计与程序设计混杂,维护升级困难

一个经典的例子便是著名论坛框架discuz的源码(他js其实控制的比较好),看过其源码的同学想必对其印象极其深刻

动则一个文件成千上万,内中html php代码混杂,整个维护阅读成本非常之高

这个时候由于社会发展,越来越多的复杂需求层出不穷,为了满足社会的需要,各大框架分分求变,于是有了一些变化

UI分离

这一次的变化我认为集中体现在UI分离这块,里面做的最成功的当然是ASP.net,javascript有与之对应的struts2

这一次的革新两大巨头不在将逻辑操作以及数据库操作写在与HTML有关的文件中了,而是写在与之对应的后台文件中,这里最优雅的是.net的codebehind方案

这次的变化倒不是说程序本身发生了什么变化,事实上程序本身并没有发生变化,以小钗熟悉的.net为例

我们一次新建页面会具有两个文件:

① index.apsx

② index.aspx.cs

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="_00综合_11mvc_index" %>
复制代码
public partial class _00综合_10doc_write_index : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
}
复制代码
值得关注的是两个文件的这些代码,这里的内部实现我们不去深究,但是小钗可以告诉你,最终的编译会将这两个文件和到一起,以上的代码映射便是他们合并的凭据

因为他们最终会和到一起,所以index.apsx与其index.apsx.cs才会具有方法数据通信的能力,跨页面就不行了

UI分离带来的第二个好处便是经典的.net三层架构以及经典的Java SSH框架,在UI分离之前,这类分离是不好实现的

于是.net与java的代码可以基于此一再细分,各个领域的程序员专注点与擅长点完全分开了,所以很多复杂的软件出现了

前端模块化

最初.net的做法也带来了很多质疑,因为懂行的都会知道,这样的代码事实上效率低了,理解差了;这里便和现今javascript的一些特征惊人的一致

我们的代码存在着这样一种递进的关系:

① 最简单的代码

<span onclick="test()">测试</span>
② 考虑方法重名问题后的代码

 <span onclick="yexiaochai.test()">测试</span>
③ 考虑一个标签具有多个事件的实现

复制代码
<body>
<div>
  <span id="test">测试</span>
</div>
<script type="text/javascript">
  var yexiaochai = {
    test: function () {
      alert('do something');
    }
  };
  document.getElementById('test').addEventListener('click', yexiaochai.test);
</script>
</body>
复制代码
以上三种递进关系其实诠释了一客观因素:

业务需求变得复杂、业务复杂后处理业务的方法便多了,所以方法会重复,也是因为逻辑的复杂度导致一个标签可能具有多个事件

所以,现阶段前端代码复杂度的提升的根本原因便是原来的写法不满足需求了,我们需要这样写

以上三种递进关系只是一个开始,由于业务逻辑的膨胀,javascript代码爆炸性的膨胀起来了,于是我们做了一下事情:

① 将javascript文件放到一个文件里面

② 一个文件太多不好维护了,于是分成多个文件

③ 文件太多了,不好管理,于是开始适应模块化管理工具

以上是前端分模块的主要原因

前端MVC

前端做久了我们会发现,很多时候我们还是在操作html,无论是js事件操作,或者css操作,或者数据来了需要改变dom结构,总而言之我们总是在操作我们的UI

这个时候我们也会遇到一种现象是,这里需要一个信息提示框,那里也需要一个信息提示框,好像长得差不多

这样相似的需求积累,我们又不傻,当然会寻求统一的解决方案,于是便出现了UI组件,UI组件的出现是前端一大进步

这样前端便不只是写点小特效,小验证的码农了,摇身一变成为了高大上的交互设计师,而且还有很多妹子,工作环境好得不得了,想来的同学请给位私信,发简历,工资高,妹子多!!!

我们刚开始可能是这样做UI的:

复制代码
function showAlert(msg) {
  //构架复杂dom结构,略
  var el = $('<div class="msg">' + msg + '</div>')
  //绑定复杂的事件,略
  el.click(function () { });
  $('body').append(el);
}
showAlert('史上最强孙组长!');
复制代码
这里面涉及到几个元素:

① 数据 => msg

② 界面 => el

③ 事件 => click

PS:此段虚构

逐渐的,我们发现这样写很不方便,原因是UED新来一个叫左盟主的抽风给我说什么语义化,要动我的dom结构!!!

尼玛,他当我傻啊!HTML有什么语义化可言嘛!我这边据理力争,因为他不懂嘛,他对不懂的事物便比较恐惧,所以我要把他说明白

但是史上最强、宇宙无敌孙组长带来了CSS天使小静mm给我语重心长的聊了一下小时代与后会无期的故事与渊源,最后还唱起了女儿情

于是我深深的理解了左盟主是正确的,前端确实应该考虑语义化,于是我决定修改我的DOM结构!!!

真正的修改下来,发现这里工作量不小:

首先是原来代码过程重来没有考虑过dom或者classname会变,这里一变的直接影响便是我那些可怜的事件绑定全部完蛋

于是我这里便考虑是不是应该将,表现与行为分离

这里的UI操作不应该影响我具体事件逻辑,而且UI样式的变化是家常便饭,就算UED不便,不同的业务场景总会要求一点不一样的东西,这里便引入了伟大的模板引擎

模板引擎

前端模板引擎的出现具有划时代的意义,他是实现表现与行为分离的基石,这里再配以zepto的事件机制,最后的结论就是左盟主的需求很简单

这里以blade的一个组件代码为例:



我们看到这里的alert组件的dom结构就是一个简单的html片段,于是我们要改某个dom结构便直接修改便是,不需要修改我们的js文件

当然这里的前提是这里的className映射不能丢,这个映射丢了,事件绑定一块仍然需要修改



到这里,我们是时候进入今天MVC的深入发掘了

理解MVC

正如.net引入Code Behind为了解决UI与业务分离一样,前端javascript也采用了MVC的方式分离UI与业务,前端MVC出现的主要原因其实便是:

职责分离!

其实小钗之前一直对MVC不太了解,不知道应该从哪个方案来了解MVC,以structs2为例,我好像就只看到几个配置向并没有看到控制器就完了

再以Backbone为例,其View的实现以及Model的实现基本处于分离状态,完全可以单独使用,Router一层看似扮演着控制器的角色但是我这里依旧觉得他仅仅是路由

再拿小钗最近写的Blade框架的app模块,他倒是有点像全局控制器了,控制着各个View的创建、销毁、通信,但是这里的Model好像由不在了

PS:传统的MVC的控制器是负责转发请求处理请求,生成View的,这点可以参考Blade的app

所以要想理解MVC还真不是一件容易的事情,当有人问起什么是MVC时往往我们都是一图打发:



事实上这个模型,在前端来说很少出现,他经常不上缺了一个Controller就是缺了一个Model,而每次问起级别较高的同事也只是将上图简单描述一下即只

好像MVC变成了一个玄之又玄的东西,处处透露着只可意会不可言传的神秘

PS:所以面试的同学小心了,一般问到什么设计模式或者MVC要么这个面试官很牛,要么狠喜欢装B,两者对你都不利

所谓MVC便是:

① View就只处理View的事情,其它神马都不要管

② 数据由Model处理,并且为View提供渲染需要的数据

③ 由于后端可能抽风可能将name变成Name坑前端所以会衍生出一套viewModel的东西作为Model与View的映射

④ 业务代码集中与viewController,提供事件让用户与View交互,入口点为View

所以一般逻辑是,Controller加载,初始化状态Model获得数据生成ViewModel,View生成,用户操作View触发事件由ViewController处理引起数据更新,然后Model通知view做更新

这里我认为实现最为优雅的是我与原leader的一个开源项目:

https://github.com/leewind/dalmatians



我觉得这个代码便很好的说明了MVC这个思想,各个模块只是关心了自己的职责,举个例子来说:

 View Code
 View Code
复制代码
  <script type="text/underscore-template" id="template-ajax-init">
      <div class="cui-alert" >
        <div class="cui-pop-box">
          <div class="cui-hd">
            <%=title%>
          </div>
          <div class="cui-bd">
            <div class="cui-error-tips">
            </div>
            <div class="cui-roller-btns" style="padding: 4px; "><input type="text" placeholder="设置最低价 {day: '', price: ''}" style="margin: 2px; width: 100%; " id="ajax_data" class="txt" value="{day: , price: }"></div>
            <div class="cui-roller-btns">
              <div class="cui-flexbd cui-btns-sure"><%=confirm%></div>
            </div>
          </div>
        </div>
      </div>
  </script>
  <script type="text/underscore-template" id="template-ajax-suc">
    <ul>
      <li>最低价:本月<%=ajaxData.day %>号,价格:<%=ajaxData.price %> 元</li>
  </ul>
  </script>

  <script type="text/underscore-template" id="template-ajax-loading">
    <span>loading....</span>
  </script>
复制代码
复制代码
//模拟Ajax请求
function getAjaxData(callback, data) {
  setTimeout(function () {
    if (!data) {
      data = {day: 3, price: 20};
    }
    callback(data);
  }, 1000);
}

var AjaxView = _.inherit(Dalmatian.View, {
  _initialize: function ($super) {
    //设置默认属性
    $super();

    this.templateSet = {
      init: $('#template-ajax-init').html(),
      loading: $('#template-ajax-loading').html(),
      ajaxSuc: $('#template-ajax-suc').html()
    };

  }
});

var AjaxAdapter = _.inherit(Dalmatian.Adapter, {
  _initialize: function ($super) {
    $super();
    this.datamodel = {
      title: '标题',
      confirm: '刷新数据'
    };
    this.datamodel.ajaxData = {};
  },

  format: function (datamodel) {
    //处理datamodel生成viewModel的逻辑
    return datamodel;
  },

  ajaxLoading: function () {
    this.notifyDataChanged();
  },

  ajaxSuc: function (data) {
    this.datamodel.ajaxData = data;
    this.notifyDataChanged();
  }
});

var AjaxViewController = _.inherit(Dalmatian.ViewController, {
  _initialize: function ($super) {
    $super();
    //设置基本的属性
    this.view = new AjaxView();
    this.adapter = new AjaxAdapter();
    this.viewstatus = 'init';
    this.container = '#container';
  },

  //处理datamodel变化引起的dom改变
  render: function (data) {
    //这里用户明确知道自己有没有viewdata
    var viewdata = this.adapter.getViewModel();
    var wrapperSet = {
      loading: '.cui-error-tips',
      ajaxSuc: '.cui-error-tips'
    };
    //view具有唯一包裹器
    var root = this.view.root;
    var selector = wrapperSet[this.viewstatus];

    if (selector) {
      root = root.find(selector);
    }

    this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel());

    root.html(this.view.html);

  },

  //显示后Ajax请求数据
  onViewAfterShow: function () {
    this._handleAjax();
  },

  _handleAjax: function (data) {
    this.setViewStatus('loading');
    this.adapter.ajaxLoading();
    getAjaxData($.proxy(function (data) {
      this.setViewStatus('ajaxSuc');
      this.adapter.ajaxSuc(data);
    }, this), data);
  },

  events: {
    'click .cui-btns-sure': function () {
      var data = this.$el.find('#ajax_data').val();
      data = eval('(' + data + ')');
      this._handleAjax(data);
    }
  }
});

var a = new AjaxViewController();
a.show();
复制代码
程序的执行流程由控制器发起,控制器至少需要一个View的实例,可能需要一个Model的实例

事件业务全部被控制器负责了,每次View的操作会引起Model的Setter操作从而影响数据模型的变化便会通知其观察者View做出相应的改变





Blade UI中的MVC

但是,上述的实现在实际使用中发现并不是那么好用,为什么呢?

分层过细

分层思维应该大力倡导,但是层次的划分也有一个度,因为总的来说分层多了业务实现复杂度或者阅读门槛就会上来

以上述的方案如果去做UI的话是相当得不偿失的,一个UI的形成,便需要一个view的实例,一个Model的实例,再变态一点设置会被划分到不同的模块

这样的话维护成本以及代码编写成本便有所提高,总之分层有理,但也要适度!这个时候便需要改造,改造点集中表现为:

① 我的View不需要具有状态值,我就只有一个模块

② 我的Model不想与人共享,我就放在自己的内部属性即可



 View Code
核心点变成了几个属性:

① template,根据他生成UI

② datamodel,根据他生成viewModel提供给template使用

③ eventArr,业务事件注册点

这里简单以alert组件做说明:

 1 define(['UILayer', getAppUITemplatePath('ui.alert')], function (UILayer, template) {
 2 
 3   return _.inherit(UILayer, {
 4     propertys: function ($super) {
 5       $super();
 6 
 7       //数据模型
 8       this.datamodel = {
 9         title: 'alert',
10         content: 'content',
11         btns: [
12           { name: 'cancel', className: 'cui-btns-cancel' },
13           { name: 'ok', className: 'cui-btns-ok' }
14         ]
15       };
16 
17       //html模板
18       this.template = template;
19 
20       //事件机制
21       this.events = {
22         'click .cui-btns-ok': 'okAction',
23         'click .cui-btns-cancel': 'cancelAction'
24       };
25     },
26 
27     initialize: function ($super, opts) {
28       $super(opts);
29     },
30 
31     addEvent: function ($super) {
32       $super();
33       this.on('onCreate', function () {
34         this.$el.addClass('cui-alert');
35       });
36       this.maskToHide = false;
37     },
38 
39     okAction: function () {
40       this.hide();
41       console.log('ok');
42     },
43 
44     cancelAction: function () {
45       this.hide();
46       console.log('cancel');
47 
48     },
49 
50     setDatamodel: function (datamodel, okAction, cancelAction) {
51       if (!datamodel) datamodel = {};
52       _.extend(this.datamodel, datamodel);
53       this.okAction = okAction;
54       this.cancelAction = cancelAction;
55       this.refresh();
56     }
57 
58   });
59 
60 });
<div class="cui-pop-box">
  <div class="cui-hd">
      <%=title%>
  </div>
  <div class="cui-bd">
    <div class="cui-error-tips">
      <%=content%></div>
    <div class="cui-roller-btns">
      <% for(var i = 0, len = btns.length; i < len; i++ ) {%>
      <div class="cui-flexbd <%=btns[i].className%>">
        <%=btns[i].name%></div>
      <% } %>
    </div>
  </div>
</div>
实例化时,alert组件会执行基类的方法,最终反正会执行AbstractView的程序逻辑

首先会根据datamodel以及template生成DOM结构,然后在使用事件代理的方式用eventArr绑定业务事件,具体实现请移步至:

https://github.com/yexiaochai/blade/tree/master/blade/ui

结语

今天又做了一回标题党,引面试党进来看了看,然后谈了自己对前端MVC、分成的一些理解,最后说了Blade UI一块的设计思路,希望对各位有帮助

有时候感觉知道了却写不出来,然后本来也想好好的解析下代码却感觉没什么说的,烦劳各位自己看看吧

最后微博求粉:http://weibo.com/yiquinian/home?wvr=5




本文转自叶小钗博客园博客,原文链接http://www.cnblogs.com/yexiaochai/p/3888373.html,如需转载请自行联系原作者

网友评论

登录后评论
0/500
评论
范大脚脚
+ 关注