【移動前端開發實踐】從無到有(統計、請求、MVC、模塊化)H5開發須知

前言

不知不覺來百度已有半年之久,這半年是996的半年,是孤軍奮戰的半年,是跌跌撞撞的半年,一個字:真的是累死人啦!javascript

我所進入的團隊至關於公司內部創業團隊,人員基本所有是新招的,最初開發時連數據庫都沒設計,當時評審需求的時候竟然有一個產品經理拿了一份他設計的數據庫,當時我做爲一個前端就驚呆了......html

最初的前端只有我1人,這事實上與我想來學習學習的願望是背道而馳的,但既然來都來了也只能獨挑大樑,立刻投入開發,當時涉及的項目有:前端

① H5站點java

② PC站點ios

③ Mis後臺管理系統git

④ 各類百度渠道接入web

第一階段的重點爲H5站點與APP,咱們便須要在20天內從無到有的完成初版的產品,而最初的Native人力嚴重不足,不少頁面依賴於H5這邊,因此前端除了自己業務以外還得約定與Native的交互細節。面試

這個狀況下根本無暇思考其它框架,熟悉的就是最好的!便將本身git上的開源框架直接拿來用了起來:[置頂]【blade利刃出鞘】一塊兒進入移動端webapp開發吧ajax

由於以前的經驗積累,工程化、Hybrid交互、各類兼容、體驗問題已經處理了不少了,因此基礎架構一層比較完備,又有完善的UI組件可使用,這個是最初的設計構想:數據庫

構想老是美好的,而在巨大的業務壓力面前任何技術願景都是蒼白的,最初我在哪裏很傻很天真的用CSS3畫圖標,而後產品經理每天像一個蒼蠅同樣在我面前嗡嗡嗡,他們事實上是不關注頁面性能是何物的,我也立刻意識的到工期不足,因而便直接用圖標了!

依賴於完善的框架,20天不到的時間,初版的項目便結束了,業務代碼有點不堪入目,頁面級的代碼也沒有太遵循MVC規則,這致使了後續的迭代,所有在那裏操做dom。

其實初期這樣作問題不大,若是項目比較小(好比什麼一次性的活動頁面)問題也不大,可是核心項目便最好不要這樣玩了,由於新需求、新場景,會讓你在原基礎上不斷的改代碼,若是頁面沒有一個很好的規範,那麼他將再也不穩定,也再也不容易維護,如何編寫一個可穩定、擴展性高、可維護性高的項目,是咱們今天討論的重點。

認真閱讀此文可能會在如下方面對你有所幫助:

① 網站初期須要統計什麼數據?產品須要的業務數據,你該如何設計你的網站才能收集到這些數據,提供給他
② 完整的請求究竟應該如何發出,H5應該如何在前端作緩存,服務器給出的數據應該在哪裏作校驗,前端錯誤日誌應該關注js錯誤仍是數據錯誤?
③ 你在寫業務代碼時犯了什麼錯誤,如何編寫高效可維護的業務代碼(頁面級別),MVC究竟是個什麼東西?
④ 網站規模大瞭如何複用一些模塊?
⑤ 站在業務角度應該如何作性能優化(這個可能不是本文的重點)

文中是我半年以來的一些業務開發經驗,但願對各位有用,也但願各位多多支持討論,指出文中不足以及提出您的一些建議

統計需求

通用統計需求

對於服務器端來講,後期最重要的莫過於監控日誌,對於前端來講,統計無疑是初期最重要的,通用的統計需求包括:

① PV/UV統計

② 機型/瀏覽器/系通通計

③ 各頁面載入速度統計

④ 某些按鈕的點擊統計

⑤ ......

這類統計直接經過百度統計之類的工具便可,算是最基礎的統計需求。百度產品的文檔、支持團隊爛估計是公認的事情了,我便只能挖掘不多一部分用法。可是這類數據也是很是重要了,對於產品甚至是老闆判斷整個產品的發展有莫大的幫助與引導做用,若是產品死了,任何技術都是沒有意義的,因此站點沒有這類統計的速度加上吧!

http://tongji.baidu.com/web/welcome/login

渠道統計

所謂渠道統計即是此次訂單來源是哪裏,就咱們產品的渠道有:

① 手機百度APP入口(由分爲生活+入口、首頁banner入口、廣告入口......)

② 百度移動站點入口

③ 百度地圖入口(包括H5站點)

④ wise卡片入口(包括:惟一答案、白卡片、極速版、點到點卡片......)

⑤ 各類大禮包、活動入口

⑥ SEM入口

⑦ ......

你永遠不能預料到你究竟有多少入口,可是這種渠道的統計的重要性直接關乎了產品的存亡,產品須要知道本身的每次的活動,每次的引流是有意義的,好比一次活動便須要獲得此次活動天天產生的訂單量,若是你告訴產品,爺作不到,那麼產品會真叫你爺爺。

固然,渠道的統計前端單方面是完成不了的,須要和服務器端配合,通常而言能夠這樣作,前端與服務器端約定,每次請求皆會帶特定的參數,我通常會與服務器約定如下參數:

var param = {
    head: {
        us: '渠道',
        version: '1.0.0'
    }
};

這個head參數是每次ajax請求都會帶上的,而us參數通常由url而來,他要求每次由其它渠道落地到咱們的站點必定要帶有us參數,us參數拿到後即是咱們本身的事情了,有幾種操做方法:

① 直接種到cookie,這個須要服務器端特殊處理

② 存入localstorage,每次請求拿出來,組裝請求參數

③ 由於咱們H5站點的每一次跳轉都會通過框架中轉,因此我直接將us數據放到了url上,每次跳轉都會帶上,一直到跳出網站。

SEM需求

SEM其實屬於渠道需求的一類,這裏會獨立出來是由於,他須要統計的數據更多,還會包含一個投放詞之類的數據,SEM投放人員須要確切的知道某個投放詞天天的訂單量,這個時候上面的參數可能就要變化了:

var param = {
    head: {
        us: '渠道',
        version: '1.0.0',
        extra: '擴展字段'
    }
};

這個時候可能便須要一個extra的擴展字段記錄投放詞是什麼,固然SEM落地到咱們網站的特殊參數也須要一直傳下去,這個須要作框架層的處理,這裏順便說下個人處理方案吧

統一跳轉

首先咱們H5站點基本不關注SEO,對於SEO咱們有特殊的處理方案,因此在咱們的H5站點上基本不會出現a標籤,咱們站點的每次跳轉皆是由js控制,我會在框架封裝幾個方法處理跳轉:

forward: function (view) {
     //處理頻道內跳轉
}

back: function (view) {
}

jump: function (project, view) {
     //處理跨頻道跳轉
}

這樣作的好處是:

① 統一封裝跳轉會讓前端控制力增長,好比forward能夠是location變化,也能夠是pushState/hash的方式作單頁跳轉,甚至能夠作Hybrid中多Webview的跳轉

② 誠如上述,forward時能夠由url獲取渠道參數帶到下一個頁面

③ 統一跳轉也能夠統一爲站點作一些打點的操做,好比單頁應用時候的統一加統計代碼

最簡單的理解就是:封裝一個全局方法作跳轉控制,全部的跳轉由他發出。

請求模塊

ajax是前端到服務器端的基石,可是前端和服務器端的交互:

每一個接口必需要寫文檔!
每一個接口必需要寫文檔!
每一個接口必需要寫文檔!
重要的事情說三遍!!!

若是不寫文檔的話,你就等着吧,由於端上是入口,一旦出問題,老闆會直觀認爲是前端的問題,若是發現是服務器的字段不統一致使,而服務器端打死不認可,你就等着吧!

不管何時,前端請求模塊的設計是很是關鍵的,由於前端只是數據的搬運工,負責展示數據而已:)

封裝請求模塊

與封裝統一跳轉一致,全部的請求必須收口,最爛的作法也是封裝一個全局的方法處理全站請求,這樣作的好處是:

① 處理公共參數

好比每次請求必須帶上上面所述head業務參數,便必須在此作處理

② 處理統一錯誤碼

服務器與前端通常會有一個格式約定,通常而言是這樣的:

{
  data: {},
  errno: 0,
  msg: "success"
}

好比錯誤碼爲1的狀況就表明須要登陸,系統會引導用戶進入登陸頁,好比非0的狀況下,須要彈出一個提示框告訴用戶出了什麼問題,你不可能在每一個地方都作這種錯誤碼處理吧

③ 統一緩存處理

有些請求數據不會常常改變,好比城市列表,好比經常使用聯繫人,這個時候便須要將之存到localstorage中作緩存

④ 數據處理、日誌處理

這裏插一句監控的問題,由於前端代碼壓縮後,js錯誤監控變得不太靠譜,而前端的錯誤有很大多是搬運數據過程當中出了問題,因此在請求model層作對應的數據校驗是十分有意義的
若是發現數據不對便發錯誤日誌,好過被用戶抓住投訴,而這裏作數據校驗也爲模板中使用數據作了基礎檢查

服務器端給前端的數據多是鬆散的,前端真實使用時候會對數據作處理,同一請求模塊若是在不一樣地方使用,就須要屢次處理,這個是不須要的,好比:

//這個判斷應該放在數據模塊中
if(data.a) ...
if(data.a.b) ...

這裏我說下blade框架中請求模塊的處理:

blade的請求模塊

咱們如今站點主要仍是源於blade框架,實際使用時候作了點改變,後續會迴歸到blade框架,項目目錄結構爲:

其中store依賴於storage模塊,是處理localstorage緩存的,他與model是獨立的,如下爲核心代碼:

  1 define([], function () {
  2 
  3   var Model = _.inherit({
  4     //默認屬性
  5     propertys: function () {
  6       this.protocol = 'http';
  7       this.domain = '';
  8       this.path = '';
  9       this.url = null;
 10       this.param = {};
 11       this.validates = [];
 12       //      this.contentType = 'application/json';
 13 
 14       this.ajaxOnly = true;
 15 
 16       this.contentType = 'application/x-www-form-urlencoded';
 17       this.type = 'GET';
 18       this.dataType = 'json';
 19     },
 20 
 21     setOption: function (options) {
 22       _.extend(this, options);
 23     },
 24 
 25     assert: function () {
 26       if (this.url === null) {
 27         throw 'not override url property';
 28       }
 29     },
 30 
 31     initialize: function (opts) {
 32       this.propertys();
 33       this.setOption(opts);
 34       this.assert();
 35 
 36     },
 37 
 38     pushValidates: function (handler) {
 39       if (typeof handler === 'function') {
 40         this.validates.push($.proxy(handler, this));
 41       }
 42     },
 43 
 44     setParam: function (key, val) {
 45       if (typeof key === 'object') {
 46         _.extend(this.param, key);
 47       } else {
 48         this.param[key] = val;
 49       }
 50     },
 51 
 52     removeParam: function (key) {
 53       delete this.param[key];
 54     },
 55 
 56     getParam: function () {
 57       return this.param;
 58     },
 59 
 60     //構建url請求方式,子類可複寫,咱們的model若是localstorage設置了值便直接讀取,可是得是非正式環境
 61     buildurl: function () {
 62       //      var baseurl = AbstractModel.baseurl(this.protocol);
 63       //      return this.protocol + '://' + baseurl.domain + '/' + baseurl.path + (typeof this.url === 'function' ? this.url() : this.url);
 64       throw "[ERROR]abstract method:buildurl, must be override";
 65 
 66     },
 67 
 68     onDataSuccess: function () {
 69     },
 70 
 71     /**
 72     *    取model數據
 73     *    @param {Function} onComplete 取完的回調函
 74     *    傳入的第一個參數爲model的數第二個數據爲元數據,元數據爲ajax下發時的ServerCode,Message等數
 75     *    @param {Function} onError 發生錯誤時的回調
 76     *    @param {Boolean} ajaxOnly 可選,默認爲false當爲true時只使用ajax調取數據
 77     * @param {Boolean} scope 可選,設定回調函數this指向的對象
 78     * @param {Function} onAbort 可選,但取消時會調用的函數
 79     */
 80     execute: function (onComplete, onError, ajaxOnly, scope) {
 81       var __onComplete = $.proxy(function (data) {
 82         var _data = data;
 83         if (typeof data == 'string') _data = JSON.parse(data);
 84 
 85         // @description 開發者能夠傳入一組驗證方法進行驗證
 86         for (var i = 0, len = this.validates.length; i < len; i++) {
 87           if (!this.validates[i](data)) {
 88             // @description 若是一個驗證不經過就返回
 89             if (typeof onError === 'function') {
 90               return onError.call(scope || this, _data, data);
 91             } else {
 92               return false;
 93             }
 94           }
 95         }
 96 
 97         // @description 對獲取的數據作字段映射
 98         var datamodel = typeof this.dataformat === 'function' ? this.dataformat(_data) : _data;
 99 
100         if (this.onDataSuccess) this.onDataSuccess.call(this, datamodel, data);
101         if (typeof onComplete === 'function') {
102           onComplete.call(scope || this, datamodel, data);
103         }
104 
105       }, this);
106 
107       var __onError = $.proxy(function (e) {
108         if (typeof onError === 'function') {
109           onError.call(scope || this, e);
110         }
111       }, this);
112 
113       this.sendRequest(__onComplete, __onError);
114 
115     },
116 
117     sendRequest: function (success, error) {
118       var url = this.buildurl();
119       var params = _.clone(this.getParam() || {});
120       var crossDomain = {
121         'json': true,
122         'jsonp': true
123       };
124 
125       //      if (this.type == 'json')
126       //      if (this.type == 'POST') {
127       //        this.dataType = 'json';
128       //      } else {
129       //        this.dataType = 'jsonp';
130       //      }
131 
132       if (this.type == 'POST') {
133         this.dataType = 'json';
134       }
135 
136       //jsonp與post互斥
137       $.ajax({
138         url: url,
139         type: this.type,
140         data: params,
141         dataType: this.dataType,
142         contentType: this.contentType,
143         crossDomain: crossDomain[this.dataType],
144         timeout: 50000,
145         xhrFields: {
146           withCredentials: true
147         },
148         success: function (res) {
149           success && success(res);
150         },
151         error: function (err) {
152           error && error(err);
153         }
154       });
155 
156     }
157 
158   });
159 
160   Model.getInstance = function () {
161     if (this.instance) {
162       return this.instance;
163     } else {
164       return this.instance = new this();
165     }
166   };
167 
168   return Model;
169 });
model
  1 define(['AbstractStorage'], function (AbstractStorage) {
  2 
  3   var Store = _.inherit({
  4     //默認屬性
  5     propertys: function () {
  6 
  7       //每一個對象必定要具備存儲鍵,而且不能重複
  8       this.key = null;
  9 
 10       //默認一條數據的生命週期,S爲秒,M爲分,D爲天
 11       this.lifeTime = '30M';
 12 
 13       //默認返回數據
 14       //      this.defaultData = null;
 15 
 16       //代理對象,localstorage對象
 17       this.sProxy = new AbstractStorage();
 18 
 19     },
 20 
 21     setOption: function (options) {
 22       _.extend(this, options);
 23     },
 24 
 25     assert: function () {
 26       if (this.key === null) {
 27         throw 'not override key property';
 28       }
 29       if (this.sProxy === null) {
 30         throw 'not override sProxy property';
 31       }
 32     },
 33 
 34     initialize: function (opts) {
 35       this.propertys();
 36       this.setOption(opts);
 37       this.assert();
 38     },
 39 
 40     _getLifeTime: function () {
 41       var timeout = 0;
 42       var str = this.lifeTime;
 43       var unit = str.charAt(str.length - 1);
 44       var num = str.substring(0, str.length - 1);
 45       var Map = {
 46         D: 86400,
 47         H: 3600,
 48         M: 60,
 49         S: 1
 50       };
 51       if (typeof unit == 'string') {
 52         unit = unit.toUpperCase();
 53       }
 54       timeout = num;
 55       if (unit) timeout = Map[unit];
 56 
 57       //單位爲毫秒
 58       return num * timeout * 1000 ;
 59     },
 60 
 61     //緩存數據
 62     set: function (value, sign) {
 63       //獲取過時時間
 64       var timeout = new Date();
 65       timeout.setTime(timeout.getTime() + this._getLifeTime());
 66       this.sProxy.set(this.key, value, timeout.getTime(), sign);
 67     },
 68 
 69     //設置單個屬性
 70     setAttr: function (name, value, sign) {
 71       var key, obj;
 72       if (_.isObject(name)) {
 73         for (key in name) {
 74           if (name.hasOwnProperty(key)) this.setAttr(k, name[k], value);
 75         }
 76         return;
 77       }
 78 
 79       if (!sign) sign = this.getSign();
 80 
 81       //獲取當前對象
 82       obj = this.get(sign) || {};
 83       if (!obj) return;
 84       obj[name] = value;
 85       this.set(obj, sign);
 86 
 87     },
 88 
 89     getSign: function () {
 90       return this.sProxy.getSign(this.key);
 91     },
 92 
 93     remove: function () {
 94       this.sProxy.remove(this.key);
 95     },
 96 
 97     removeAttr: function (attrName) {
 98       var obj = this.get() || {};
 99       if (obj[attrName]) {
100         delete obj[attrName];
101       }
102       this.set(obj);
103     },
104 
105     get: function (sign) {
106       var result = [], isEmpty = true, a;
107       var obj = this.sProxy.get(this.key, sign);
108       var type = typeof obj;
109       var o = { 'string': true, 'number': true, 'boolean': true };
110       if (o[type]) return obj;
111 
112       if (_.isArray(obj)) {
113         for (var i = 0, len = obj.length; i < len; i++) {
114           result[i] = obj[i];
115         }
116       } else if (_.isObject(obj)) {
117         result = obj;
118       }
119 
120       for (a in result) {
121         isEmpty = false;
122         break;
123       }
124       return !isEmpty ? result : null;
125     },
126 
127     getAttr: function (attrName, tag) {
128       var obj = this.get(tag);
129       var attrVal = null;
130       if (obj) {
131         attrVal = obj[attrName];
132       }
133       return attrVal;
134     }
135 
136   });
137 
138   Store.getInstance = function () {
139     if (this.instance) {
140       return this.instance;
141     } else {
142       return this.instance = new this();
143     }
144   };
145 
146   return Store;
147 });
store
  1 define([], function () {
  2 
  3   var Storage = _.inherit({
  4     //默認屬性
  5     propertys: function () {
  6 
  7       //代理對象,默認爲localstorage
  8       this.sProxy = window.localStorage;
  9 
 10       //60 * 60 * 24 * 30 * 1000 ms ==30天
 11       this.defaultLifeTime = 2592000000;
 12 
 13       //本地緩存用以存放全部localstorage鍵值與過時日期的映射
 14       this.keyCache = 'SYSTEM_KEY_TIMEOUT_MAP';
 15 
 16       //當緩存容量已滿,每次刪除的緩存數
 17       this.removeNum = 5;
 18 
 19     },
 20 
 21     assert: function () {
 22       if (this.sProxy === null) {
 23         throw 'not override sProxy property';
 24       }
 25     },
 26 
 27     initialize: function (opts) {
 28       this.propertys();
 29       this.assert();
 30     },
 31 
 32     /*
 33     新增localstorage
 34     數據格式包括惟一鍵值,json字符串,過時日期,存入日期
 35     sign 爲格式化後的請求參數,用於同一請求不一樣參數時候返回新數據,好比列表爲北京的城市,後切換爲上海,會判斷tag不一樣而更新緩存數據,tag至關於簽名
 36     每一鍵值只會緩存一條信息
 37     */
 38     set: function (key, value, timeout, sign) {
 39       var _d = new Date();
 40       //存入日期
 41       var indate = _d.getTime();
 42 
 43       //最終保存的數據
 44       var entity = null;
 45 
 46       if (!timeout) {
 47         _d.setTime(_d.getTime() + this.defaultLifeTime);
 48         timeout = _d.getTime();
 49       }
 50 
 51       //
 52       this.setKeyCache(key, timeout);
 53       entity = this.buildStorageObj(value, indate, timeout, sign);
 54 
 55       try {
 56         this.sProxy.setItem(key, JSON.stringify(entity));
 57         return true;
 58       } catch (e) {
 59         //localstorage寫滿時,全清掉
 60         if (e.name == 'QuotaExceededError') {
 61           //            this.sProxy.clear();
 62           //localstorage寫滿時,選擇離過時時間最近的數據刪除,這樣也會有些影響,可是感受比全清除好些,若是緩存過多,此過程比較耗時,100ms之內
 63           if (!this.removeLastCache()) throw '本次數據存儲量過大';
 64           this.set(key, value, timeout, sign);
 65         }
 66         console && console.log(e);
 67       }
 68       return false;
 69     },
 70 
 71     //刪除過時緩存
 72     removeOverdueCache: function () {
 73       var tmpObj = null, i, len;
 74 
 75       var now = new Date().getTime();
 76       //取出鍵值對
 77       var cacheStr = this.sProxy.getItem(this.keyCache);
 78       var cacheMap = [];
 79       var newMap = [];
 80       if (!cacheStr) {
 81         return;
 82       }
 83 
 84       cacheMap = JSON.parse(cacheStr);
 85 
 86       for (i = 0, len = cacheMap.length; i < len; i++) {
 87         tmpObj = cacheMap[i];
 88         if (tmpObj.timeout < now) {
 89           this.sProxy.removeItem(tmpObj.key);
 90         } else {
 91           newMap.push(tmpObj);
 92         }
 93       }
 94       this.sProxy.setItem(this.keyCache, JSON.stringify(newMap));
 95 
 96     },
 97 
 98     removeLastCache: function () {
 99       var i, len;
100       var num = this.removeNum || 5;
101 
102       //取出鍵值對
103       var cacheStr = this.sProxy.getItem(this.keyCache);
104       var cacheMap = [];
105       var delMap = [];
106 
107       //說明本次存儲過大
108       if (!cacheStr) return false;
109 
110       cacheMap.sort(function (a, b) {
111         return a.timeout - b.timeout;
112       });
113 
114       //刪除了哪些數據
115       delMap = cacheMap.splice(0, num);
116       for (i = 0, len = delMap.length; i < len; i++) {
117         this.sProxy.removeItem(delMap[i].key);
118       }
119 
120       this.sProxy.setItem(this.keyCache, JSON.stringify(cacheMap));
121       return true;
122     },
123 
124     setKeyCache: function (key, timeout) {
125       if (!key || !timeout || timeout < new Date().getTime()) return;
126       var i, len, tmpObj;
127 
128       //獲取當前已經緩存的鍵值字符串
129       var oldstr = this.sProxy.getItem(this.keyCache);
130       var oldMap = [];
131       //當前key是否已經存在
132       var flag = false;
133       var obj = {};
134       obj.key = key;
135       obj.timeout = timeout;
136 
137       if (oldstr) {
138         oldMap = JSON.parse(oldstr);
139         if (!_.isArray(oldMap)) oldMap = [];
140       }
141 
142       for (i = 0, len = oldMap.length; i < len; i++) {
143         tmpObj = oldMap[i];
144         if (tmpObj.key == key) {
145           oldMap[i] = obj;
146           flag = true;
147           break;
148         }
149       }
150       if (!flag) oldMap.push(obj);
151       //最後將新數組放到緩存中
152       this.sProxy.setItem(this.keyCache, JSON.stringify(oldMap));
153 
154     },
155 
156     buildStorageObj: function (value, indate, timeout, sign) {
157       var obj = {
158         value: value,
159         timeout: timeout,
160         sign: sign,
161         indate: indate
162       };
163       return obj;
164     },
165 
166     get: function (key, sign) {
167       var result, now = new Date().getTime();
168       try {
169         result = this.sProxy.getItem(key);
170         if (!result) return null;
171         result = JSON.parse(result);
172 
173         //數據過時
174         if (result.timeout < now) return null;
175 
176         //須要驗證簽名
177         if (sign) {
178           if (sign === result.sign)
179             return result.value;
180           return null;
181         } else {
182           return result.value;
183         }
184 
185       } catch (e) {
186         console && console.log(e);
187       }
188       return null;
189     },
190 
191     //獲取簽名
192     getSign: function (key) {
193       var result, sign = null;
194       try {
195         result = this.sProxy.getItem(key);
196         if (result) {
197           result = JSON.parse(result);
198           sign = result && result.sign
199         }
200       } catch (e) {
201         console && console.log(e);
202       }
203       return sign;
204     },
205 
206     remove: function (key) {
207       return this.sProxy.removeItem(key);
208     },
209 
210     clear: function () {
211       this.sProxy.clear();
212     }
213   });
214 
215   Storage.getInstance = function () {
216     if (this.instance) {
217       return this.instance;
218     } else {
219       return this.instance = new this();
220     }
221   };
222 
223   return Storage;
224 
225 });
storage

真實的使用場景業務model首先得作一層業務封裝,而後纔是真正的使用:

  1 define(['AbstractModel', 'AbstractStore', 'cUser'], function (AbstractModel, AbstractStore, cUser) {
  2 
  3     var ERROR_CODE = {
  4         'NOT_LOGIN': '00001'
  5     };
  6 
  7     //獲取產品來源
  8     var getUs = function () {
  9         var us = 'webapp';
 10         //其它操做......
 11 
 12         //若是url具備us標誌,則首先讀取
 13         if (_.getUrlParam().us) {
 14             us = _.getUrlParam().us;
 15         }
 16         return us;
 17     };
 18 
 19     var BaseModel = _.inherit(AbstractModel, {
 20 
 21         initDomain: function () {
 22             var host = window.location.host;
 23 
 24             this.domain = host;
 25 
 26             //開發環境
 27             if (host.indexOf('yexiaochai.baidu.com') != -1) {
 28                 this.domain = 'xxx';
 29             }
 30 
 31             //qa環境
 32             if (host.indexOf('baidu.com') == -1) {
 33                 this.domain = 'xxx';
 34             }
 35 
 36             //正式環境
 37             if (host.indexOf('xxx.baidu.com') != -1 || host.indexOf('xxx.baidu.com') != -1) {
 38                 this.domain = 'api.xxx.baidu.com';
 39             }
 40 
 41         },
 42 
 43         propertys: function ($super) {
 44             $super();
 45 
 46             this.initDomain();
 47 
 48             this.path = '';
 49 
 50             this.cacheData = null;
 51             this.param = {
 52                 head: {
 53                     us: getUs(),
 54                     version: '1.0.0'
 55                 }
 56             };
 57             this.dataType = 'jsonp';
 58 
 59             this.errorCallback = function () { };
 60 
 61             //統一處理分返回驗證
 62             this.pushValidates(function (data) {
 63                 return this.baseDataValidate(data);
 64             });
 65 
 66         },
 67 
 68         //首輪處理返回數據,檢查錯誤碼作統一驗證處理
 69         baseDataValidate: function (data) {
 70             if (!data) {
 71                 window.APP.showToast('服務器出錯,請稍候再試', function () {
 72                     window.location.href = 'xxx';
 73                 });
 74                 return;
 75             }
 76 
 77             if (_.isString(data)) data = JSON.parse(data);
 78             if (data.errno === 0) return true;
 79 
 80             //處理統一登陸邏輯
 81             if (data.errno == ERROR_CODE['NOT_LOGIN']) {
 82                 cUser.login();
 83             }
 84 
 85             //其它通用錯誤碼的處理邏輯
 86             if (data.errno == xxxx) {
 87                 this.errorCallback();
 88                 return false;
 89             }
 90 
 91             //若是出問題則打印錯誤
 92             if (window.APP && data && data.msg) window.APP.showToast(data.msg, this.errorCallback);
 93 
 94             return false;
 95         },
 96 
 97         dataformat: function (data) {
 98             if (_.isString(data)) data = JSON.parse(data);
 99             if (data.data) return data.data;
100             return data;
101         },
102 
103         buildurl: function () {
104             return this.protocol + '://' + this.domain + this.path + (typeof this.url === 'function' ? this.url() : this.url);
105         },
106 
107         getSign: function () {
108             var param = this.getParam() || {};
109             return JSON.stringify(param);
110         },
111 
112         onDataSuccess: function (fdata, data) {
113             if (this.cacheData && this.cacheData.set)
114                 this.cacheData.set(fdata, this.getSign());
115         },
116 
117         //重寫父類getParam方法,加入方法簽名
118         getParam: function () {
119             var param = _.clone(this.param || {});
120 
121             //此處對參數進行特殊處理
122             //......
123 
124             return this.param;
125         },
126 
127         execute: function ($super, onComplete, onError, ajaxOnly, scope) {
128             var data = null;
129             if (!ajaxOnly && !this.ajaxOnly && this.cacheData && this.cacheData.get) {
130                 data = this.cacheData.get(this.getSign());
131                 if (data) {
132                     onComplete(data);
133                     return;
134                 }
135             }
136 
137             //記錄請求發出
138             $super(onComplete, onError, ajaxOnly, scope);
139         }
140 
141     });
142 
143     //localstorage存儲類
144     var Store = {
145         RequestStore: _.inherit(AbstractStore, {
146             //默認屬性
147             propertys: function ($super) {
148                 $super();
149                 this.key = 'BUS_RequestStore';
150                 this.lifeTime = '1D'; //緩存時間
151             }
152         })
153     };
154 
155     //返回真實的業務類
156     return {
157         //真實的業務請求
158         requestModel: _.inherit(BaseModel, {
159             //默認屬性
160             propertys: function ($super) {
161                 $super();
162                 this.url = '/url';
163                 this.ajaxOnly = false;
164                 this.cacheData = Store.RequestStore.getInstance();
165             }
166         })
167     };
168 });
業務封裝
 1 define(['BusinessModel'], function (Model) {
 2     var model = Model.requestModel.getInstance();
 3 
 4     //設置請求參數
 5     model.setParam();
 6     model.execute(function (data) {
 7         //這裏的data,若是model設置的完善,則前端使用可徹底信任其可用性不用作判斷了
 8 
 9         //這個是不須要的
10         if (data.person && data.person.name) {
11             //...
12         }
13 
14         //根據數據渲染頁面
15         //......
16     });
17 })

複雜的前端頁面

我以爲三端的開發中,前端的業務是最複雜的,由於IOS與Andriod的落地頁每每都是首頁,而前端的落地頁多是任何頁面(產品列表頁,訂單填寫頁,訂單詳情頁等),由於用戶徹底可能把這個url告訴朋友,讓朋友直接進入這個產品填寫頁。

而隨着業務發展、需求迭代,前端的頁面可能更加複雜,最初穩定的頁面承受了來自多方的挑戰。這個狀況在咱們團隊大概是這樣的:

在第一輪產品作完後,產品立刻安排了第二輪迭代,此次迭代的重點是訂單填寫頁,對訂單填寫有如下需求:

① 新增優惠券功能

② 優惠券在H5站點下默認不使用,在IOS、andriod下默認使用(恰好這個時候IOS還在用H5的頁面囧囧囧)

③ 默認自動填入用戶上一次的信息(站點經常使用功能)

這裏一、3是正常功能迭代,可是需求2能夠說是IOS APP 暫時使用H5站點的頁面,由於當時IOS已經招到了足夠的人,也正在進行訂單填寫的開發,事實上一個月之後他們APP便換掉了H5的訂單填寫,那麼這個時候將對應IOS的邏輯寫到本身的主邏輯中是很是愚蠢的,並且後續的發展更是超出了所料,由於H5站點的容器變成了:

① IOS APP裝載部分H5頁面

② Andriod APP裝載部分H5頁面

PS:這裏之因此把andriod和ios分開,由於andriod都開發了20多天了,ios才招到一我的,他們對H5頁面的需求徹底是兩回事囧!

③ 手機百度裝載H5頁面(基本與H5站點邏輯一致,有一些特殊需求,好比登陸、支付須要使用clouda調用apk)

④ 百度地圖webview容器

因而整我的就一下傻逼了,由於主邏輯基本類似,總有容器會但願一點特殊需求,從重構角度來講,咱們不會但願咱們的業務中出現上述代碼太多的if else;

從性能優化角度來講,就普通瀏覽器根本不須要理睬Hybrid交互相關,這個時候咱們完善的框架便派上了用場,抽離公共部分了:

H5仍然只關注主邏輯,而且將內部的每部操做盡量的細化,好比初始化操做,對某一個按鈕的點擊行爲等都應該儘量的分解到一個個獨立的方法中,真實項目大概是這個樣子的:

依賴框架自帶的繼承抽象,以及控制器路由層的按環境加載的機制,能夠有效解決此類問題,也有效下降了頁面的複雜度,可是他改變不了頁面愈來愈複雜的事實,而且這個時候迎來了第三輪迭代:

① 加入保險功能

② H5站點在某些渠道下默認開啓使用優惠券功能(囧囧囧!!!)

③ 限制優惠券必須達到某些條件才能使用

④ 訂單填寫頁做爲某一合做方的落地頁,請求參數和url有所變化,可是返回的字段一致,交互一致......

由於最初20天的慌亂處理,加之隨後兩輪的迭代,我已經在訂單填寫頁中買下了太多坑,並且網頁中隨處可見的dom操做讓代碼可維護程度大大下降,而點擊某一按鈕而致使的連鎖變化常常發生,好比,用戶增減購買商品數量時:

① 會改變自己商品數量的展現

② 會根據當前條件去刷新優惠卷使用數據

③ 改變支付條上的最終總額

④ ......

因而此次迭代後,你會發現訂單填寫頁尼瑪常常出BUG,每次改了又會有地方出BUG,一段時間不在,同事幫助修復了一個BUG,又引發了其它三個BUG,這個時候迎來了第四輪迭代,而這種種跡象代表:

若是一個頁面開始頻繁的出BUG,若是一個頁面邏輯愈來愈複雜,若是一個頁面的代碼你以爲很差維護了,那麼意味着,他應該獲得應有的重構了!

前端的MVC

不太MVC的作法

若是在你的頁面(會長久維護的項目)中有如下狀況的話,也許你應該重構你的頁面或者換掉你框架了:

① 在js中大規模的拼接HTML,好比這樣:

 1 for (i = 0; i < len; i++) {
 2     for (key in data[i]) {
 3         item = data[i][key];
 4         len2 = item.length;
 5         if (len2 === 0) continue;
 6         str += '<h2 class="wa-xxx-groupname">' + key + '</h2>';
 7         str += '<ul class=" wa-xxx-city-list-item ">';
 8         for (j = 0; j < len2; j++) {
 9             str += '<li data-type="' + item[j].type + '" data-city="' + item[j].regionid + '">' + item[j].cnname + '</li>';
10         }
11         str += '</ul>';
12         break;
13     }
14     if (str !== '')
15         html.push('<div class="wa-xxx-city-list">' + str + '</div>');
16     str = '';
17 }

對於這個狀況,你應該使用前端模板引擎

② 在js中出現大規模的獲取非文本框元素的值

③ 在html頁面中看到了大規模的數據鉤子,好比這個樣子:

④ 你在js中發現,一個數據由js變量可獲取,也能夠由dom獲取,並你對從哪獲取數據猶豫不決

⑤ 在你的頁面中,click事件分散到一個頁面的各個地方

⑥ 當你的js文件超過1000行,而且你以爲無法拆分

以上種種跡象代表,喲!這個頁面好像要被玩壞了,好像能夠用MVC的思想重構一下啦!

什麼是MVC

其實MVC這個東西有點懸,通常人壓根都不知道他是幹嗎的,就知道一個model-view-controller;

知道一點的又說不清楚;

真正懂的人要麼喜歡東扯西扯,要麼不肯意寫博客或者博客一來便很難,曲高和寡。

因此前端MVC這個東西一直是一個玄之又玄的東西,不少開發了好久的朋友都不能瞭解什麼是MVC。

今天我做爲一個自認爲懂得一點的人,便來講一說我對MVC在前端的認識,但願對你們有幫助。

前端給你們的認識即是頁面,頁面由HTML+CSS實現,若是有交互便須要JS的介入,其中:

對於真實的業務來講,HTML&CSS是零件,JS是搬運工,數據是設計圖與指令。
JS要根據數據指令將零件組裝爲玩具,用戶操做了玩具致使了數據變化,因而JS又根據數據指令從新組裝玩具
咱們事實上不寫代碼,咱們只是數據的搬運工

上述例子可能不必定準確,但他能夠表達一些中心思想,那就是:

對於頁面來講,要展現的只是數據

因此,數據纔是咱們應該關注的核心,這裏回到咱們MVC的基本概念:

MVC即Model-View-Controller三個詞的縮寫

Model

是數據模型,是客觀事物的一種抽象,好比機票訂單填寫的經常使用聯繫人模塊即可以抽象爲一個Model類,他會有一次航班最多可選擇多少聯繫人這種被當前業務限制的屬性,而且會有增減聯繫人、獲取聯繫人、獲取最大可設置聯繫人等業務數據。

Model應該是一個比較穩定的模塊,不會常常變化而且可被重用的模塊;固然最重要的是,每一次數據變化便會有一個通知機制,通知全部的controller對數據變化作出響應

View

View就是視圖,在前端中甚至可簡單理解爲html模板,Controller會根據數據組裝爲最終的html字符串,而後展現給咱們,至於怎麼展現是CSS的事情,咱們這裏不太關注。

PS:通常來講,過於複雜的if else流程判斷,不該該出如今view中,那是controller該作的事情

固然並非每次model變化controller都須要完整的渲染頁面,也有可能一次model改變,其響應的controller只是操做了一次dom,只要model的controller足夠細分,每一個controller就算是在操做dom也是無所謂的

Controller

控制器其實就是負責與View以及Model打交道的,由於View與Model應該沒有任何交互,model中不會出現html標籤,html標籤也不該該出現完整的model對應數據,更不會有model數據的增刪

PS:html標籤固然須要一些關鍵model值用於controller獲取model相關標誌了

這裏拷貝一個圖示來幫助咱們解析:

這個圖基本能夠表達清楚MVC是幹嗎的,可是卻不能幫助新手很好的瞭解什麼是MVC,由於真實的場景多是這樣的:

一個model實例化完畢,通知controller1去更新了view

view發生了click交互經過controller2改變了model的值

model立刻通知了controller三、controller四、controller5響應數據變化

因此這裏controller影響的model可能不止一個,而model通知的controller也不止一個,會引發的界面連鎖反應,上圖可能會誤導初學者只有一個controller在作這些事情。

這裏舉一個簡單的例子說明狀況:

① 你們看到新浪微博首頁,你發了一條微博,這個時候你關注的好友轉發了該微博

② 服務器響應此次微博,而且將此次新增微博推送給了你(也有多是頁面有一個js不斷輪詢去拉取數據),總之最後數據變了,你的微博Model立刻將此次數據變化通知了至少如下響應程序:

1)消息通知控制器,他引發了右上角消息變化,用戶看見了有人轉發個人weib

2)微博主頁面顯示多了一條微博,讓咱們點擊查看

3)......

這是一條微博新增產生的變化,若是頁面想再多一個模塊響應變化,只須要在微博Model的控制器集合中新增一個控制器便可

MVC的實現

千言不如一碼,我這裏臨時設計一個例子並書寫代碼來講明本身對MVC的認識,,考慮到簡單,便不使用模塊化了,咱們設計了一個博客頁面,大概是這個樣子的:

不管什麼功能,都須要第三方庫,咱們這裏選擇了:

① zepto

② underscore

這裏依舊用到了咱們的繼承機制,若是對這個不熟悉的朋友煩請看看我以前的博客:【一次面試】再談javascript中的繼承

Model的實現

咱們只是數據的搬運工,因此要以數據爲先,這裏先設計了Model的基類:

  1 var AbstractModel = _.inherit({
  2   initialize: function (opts) {
  3     this.propertys();
  4     this.setOption(opts);
  5   },
  6 
  7   propertys: function () {
  8     //只取頁面展現須要數據
  9     this.data = {};
 10 
 11     //局部數據改變對應的響應程序,暫定爲一個方法
 12     //能夠是一個類的實例,若是是實例必須有render方法
 13     this.controllers = {};
 14 
 15     //全局初始化數據時候調用的控制器
 16     this.initController = null;
 17 
 18     this.scope = null;
 19 
 20   },
 21 
 22   addController: function (k, v) {
 23     if (!k || !v) return;
 24     this.controllers[k] = v;
 25   },
 26 
 27   removeController: function (k) {
 28     if (!k) return;
 29     delete this.controllers[k];
 30   },
 31 
 32   setOption: function (opts) {
 33     for (var k in opts) {
 34       this[k] = opts[k];
 35     }
 36   },
 37 
 38   //首次初始化時,須要矯正數據,好比作服務器適配
 39   //@override
 40   handleData: function () { },
 41 
 42   //通常用於首次根據服務器數據源填充數據
 43   initData: function (data) {
 44     var k;
 45     if (!data) return;
 46 
 47     //若是默認數據沒有被覆蓋可能有誤
 48     for (k in this.data) {
 49       if (data[k]) this.data[k] = data[k];
 50     }
 51 
 52     this.handleData();
 53 
 54     if (this.initController && this.get()) {
 55       this.initController.call(this.scope, this.get());
 56     }
 57 
 58   },
 59 
 60   //驗證data的有效性,若是無效的話,不該該進行如下邏輯,而且應該報警
 61   //@override
 62   validateData: function () {
 63     return true;
 64   },
 65 
 66   //獲取數據前,能夠進行格式化
 67   //@override
 68   formatData: function (data) {
 69     return data;
 70   },
 71 
 72   //獲取數據
 73   get: function () {
 74     if (!this.validateData()) {
 75       //須要log
 76       return {};
 77     }
 78     return this.formatData(this.data);
 79   },
 80 
 81   _update: function (key, data) {
 82     if (typeof this.controllers[key] === 'function')
 83       this.controllers[key].call(this.scope, data);
 84     else if (typeof this.controllers[key].render === 'function')
 85       this.controllers[key].render.call(this.scope, data);
 86   },
 87 
 88   //數據跟新後須要作的動做,執行對應的controller改變dom
 89   //@override
 90   update: function (key) {
 91     var data = this.get();
 92     var k;
 93     if (!data) return;
 94 
 95     if (this.controllers[key]) {
 96       this._update(key, data);
 97       return;
 98     }
 99 
100     for (k in this.controllers) {
101       this._update(k, data);
102     }
103   }
104 });
View Code

而後咱們開始設計真正的博客相關model:

 1 //博客的model模塊應該是徹底獨立與頁面的主流層的,而且可複用
 2 var Model = _.inherit(AbstractModel, {
 3   propertys: function () {
 4     this.data = {
 5       blogs: []
 6     };
 7   },
 8   //新增博客
 9   add: function (title, type, label) {
10     //作數據校驗,具體要多嚴格由業務決定
11     if (!title || !type) return null;
12 
13     var blog = {};
14     blog.id = 'blog_' + _.uniqueId();
15     blog.title = title;
16     blog.type = type;
17     if (label) blog.label = label.split(',');
18     else blog.label = [];
19 
20     this.data.blogs.push(blog);
21 
22     //通知各個控制器變化
23     this.update();
24 
25     return blog;
26   },
27   //刪除某一博客
28   remove: function (id) {
29     if (!id) return null;
30     var i, len, data;
31     for (i = 0, len = this.data.blogs.length; i < len; i++) {
32       if (this.data.blogs[i].id === id) {
33         data = this.data.blogs.splice(i, 1)
34         this.update();
35         return data;
36       }
37     }
38     return null;
39   },
40   //獲取全部類型映射表
41   getTypeInfo: function () {
42     var obj = {};
43     var i, len, type;
44     for (i = 0, len = this.data.blogs.length; i < len; i++) {
45       type = this.data.blogs[i].type;
46       if (!obj[type]) obj[type] = 1;
47       else obj[type] = obj[type] + 1;
48     }
49     return obj;
50   },
51   //獲取標籤映射表
52   getLabelInfo: function () {
53     var obj = {}, label;
54     var i, len, j, len1, blog, label;
55     for (i = 0, len = this.data.blogs.length; i < len; i++) {
56       blog = this.data.blogs[i];
57       for (j = 0, len1 = blog.label.length; j < len1; j++) {
58         label = blog.label[j];
59         if (!obj[label]) obj[label] = 1;
60         else obj[label] = obj[label] + 1;
61       }
62     }
63     return obj;
64   },
65   //獲取總數
66   getNum: function () {
67     return this.data.blogs.length;
68   }
69 
70 });

這個時候再附上業務代碼:

 1 var AbstractView = _.inherit({
 2   propertys: function () {
 3     this.$el = $('#main');
 4     //事件機制
 5     this.events = {};
 6   },
 7   initialize: function (opts) {
 8     //這種默認屬性
 9     this.propertys();
10   },
11   $: function (selector) {
12     return this.$el.find(selector);
13   },
14   show: function () {
15     this.$el.show();
16     this.bindEvents();
17   },
18   bindEvents: function () {
19     var events = this.events;
20 
21     if (!(events || (events = _.result(this, 'events')))) return this;
22     this.unBindEvents();
23 
24     // 解析event參數的正則
25     var delegateEventSplitter = /^(\S+)\s*(.*)$/;
26     var key, method, match, eventName, selector;
27 
28     // 作簡單的字符串數據解析
29     for (key in events) {
30       method = events[key];
31       if (!_.isFunction(method)) method = this[events[key]];
32       if (!method) continue;
33 
34       match = key.match(delegateEventSplitter);
35       eventName = match[1], selector = match[2];
36       method = _.bind(method, this);
37       eventName += '.delegateUIEvents' + this.id;
38 
39       if (selector === '') {
40         this.$el.on(eventName, method);
41       } else {
42         this.$el.on(eventName, selector, method);
43       }
44     }
45     return this;
46   },
47 
48   unBindEvents: function () {
49     this.$el.off('.delegateUIEvents' + this.id);
50     return this;
51   }
52 
53 });
View的基類
 1 //頁面主流程
 2 var View = _.inherit(AbstractView, {
 3   propertys: function ($super) {
 4     $super();
 5     this.$el = $('#main');
 6 
 7     //統合頁面全部點擊事件
 8     this.events = {
 9       'click .js_add': 'blogAddAction',
10       'click .js_blog_del': 'blogDeleteAction'
11     };
12 
13     //實例化model而且註冊須要通知的控制器
14     //控制器務必作到職責單一
15     this.model = new Model({
16       scope: this,
17       controllers: {
18         numController: this.numController,
19         typeController: this.typeController,
20         labelController: this.labelController,
21         blogsController: this.blogsController
22       }
23     });
24   },
25   //總博客數
26   numController: function () {
27     this.$('.js_num').html(this.model.getNum());
28   },
29   //分類數
30   typeController: function () {
31     var html = '';
32     var tpl = document.getElementById('js_tpl_kv').innerHTML;
33     var data = this.model.getTypeInfo();
34     html = _.template(tpl)({ objs: data });
35     this.$('.js_type_wrapper').html(html);
36 
37 
38   },
39   //label分類
40   labelController: function () {
41     //這裏的邏輯與type基本一致,可是真實狀況不會這樣
42     var html = '';
43     var tpl = document.getElementById('js_tpl_kv').innerHTML;
44     var data = this.model.getLabelInfo();
45     html = _.template(tpl)({ objs: data });
46     this.$('.js_label_wrapper').html(html);
47 
48   },
49   //列表變化
50   blogsController: function () {
51     console.log(this.model.get());
52     var html = '';
53     var tpl = document.getElementById('js_tpl_blogs').innerHTML;
54     var data = this.model.get();
55     html = _.template(tpl)(data);
56     this.$('.js_blogs_wrapper').html(html);
57   },
58   //添加博客點擊事件
59   blogAddAction: function () {
60     //此處未作基本數據校驗,由於校驗的工做應該model作,好比字數限制,標籤過濾什麼的
61     //這裏只是往model中增長一條數據,事實上這裏還應該寫if預計判斷是否添加成功,略去
62     this.model.add(
63       this.$('.js_title').val(),
64       this.$('.js_type').val(),
65       this.$('.js_label').val()
66     );
67 
68   },
69   blogDeleteAction: function (e) {
70     var el = $(e.currentTarget);
71     this.model.remove(el.attr('data-id'));
72   }
73 });
74 
75 var view = new View();
76 view.show();

完整代碼&示例

  1 <!doctype html>
  2 <html>
  3 <head>
  4   <meta charset="UTF-8">
  5   <title>前端MVC</title>
  6   <script src="zepto.js" type="text/javascript"></script>
  7   <script src="underscore.js" type="text/javascript"></script>
  8   <style>
  9     li {
 10       list-style: none;
 11       margin: 5px 0;
 12     }
 13     fieldset {
 14       margin: 5px 0;
 15     }
 16   </style>
 17 </head>
 18 <body>
 19   <div id="main">
 20     <fieldset>
 21       <legend>文章總數</legend>
 22       <div class="js_num">
 23         0
 24       </div>
 25     </fieldset>
 26     <fieldset>
 27       <legend>分類</legend>
 28       <div class="js_type_wrapper">
 29       </div>
 30     </fieldset>
 31     <fieldset>
 32       <legend>標籤</legend>
 33       <div class="js_label_wrapper">
 34       </div>
 35     </fieldset>
 36     <fieldset>
 37       <legend>博客列表</legend>
 38       <div class="js_blogs_wrapper">
 39       </div>
 40     </fieldset>
 41     <fieldset>
 42       <legend>新增博客</legend>
 43       <ul>
 44         <li>標題 </li>
 45         <li>
 46           <input type="text" class="js_title" />
 47         </li>
 48         <li>類型 </li>
 49         <li>
 50           <input type="text" class="js_type" />
 51         </li>
 52         <li>標籤(逗號隔開) </li>
 53         <li>
 54           <input type="text" class="js_label" />
 55         </li>
 56         <li>
 57           <input type="button" class="js_add" value="新增博客" />
 58         </li>
 59       </ul>
 60     </fieldset>
 61   </div>
 62   <script type="text/template" id="js_tpl_kv">
 63     <ul>
 64       <%for(var k in objs){ %>
 65         <li><%=k %>(<%=objs[k] %>)</li>
 66       <%} %>
 67     </ul>
 68   </script>
 69   <script type="text/template" id="js_tpl_blogs">
 70     <ul>
 71       <%for(var i = 0, len = blogs.length; i < len; i++ ){ %>
 72         <li><%=blogs[i].title %> - <span class="js_blog_del" data-id="<%=blogs[i].id %>">刪除</span></li>
 73       <%} %>
 74     </ul>
 75   </script>
 76   <script type="text/javascript">
 77 
 78     //繼承相關邏輯
 79     (function () {
 80 
 81       // 全局可能用到的變量
 82       var arr = [];
 83       var slice = arr.slice;
 84       /**
 85       * inherit方法,js的繼承,默認爲兩個參數
 86       *
 87       * @param  {function} origin  可選,要繼承的類
 88       * @param  {object}   methods 被建立類的成員,擴展的方法和屬性
 89       * @return {function}         繼承以後的子類
 90       */
 91       _.inherit = function (origin, methods) {
 92 
 93         // 參數檢測,該繼承方法,只支持一個參數建立類,或者兩個參數繼承類
 94         if (arguments.length === 0 || arguments.length > 2) throw '參數錯誤';
 95 
 96         var parent = null;
 97 
 98         // 將參數轉換爲數組
 99         var properties = slice.call(arguments);
100 
101         // 若是第一個參數爲類(function),那麼就將之取出
102         if (typeof properties[0] === 'function')
103           parent = properties.shift();
104         properties = properties[0];
105 
106         // 建立新類用於返回
107         function klass() {
108           if (_.isFunction(this.initialize))
109             this.initialize.apply(this, arguments);
110         }
111 
112         klass.superclass = parent;
113 
114         // 父類的方法不作保留,直接賦給子類
115         // parent.subclasses = [];
116 
117         if (parent) {
118           // 中間過渡類,防止parent的構造函數被執行
119           var subclass = function () { };
120           subclass.prototype = parent.prototype;
121           klass.prototype = new subclass();
122 
123           // 父類的方法不作保留,直接賦給子類
124           // parent.subclasses.push(klass);
125         }
126 
127         var ancestor = klass.superclass && klass.superclass.prototype;
128         for (var k in properties) {
129           var value = properties[k];
130 
131           //知足條件就重寫
132           if (ancestor && typeof value == 'function') {
133             var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/g, '').split(',');
134             //只有在第一個參數爲$super狀況下才須要處理(是否具備重複方法須要用戶本身決定)
135             if (argslist[0] === '$super' && ancestor[k]) {
136               value = (function (methodName, fn) {
137                 return function () {
138                   var scope = this;
139                   var args = [
140                 function () {
141                   return ancestor[methodName].apply(scope, arguments);
142                 }
143               ];
144                   return fn.apply(this, args.concat(slice.call(arguments)));
145                 };
146               })(k, value);
147             }
148           }
149 
150           //此處對對象進行擴展,當前原型鏈已經存在該對象,便進行擴展
151           if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
152             //原型鏈是共享的,這裏處理邏輯要改
153             var temp = {};
154             _.extend(temp, klass.prototype[k]);
155             _.extend(temp, value);
156             klass.prototype[k] = temp;
157           } else {
158             klass.prototype[k] = value;
159           }
160         }
161 
162         //靜態屬性繼承
163         //兼容代碼,非原型屬性也須要進行繼承
164         for (key in parent) {
165           if (parent.hasOwnProperty(key) && key !== 'prototype' && key !== 'superclass')
166             klass[key] = parent[key];
167         }
168 
169         if (!klass.prototype.initialize)
170           klass.prototype.initialize = function () { };
171 
172         klass.prototype.constructor = klass;
173 
174         return klass;
175       };
176 
177     })();
178   </script>
179   <script type="text/javascript">
180     //基類view設計
181     var AbstractView = _.inherit({
182       propertys: function () {
183         this.$el = $('#main');
184         //事件機制
185         this.events = {};
186       },
187       initialize: function (opts) {
188         //這種默認屬性
189         this.propertys();
190       },
191       $: function (selector) {
192         return this.$el.find(selector);
193       },
194       show: function () {
195         this.$el.show();
196         this.bindEvents();
197       },
198       bindEvents: function () {
199         var events = this.events;
200 
201         if (!(events || (events = _.result(this, 'events')))) return this;
202         this.unBindEvents();
203 
204         // 解析event參數的正則
205         var delegateEventSplitter = /^(\S+)\s*(.*)$/;
206         var key, method, match, eventName, selector;
207 
208         // 作簡單的字符串數據解析
209         for (key in events) {
210           method = events[key];
211           if (!_.isFunction(method)) method = this[events[key]];
212           if (!method) continue;
213 
214           match = key.match(delegateEventSplitter);
215           eventName = match[1], selector = match[2];
216           method = _.bind(method, this);
217           eventName += '.delegateUIEvents' + this.id;
218 
219           if (selector === '') {
220             this.$el.on(eventName, method);
221           } else {
222             this.$el.on(eventName, selector, method);
223           }
224         }
225         return this;
226       },
227 
228       unBindEvents: function () {
229         this.$el.off('.delegateUIEvents' + this.id);
230         return this;
231       }
232 
233     });
234 
235     //基類Model設計
236     var AbstractModel = _.inherit({
237       initialize: function (opts) {
238         this.propertys();
239         this.setOption(opts);
240       },
241 
242       propertys: function () {
243         //只取頁面展現須要數據
244         this.data = {};
245 
246         //局部數據改變對應的響應程序,暫定爲一個方法
247         //能夠是一個類的實例,若是是實例必須有render方法
248         this.controllers = {};
249 
250         //全局初始化數據時候調用的控制器
251         this.initController = null;
252 
253         this.scope = null;
254 
255       },
256 
257       addController: function (k, v) {
258         if (!k || !v) return;
259         this.controllers[k] = v;
260       },
261 
262       removeController: function (k) {
263         if (!k) return;
264         delete this.controllers[k];
265       },
266 
267       setOption: function (opts) {
268         for (var k in opts) {
269           this[k] = opts[k];
270         }
271       },
272 
273       //首次初始化時,須要矯正數據,好比作服務器適配
274       //@override
275       handleData: function () { },
276 
277       //通常用於首次根據服務器數據源填充數據
278       initData: function (data) {
279         var k;
280         if (!data) return;
281 
282         //若是默認數據沒有被覆蓋可能有誤
283         for (k in this.data) {
284           if (data[k]) this.data[k] = data[k];
285         }
286 
287         this.handleData();
288 
289         if (this.initController && this.get()) {
290           this.initController.call(this.scope, this.get());
291         }
292 
293       },
294 
295       //驗證data的有效性,若是無效的話,不該該進行如下邏輯,而且應該報警
296       //@override
297       validateData: function () {
298         return true;
299       },
300 
301       //獲取數據前,能夠進行格式化
302       //@override
303       formatData: function (data) {
304         return data;
305       },
306 
307       //獲取數據
308       get: function () {
309         if (!this.validateData()) {
310           //須要log
311           return {};
312         }
313         return this.formatData(this.data);
314       },
315 
316       _update: function (key, data) {
317         if (typeof this.controllers[key] === 'function')
318           this.controllers[key].call(this.scope, data);
319         else if (typeof this.controllers[key].render === 'function')
320           this.controllers[key].render.call(this.scope, data);
321       },
322 
323       //數據跟新後須要作的動做,執行對應的controller改變dom
324       //@override
325       update: function (key) {
326         var data = this.get();
327         var k;
328         if (!data) return;
329 
330         if (this.controllers[key]) {
331           this._update(key, data);
332           return;
333         }
334 
335         for (k in this.controllers) {
336           this._update(k, data);
337         }
338       }
339     });
340 
341   </script>
342   <script type="text/javascript">
343 
344     //博客的model模塊應該是徹底獨立與頁面的主流層的,而且可複用
345     var Model = _.inherit(AbstractModel, {
346       propertys: function () {
347         this.data = {
348           blogs: []
349         };
350       },
351       //新增博客
352       add: function (title, type, label) {
353         //作數據校驗,具體要多嚴格由業務決定
354         if (!title || !type) return null;
355 
356         var blog = {};
357         blog.id = 'blog_' + _.uniqueId();
358         blog.title = title;
359         blog.type = type;
360         if (label) blog.label = label.split(',');
361         else blog.label = [];
362 
363         this.data.blogs.push(blog);
364 
365         //通知各個控制器變化
366         this.update();
367 
368         return blog;
369       },
370       //刪除某一博客
371       remove: function (id) {
372         if (!id) return null;
373         var i, len, data;
374         for (i = 0, len = this.data.blogs.length; i < len; i++) {
375           if (this.data.blogs[i].id === id) {
376             data = this.data.blogs.splice(i, 1)
377             this.update();
378             return data;
379           }
380         }
381         return null;
382       },
383       //獲取全部類型映射表
384       getTypeInfo: function () {
385         var obj = {};
386         var i, len, type;
387         for (i = 0, len = this.data.blogs.length; i < len; i++) {
388           type = this.data.blogs[i].type;
389           if (!obj[type]) obj[type] = 1;
390           else obj[type] = obj[type] + 1;
391         }
392         return obj;
393       },
394       //獲取標籤映射表
395       getLabelInfo: function () {
396         var obj = {}, label;
397         var i, len, j, len1, blog, label;
398         for (i = 0, len = this.data.blogs.length; i < len; i++) {
399           blog = this.data.blogs[i];
400           for (j = 0, len1 = blog.label.length; j < len1; j++) {
401             label = blog.label[j];
402             if (!obj[label]) obj[label] = 1;
403             else obj[label] = obj[label] + 1;
404           }
405         }
406         return obj;
407       },
408       //獲取總數
409       getNum: function () {
410         return this.data.blogs.length;
411       }
412 
413     });
414 
415     //頁面主流程
416     var View = _.inherit(AbstractView, {
417       propertys: function ($super) {
418         $super();
419         this.$el = $('#main');
420 
421         //統合頁面全部點擊事件
422         this.events = {
423           'click .js_add': 'blogAddAction',
424           'click .js_blog_del': 'blogDeleteAction'
425         };
426 
427         //實例化model而且註冊須要通知的控制器
428         //控制器務必作到職責單一
429         this.model = new Model({
430           scope: this,
431           controllers: {
432             numController: this.numController,
433             typeController: this.typeController,
434             labelController: this.labelController,
435             blogsController: this.blogsController
436           }
437         });
438       },
439       //總博客數
440       numController: function () {
441         this.$('.js_num').html(this.model.getNum());
442       },
443       //分類數
444       typeController: function () {
445         var html = '';
446         var tpl = document.getElementById('js_tpl_kv').innerHTML;
447         var data = this.model.getTypeInfo();
448         html = _.template(tpl)({ objs: data });
449         this.$('.js_type_wrapper').html(html);
450 
451 
452       },
453       //label分類
454       labelController: function () {
455         //這裏的邏輯與type基本一致,可是真實狀況不會這樣
456         var html = '';
457         var tpl = document.getElementById('js_tpl_kv').innerHTML;
458         var data = this.model.getLabelInfo();
459         html = _.template(tpl)({ objs: data });
460         this.$('.js_label_wrapper').html(html);
461 
462       },
463       //列表變化
464       blogsController: function () {
465         console.log(this.model.get());
466         var html = '';
467         var tpl = document.getElementById('js_tpl_blogs').innerHTML;
468         var data = this.model.get();
469         html = _.template(tpl)(data);
470         this.$('.js_blogs_wrapper').html(html);
471       },
472       //添加博客點擊事件
473       blogAddAction: function () {
474         //此處未作基本數據校驗,由於校驗的工做應該model作,好比字數限制,標籤過濾什麼的
475         //這裏只是往model中增長一條數據,事實上這裏還應該寫if預計判斷是否添加成功,略去
476         this.model.add(
477       this.$('.js_title').val(),
478       this.$('.js_type').val(),
479       this.$('.js_label').val()
480     );
481 
482       },
483       blogDeleteAction: function (e) {
484         var el = $(e.currentTarget);
485         this.model.remove(el.attr('data-id'));
486       }
487     });
488 
489     var view = new View();
490     view.show();
491 
492   </script>
493 </body>
494 </html>
View Code

http://sandbox.runjs.cn/show/bvux03nx

分析

這裏註釋寫的很詳細,例子也很簡單很完整,其實並不須要太多的分析,對MVC還不太理解的朋友能夠換本身方式實現以上代碼,而後再加入評論模塊,或者其它模塊後,體會下開發難度,而後再用這種方式開發試試,體會不一樣才能體會真理,道不證不明嘛,這裏的代碼組成爲:

① 公共的繼承方法

② 公共的View抽象類,主要來講完成了view的事件綁定功能,能夠將全部click事件所有寫在events中

PS:這個view是我閹割便於各位理解的view,真實狀況會比較複雜

③ 公共的Model抽象類,主要完成model的骨架相關,其中比較關鍵的是update後的通知機制

④ 業務model,這個是關於博客model的功能體現,單純的數據操做

⑤ 業務View,這個爲類實例化後執行了show方法,便綁定了各個事件

這裏以一次博客新增爲例說明一下程序流程:

① 用戶填好數據後,點擊增長博客,會觸發相應js函數

② js獲取文本框數據,爲model新增數據

③ model數據變化後,分發事件通知各個控制器響應變化

④ 各個controller執行,並根據model產生view的變化

好了,這個例子就到此爲止,但願對幫助各位瞭解MVC有所幫助

優點與不足

對於移動端的頁面來講,一個頁面對應着一個View.js,即上面的業務View,其中model能夠徹底的分離出來,若是以AMD模塊化的作法的話,View.js的體積會很是小,而主要邏輯又基本拆分到了Model業務中,controller作的工做因爲前端模板的介入反而變得簡單

不足之處,即是全部的controller所有綁定到了view上,交互的觸發點也所有在view身上,而更好的作法,多是組件化,可是這類模塊包含太多業務數據,作成組件化彷佛重用性不高,因而就有了業務組件的誕生。

業務組件&公共頻道

所謂業務組件或者公共頻道都是網站上了必定規模會實際遇到的問題,我這裏舉一個例子:

最初咱們是作機票項目因而目錄結構爲:

blade 框架目錄

flight 機票業務頻道

static 公共樣式文件

而後逐漸咱們多了酒店項目以及用車項目目錄結構變成了:

blade 框架目錄

car 用車頻道

hotel 酒店頻道

flight 機票業務頻道

static 公共樣式文件

因而一個比較實際的問題出現了,最初機票頻道的城市列表模塊以及登陸模塊與經常使用聯繫人模塊好像其餘兩個頻道也能用,可是問題也出現了:

① 將他們抽離爲UI組件,但他們又帶有業務數據

② 其它兩個頻道並不想引入機票頻道的模塊配置,並且也不信任機票頻道

這個時候便會出現一個叫公共頻道的東西,他完成的工做與框架相似,可是他會涉及到業務數據,而且除了該公司,也許便不能重用:

blade 框架目錄

common 公共頻道

car 用車頻道

hotel 酒店頻道

flight 機票業務頻道

static 公共樣式文件

各個業務頻道引入公共頻道的產品即可解決重用問題,但這樣也同時發生了耦合,若是公共頻道的頁面作的不夠靈活可配置,業務團隊使用起來會是一個噩夢!

因而更好的方案彷佛是頁面模塊化,儘量的將頁面分爲一個個可重用的小模塊,有興趣的朋友請到這裏看看:

【前端優化之拆分CSS】前端三劍客的分分合合

【shadow dom入UI】web components思想如何應用於實際項目

網站慢了

關於系統優化的建議我以前寫了不少文章,有興趣的朋友能夠移駕至這裏看看:

淺談移動前端的最佳實踐

我這裏補充一點業務優化點:

① ajax請求剝離無心義的請求,命名使用短拼

這條比較適用於新團隊,服務器端的同事並不會關注網絡請求的耗時,因此請求每每又臭又長,一個真實的例子就是,上週我推進服務器端同事將城市列表的無心義字段刪除後容量由90k降到了50k,而且還有優化空間!!!

② 工程化打包時候最好採用MD5的方式,這樣可作到比較舒服的application cache效果,十分推崇!

③ ......

結語&核心點

半年了,項目由最初的無趣到如今能夠在上面玩MVC、玩ABTesting等高端東西了,而看着產品訂單破一,破百,破千,破萬,雖然很累,可是這個時候仍是以爲是值得的。

只惋惜我廠的一些制度有點過於噁心,跨團隊交流跟吃屎同樣,工做量過大,工資又低,這些點滴仍是讓人感到失望的。

好了,抱怨結束,文章淺談了一些本身對移動端從0到1作業務開發的一些經驗及建議,沒有什麼高深的知識,也許還有不少錯誤的地方,請各位不吝賜教,多多指點,接下來時間學習的重點應該仍是IOS,偶爾會穿插MVVM框架(angularJS等)的相關學習,有興趣的朋友能夠一塊兒關注,也但願本身儘快打通端到端吧,突破自身瓶頸。

最後,個人微博粉絲及其少,若是您以爲這篇博客對您哪怕有一絲絲的幫助,微博求粉博客求贊!!!

相關文章
相關標籤/搜索