【blade的UI設計】理解前端MVC與分層思想

葉小釵發表於2014-08-03

前言

最近校招要來了,很多大三的同學一定按捺不住心中的焦躁,其中有期待也有彷徨,或許更多的是些許擔憂,最近在開始瘋狂的複習了吧

這裡小釵有幾點建議給各位:

① 不要看得太重,關心則亂,太緊張反而表現不好

② 好的選擇比堅持更重要

這點小釵便深有體會了,因為當年我是搞.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這個思想,各個模組只是關心了自己的職責,舉個例子來說:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ToDoList</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">
  <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">
  <link href="../style/main.css" rel="stylesheet" type="text/css" />
  <style type="text/css">
    .cui-alert { width: auto; position: static; }
    .txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; }
    ul, li { padding: 0; margin: 0; }
    .cui_calendar, .cui_week { list-style: none; }
    .cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; }
  </style>
</head>
<body>
  <article id="container">
  </article>
  <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>

  <script src="../../vendor/underscore-min.js" type="text/javascript"></script>
  <script src="../../vendor/zepto.min.js" type="text/javascript"></script>
  <script src="../../src/underscore-extend.js" type="text/javascript"></script>
  <script src="../../src/util.js" type="text/javascript"></script>
  <script src="../../src/mvc.js" type="text/javascript"></script>
  <script type="text/javascript">

//模擬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();

  </script>
</body>
</html>

完成HTML
View Code
"use strict";

// ------------------華麗的分割線--------------------- //

// @description 正式的宣告Dalmatian框架的名稱空間
var Dalmatian = Dalmatian || {};

// @description 定義預設的template方法來自於underscore
Dalmatian.template = _.template;
Dalmatian.View = _.inherit({
  // @description 建構函式入口
  initialize: function (options) {
    this._initialize();
    this.handleOptions(options);
    this._initRoot();

  },

  _initRoot: function () {
    //根據html生成的dom包裝物件
    //有一種場景是使用者的view本身就是一個只有一個包裹器的結構,他不想要多餘的包裹器
    this.root = $(this.defaultContainerTemplate);
    this.root.attr('id', this.viewid);
  },

  // @description 設定預設屬性
  _initialize: function () {

    var DEFAULT_CONTAINER_TEMPLATE = '<section class="view" id="<%=viewid%>"><%=html%></section>';

    // @description view狀態機
    // this.statusSet = {};

    this.defaultContainerTemplate = DEFAULT_CONTAINER_TEMPLATE;

    // @override
    // @description template集合,根據status做template的map
    // @example
    //    { 0: '<ul><%_.each(list, function(item){%><li><%=item.name%></li><%});%></ul>' }
    // this.templateSet = {};

    this.viewid = _.uniqueId('dalmatian-view-');

  },

  // @description 操作建構函式傳入操作
  handleOptions: function (options) {
    // @description 從形參中獲取key和value繫結在this上
    if (_.isObject(options)) _.extend(this, options);

  },

  // @description 通過模板和資料渲染具體的View
  // @param status {enum} View的狀態引數
  // @param data {object} 匹配View的資料格式的具體資料
  // @param callback {functiion} 執行完成之後的回撥
  render: function (status, data, callback) {

    var templateSelected = this.templateSet[status];
    if (templateSelected) {

      // @description 渲染view
      var templateFn = Dalmatian.template(templateSelected);
      this.html = templateFn(data);

      //這裡減少一次js編譯
//      this.root.html('');
//      this.root.append(this.html);

      this.currentStatus = status;

      _.callmethod(callback, this);

      return this.html;

    }
  },

  // @override
  // @description 可以被複寫,當status和data分別發生變化時候
  // @param status {enum} view的狀態值
  // @param data {object} viewmodel的資料
  update: function (status, data) {

    if (!this.currentStatus || this.currentStatus !== status) {
      return this.render(status, data);
    }

    // @override
    // @description 可複寫部分,當資料發生變化但是狀態沒有發生變化時,頁面僅僅變化的可以是區域性顯示
    //              可以通過獲取this.html進行修改
    _.callmethod(this.onUpdate, this, data);
  }
});

Dalmatian.Adapter = _.inherit({

  // @description 建構函式入口
  initialize: function (options) {
    this._initialize();
    this.handleOptions(options);

  },

  // @description 設定預設屬性
  _initialize: function () {
    this.observers = [];
    //    this.viewmodel = {};
    this.datamodel = {};
  },

  // @description 操作建構函式傳入操作
  handleOptions: function (options) {
    // @description 從形參中獲取key和value繫結在this上
    if (_.isObject(options)) _.extend(this, options);
  },

  // @override
  // @description 設定
  format: function (datamodel) {
    return datamodel;
  },

  getViewModel: function () {
    return this.format(this.datamodel);
  },

  registerObserver: function (viewcontroller) {
    // @description 檢查佇列中如果沒有viewcontroller,從佇列尾部推入
    if (!_.contains(this.observers, viewcontroller)) {
      this.observers.push(viewcontroller);
    }
  },

  unregisterObserver: function (viewcontroller) {
    // @description 從observers的佇列中剔除viewcontroller
    this.observers = _.without(this.observers, viewcontroller);
  },

  //統一設定所有觀察者的狀態,因為對應觀察者也許根本不具備相關狀態,所以這裡需要處理
//  setStatus: function (status) {
//    _.each(this.observers, function (viewcontroller) {
//      if (_.isObject(viewcontroller))
//        viewcontroller.setViewStatus(status);
//    });
//  },

  notifyDataChanged: function () {
    // @description 通知所有註冊的觀察者被觀察者的資料發生變化
    var data = this.getViewModel();
    _.each(this.observers, function (viewcontroller) {
      if (_.isObject(viewcontroller))
        _.callmethod(viewcontroller.update, viewcontroller, [data]);
    });
  }
});

Dalmatian.ViewController = _.inherit({

  _initialize: function () {

    //使用者設定的容器選擇器,或者dom結構
    this.container;
    //根元素
    this.$el;

    //一定會出現
    this.view;
    //可能會出現
    this.adapter;
    //初始化的時候便需要設定view的狀態,否則會渲染失敗,這裡給一個預設值
    this.viewstatus = 'init';

  },

  // @description 建構函式入口
  initialize: function (options) {
    this._initialize();
    this.handleOptions(options);
    this._handleAdapter();
    this.create();
  },

  //處理dataAdpter中的datamodel,為其注入view的預設容器資料
  _handleAdapter: function () {
    //不存在就不予理睬
    if (!this.adapter) return;
    this.adapter.registerObserver(this);
  },

  // @description 操作建構函式傳入操作
  handleOptions: function (options) {
    if (!options) return;

    this._verify(options);

    // @description 從形參中獲取key和value繫結在this上
    if (_.isObject(options)) _.extend(this, options);
  },

  setViewStatus: function (status) {
    this.viewstatus = status;
  },

  // @description 驗證引數
  _verify: function (options) {
    //這個underscore方法新框架在報錯
    //    if (!_.property('view')(options) && (!this.view)) throw Error('view必須在例項化的時候傳入ViewController');
    if (options.view && (!this.view)) throw Error('view必須在例項化的時候傳入ViewController');
  },

  // @description 當資料發生變化時呼叫onViewUpdate,如果onViewUpdate方法不存在的話,直接呼叫render方法重繪
  update: function (data) {

    //    _.callmethod(this.hide, this);

    if (this.onViewUpdate) {
      _.callmethod(this.onViewUpdate, this, [data]);
      return;
    }
    this.render();

    //    _.callmethod(this.show, this);
  },

  /**
  * @override
  */
  render: function () {
    // @notation  這個方法需要被複寫
    this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel());
    this.view.root.html(this.view.html);
  },

  _create: function () {
    this.render();

    //render 結束後構建好根元素dom結構
    this.view.root.html(this.view.html);
    this.$el = this.view.root;
  },

  create: function () {

    //l_wang這塊不是很明白
    //是否檢查對映關係,不存在則recreate,但是在這裡dom結構未必在document上
    //    if (!$('#' + this.view.viewid)[0]) {
    //      return _.callmethod(this.recreate, this);
    //    }

    // @notation 在create方法呼叫前後設定onViewBeforeCreate和onViewAfterCreate兩個回撥
    _.wrapmethod(this._create, 'onViewBeforeCreate', 'onViewAfterCreate', this);

  },

  /**
  * @description 如果進入create判斷是否需要update一下頁面,sync view和viewcontroller的資料
  */
  _recreate: function () {
    this.update();
  },

  recreate: function () {
    _.wrapmethod(this._recreate, 'onViewBeforeRecreate', 'onViewAfterRecreate', this);
  },

  //事件註冊點
  bindEvents: function (events) {
    if (!(events || (events = _.result(this, 'events')))) return this;
    this.unBindEvents();

    // @description 解析event引數的正則
    var delegateEventSplitter = /^(\S+)\s*(.*)$/;
    var key, method, match, eventName, selector;

    //注意,此處做簡單的字串資料解析即可,不做實際業務
    for (key in events) {
      method = events[key];
      if (!_.isFunction(method)) method = this[events[key]];
      if (!method) continue;

      match = key.match(delegateEventSplitter);
      eventName = match[1], selector = match[2];
      method = _.bind(method, this);
      eventName += '.delegateEvents' + this.view.viewid;

      if (selector === '') {
        this.$el.on(eventName, method);
      } else {
        this.$el.on(eventName, selector, method);
      }
    }

    return this;
  },

  //取消所有事件
  unBindEvents: function () {
    this.$el.off('.delegateEvents' + this.view.viewid);
    return this;
  },

  _show: function () {
    this.bindEvents();
    $(this.container).append(this.$el);
    this.$el.show();
  },

  show: function () {
    _.wrapmethod(this._show, 'onViewBeforeShow', 'onViewAfterShow', this);
  },

  _hide: function () {
    this.forze();
    this.$el.hide();
  },

  hide: function () {
    _.wrapmethod(this._hide, 'onViewBeforeHide', 'onViewAfterHide', this);
  },

  _forze: function () {
    this.unBindEvents();
  },

  forze: function () {
    _.wrapmethod(this._forze, 'onViewBeforeForzen', 'onViewAfterForzen', this);
  },

  _destory: function () {
    this.unBindEvents();
    this.$el.remove();
    //    delete this;
  },

  destory: function () {
    _.wrapmethod(this._destory, 'onViewBeforeDestory', 'onViewAfterDestory', this);
  }
});

完整MVC想法
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不想與人共享,我就放在自己的內部屬性即可

define([], function () {

  //閉包儲存所有UI共用的資訊,比如z-index
  var getBiggerzIndex = (function () {
    var index = 3000;
    return function (level) {
      return level + (++index);
    };
  })();

  var UIContainerUtil = (function () {
    //一個閉包物件存放所有例項化的ui例項
    var UIContainer = {};

    return {
      addItem: function (id, ui) {
        UIContainer[id] = ui;
      },

      removeItem: function (id) {
        if (UIContainer[id]) delete UIContainer[id];
      },

      getItem: function (id) {
        if (id) return UIContainer[id];
        return UIContainer;
      }
    };
  })();


  return _.inherit({

    //預設屬性
    propertys: function () {
      //模板狀態
      this.template = '';
      this.datamodel = {};
      this.events = {};
      this.wrapper = $('body');
      this.id = _.uniqueId('ui-view-');

      //自定義事件
      //此處需要注意mask 繫結事件前後問題,考慮scroll.radio外掛型別的mask應用,考慮元件通訊
      this.eventArr = {};

      //初始狀態為例項化
      this.status = 'init';

      //      this.availableFn = function () { }

    },

    //繫結事件,這裡應該提供一個方法,表明是insert 或者 push
    on: function (type, fn, insert) {
      if (!this.eventArr[type]) this.eventArr[type] = [];

      //頭部插入
      if (insert) {
        this.eventArr[type].splice(0, 0, fn);
      } else {
        this.eventArr[type].push(fn);
      }
    },

    off: function (type, fn) {
      if (!this.eventArr[type]) return;
      if (fn) {
        this.eventArr[type] = _.without(this.eventArr[type], fn);
      } else {
        this.eventArr[type] = [];
      }
    },

    trigger: function (type) {
      var _slice = Array.prototype.slice;
      var args = _slice.call(arguments, 1);
      var events = this.eventArr;
      var results = [], i, l;

      if (events[type]) {
        for (i = 0, l = events[type].length; i < l; i++) {
          results[results.length] = events[type][i].apply(this, args);
        }
      }
      return results;
    },

    createRoot: function () {
      this.$el = $('<div class="view" style="display: none; " id="' + this.id + '"></div>');
    },

    setOption: function (options) {
      for (var k in options) {
        if (k == 'datamodel') {
          _.extend(this.datamodel, options[k]);
          continue;
        }
        this[k] = options[k]
      }
      //      _.extend(this, options);
    },

    initialize: function (opts) {
      this.propertys();
      this.setOption(opts);
      this.resetPropery();
      this.createRoot();
      //新增系統級別事件
      this.addSysEvents();
      this.addEvent();

      //開始建立dom
      this.create();
      this.initElement();

      //將當前的ui例項裝入容器
      UIContainerUtil.addItem(this.id, this);

    },

    //返回所有例項化的UI元件集合
    getUIContainer: function () {
      return UIContainerUtil.getItem();
    },

    //內部重置event,加入全域性控制類事件
    addSysEvents: function () {
      if (typeof this.availableFn != 'function') return;
      this.removeSysEvents();
      this.$el.on('click.system' + this.id, $.proxy(function (e) {
        if (!this.availableFn()) {
          e.preventDefault();
          e.stopImmediatePropagation && e.stopImmediatePropagation();
        }
      }, this));
    },

    removeSysEvents: function () {
      this.$el.off('.system' + this.id);
    },

    $: function (selector) {
      return this.$el.find(selector);
    },

    //提供屬性重置功能,對屬性做檢查
    resetPropery: function () {
    },

    //各事件註冊點,用於被繼承
    addEvent: function () {
    },

    create: function () {
      this.trigger('onPreCreate');
      //      this.$el.html(this.render(this.getViewModel()));
      this.render();
      this.status = 'create';
      this.trigger('onCreate');
    },

    //例項化需要用到到dom元素
    initElement: function () { },

    render: function (data, callback) {
      data = this.getViewModel() || {};
      var html = this.template;
      if (!this.template) return '';
      if (data) {
        html = _.template(this.template)(data);
      }
      typeof callback == 'function' && callback.call(this);
      this.$el.html(html);
      return html;
    },

    //重新整理根據傳入引數判斷是否走onCreate事件
    //這裡原來的dom會被移除,事件會全部丟失 需要修復*****************************
    refresh: function (needEvent) {
      this.resetPropery();
      if (needEvent) {
        this.create();
      } else {
        this.render();
      }
      this.initElement();
      if (this.status == 'show') this.show();
    },

    show: function () {
      this.wrapper.append(this.$el);
      this.trigger('onPreShow');
      this.$el.show();
      this.status = 'show';
      this.bindEvents();
      this.trigger('onShow');
    },

    hide: function () {
      this.trigger('onPreHide');
      this.$el.hide();
      this.status = 'hide';
      this.unBindEvents();
      this.removeSysEvents();
      this.trigger('onHide');
    },

    destroy: function () {
      this.unBindEvents();
      this.removeSysEvents();
      UIContainerUtil.removeItem(this.id);
      this.$el.remove();
      delete this;
    },

    getViewModel: function () {
      return this.datamodel;
    },

    setzIndexTop: function (el, level) {
      if (!el) el = this.$el;
      if (!level || level > 10) level = 0;
      level = level * 1000;
      el.css('z-index', getBiggerzIndex(level));

    },

    /**
    * 解析events,根據events的設定在dom上設定事件
    */
    bindEvents: function () {
      var events = this.events;

      if (!(events || (events = _.result(this, 'events')))) return this;
      this.unBindEvents();

      // 解析event引數的正則
      var delegateEventSplitter = /^(\S+)\s*(.*)$/;
      var key, method, match, eventName, selector;

      // 做簡單的字串資料解析
      for (key in events) {
        method = events[key];
        if (!_.isFunction(method)) method = this[events[key]];
        if (!method) continue;

        match = key.match(delegateEventSplitter);
        eventName = match[1], selector = match[2];
        method = _.bind(method, this);
        eventName += '.delegateUIEvents' + this.id;

        if (selector === '') {
          this.$el.on(eventName, method);
        } else {
          this.$el.on(eventName, selector, method);
        }
      }

      return this;
    },

    /**
    * 凍結dom上所有元素的所有事件
    *
    * @return {object} 執行作用域
    */
    unBindEvents: function () {
      this.$el.off('.delegateUIEvents' + this.id);
      return this;
    }

  });

});
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

相關文章