不知不覺來百度已有半年之久,這半年是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投放人員須要確切的知道某個投放詞天天的訂單量,這個時候上面的參數可能就要變化了:
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框架,項目目錄結構爲:
其中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 });
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 });
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 });
真實的使用場景業務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,若是一個頁面邏輯愈來愈複雜,若是一個頁面的代碼你以爲很差維護了,那麼意味着,他應該獲得應有的重構了!
若是在你的頁面(會長久維護的項目)中有如下狀況的話,也許你應該重構你的頁面或者換掉你框架了:
① 在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這個東西有點懸,通常人壓根都不知道他是幹嗎的,就知道一個model-view-controller;
知道一點的又說不清楚;
真正懂的人要麼喜歡東扯西扯,要麼不肯意寫博客或者博客一來便很難,曲高和寡。
因此前端MVC這個東西一直是一個玄之又玄的東西,不少開發了好久的朋友都不能瞭解什麼是MVC。
今天我做爲一個自認爲懂得一點的人,便來講一說我對MVC在前端的認識,但願對你們有幫助。
前端給你們的認識即是頁面,頁面由HTML+CSS實現,若是有交互便須要JS的介入,其中:
對於真實的業務來講,HTML&CSS是零件,JS是搬運工,數據是設計圖與指令。
JS要根據數據指令將零件組裝爲玩具,用戶操做了玩具致使了數據變化,因而JS又根據數據指令從新組裝玩具
咱們事實上不寫代碼,咱們只是數據的搬運工
上述例子可能不必定準確,但他能夠表達一些中心思想,那就是:
對於頁面來講,要展現的只是數據
因此,數據纔是咱們應該關注的核心,這裏回到咱們MVC的基本概念:
MVC即Model-View-Controller三個詞的縮寫
是數據模型,是客觀事物的一種抽象,好比機票訂單填寫的經常使用聯繫人模塊即可以抽象爲一個Model類,他會有一次航班最多可選擇多少聯繫人這種被當前業務限制的屬性,而且會有增減聯繫人、獲取聯繫人、獲取最大可設置聯繫人等業務數據。
Model應該是一個比較穩定的模塊,不會常常變化而且可被重用的模塊;固然最重要的是,每一次數據變化便會有一個通知機制,通知全部的controller對數據變化作出響應
View就是視圖,在前端中甚至可簡單理解爲html模板,Controller會根據數據組裝爲最終的html字符串,而後展現給咱們,至於怎麼展現是CSS的事情,咱們這裏不太關注。
PS:通常來講,過於複雜的if else流程判斷,不該該出如今view中,那是controller該作的事情
固然並非每次model變化controller都須要完整的渲染頁面,也有可能一次model改變,其響應的controller只是操做了一次dom,只要model的controller足夠細分,每一個controller就算是在操做dom也是無所謂的
控制器其實就是負責與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的認識,,考慮到簡單,便不使用模塊化了,咱們設計了一個博客頁面,大概是這個樣子的:
不管什麼功能,都須要第三方庫,咱們這裏選擇了:
① zepto
② underscore
這裏依舊用到了咱們的繼承機制,若是對這個不熟悉的朋友煩請看看我以前的博客:【一次面試】再談javascript中的繼承
咱們只是數據的搬運工,因此要以數據爲先,這裏先設計了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 });
而後咱們開始設計真正的博客相關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 });
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>
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 公共樣式文件
各個業務頻道引入公共頻道的產品即可解決重用問題,但這樣也同時發生了耦合,若是公共頻道的頁面作的不夠靈活可配置,業務團隊使用起來會是一個噩夢!
因而更好的方案彷佛是頁面模塊化,儘量的將頁面分爲一個個可重用的小模塊,有興趣的朋友請到這裏看看:
【shadow dom入UI】web components思想如何應用於實際項目
關於系統優化的建議我以前寫了不少文章,有興趣的朋友能夠移駕至這裏看看:
我這裏補充一點業務優化點:
① ajax請求剝離無心義的請求,命名使用短拼
這條比較適用於新團隊,服務器端的同事並不會關注網絡請求的耗時,因此請求每每又臭又長,一個真實的例子就是,上週我推進服務器端同事將城市列表的無心義字段刪除後容量由90k降到了50k,而且還有優化空間!!!
② 工程化打包時候最好採用MD5的方式,這樣可作到比較舒服的application cache效果,十分推崇!
③ ......
半年了,項目由最初的無趣到如今能夠在上面玩MVC、玩ABTesting等高端東西了,而看着產品訂單破一,破百,破千,破萬,雖然很累,可是這個時候仍是以爲是值得的。
只惋惜我廠的一些制度有點過於噁心,跨團隊交流跟吃屎同樣,工做量過大,工資又低,這些點滴仍是讓人感到失望的。
好了,抱怨結束,文章淺談了一些本身對移動端從0到1作業務開發的一些經驗及建議,沒有什麼高深的知識,也許還有不少錯誤的地方,請各位不吝賜教,多多指點,接下來時間學習的重點應該仍是IOS,偶爾會穿插MVVM框架(angularJS等)的相關學習,有興趣的朋友能夠一塊兒關注,也但願本身儘快打通端到端吧,突破自身瓶頸。
最後,個人微博粉絲及其少,若是您以爲這篇博客對您哪怕有一絲絲的幫助,微博求粉博客求贊!!!