【組件化開發】前端進階篇之如何編寫可維護可升級的代碼

前言

我還在攜程的作業務的時候,每一個看似簡單的移動頁面背後每每會隱藏5個以上的數據請求,其中最過複雜的當屬機票與酒店的訂單填寫業務代碼javascript

這裏先看看比較「簡單」的機票代碼:css

而後看看稍微複雜的酒店業務邏輯:html

機票一個頁面的代碼量達到了5000行代碼,而酒店的代碼居然超過了8000行,這裏還不包括模板(html)文件!!!前端

而後初略看了機票的代碼,就該頁面可能發生的接口請求有19個之多!!!而酒店的的交互DOM事件基本多到了使人髮指的地步:java

固然,機票團隊的交互DOM事件已經多到了我筆記本不能截圖了:ios

 1 events: {
 2 'click .js_check_invoice_type': 'checkInvoiceType', //切換髮票類型
 3 'click .flight-hxtipshd': 'huiXuanDesc', //惠選說明
 4 'click .js_ListReload': 'hideNetError',
 5 'click #js_return': 'backAction', //返回列表頁
 6 'click div[data-rbtType]': 'showRebate', //插爛返現說明
 7 'click #paybtn .j_btn': 'beforePayAction', //提交訂單                        //flightDetailsStore, passengerQueryStore, mdStore, postAddressStorage, userStore, flightDeliveryStore
 8 'click .flight-loginbtn2': 'bookLogin', //登陸
 9 'input #linkTel': 'setContact', //保存用戶輸入的聯繫人
10 'click #addPassenger .flight-labq': 'readmeAction',//姓名幫助
11 'click .jsDelivery': 'selDelivery', //選擇配送方式
12 'click #jsViewCoupons': 'viewCoupons', //查看消費券使用說明                                                  //flightDetailsStore
13 // 'click .j_refundPolicy': 'fanBoxAction', //查看返現信息
14 //'click .flight-bkinfo-tgq .f-r': 'tgBoxAction', //查看退改簽
15 'click .js_del_tab': 'showDelListUI', //配送方式
16 //            'click .js_del_cost .flight-psf i': 'selectPaymentType', // 選擇快遞費用方式
17 'click #js_addrList': 'AddrListAction', //選擇地址
18 'click #date-picker': 'calendarAction', //取票日期                                                                    //airportDeliveryStore
19 'click #done-address': 'zqinairselect', //取票櫃檯
20 'click #selectCity': 'selectCityAction', //選擇城市
21 'click #date-zqtime': 'showZqTimeUI', //取票時間                                                                        //airportDeliveryStore
22 'click #jsinsure': 'viewInsure', //保險說明
23 'click #js_invoice_title': 'inTitleChangeWrp', //發票擡頭更改                // userStore, flightOrderInfoInviceStore, flightOrderStore    //don't move outside
24 'click #js_invoice_title_div': 'inTitleChangeWrp',
25 'click .flight-icon-arrrht': 'showinTitleList', //‘+’號,跳轉發票擡頭列表                 //userStore, invoiceURLStore
26 'focusin #linkTel': 'telInput',
27 'focusout #linkTel': 'telInputFinish',
28 'touchstart input': 'touchStartAction', // 處理Android手機上點擊不靈敏問題
29 'click #package .flight-arrrht': 'packageSelect',
30 'focusin input': 'hideErrorTips',
31 'click #dist_text_div': 'hideErrorTips',
32 'click .j_PackageNotice': 'toggletips',
33 'click .j_AnnouncementNotice': 'toggleNotice',
34 'click #travalPackageDesc': 'forwardToTravalPackage',       //don't move into child modules
35 'click #airInsureDesc': 'showAirInsureDesc',
36 'click #paybtn': 'orderDetailAction',//價格明細
37 'click .J_retriveVerifyCodeBtn': 'getVerifyCode',
38 'click .J_toPay': 'toPayAction',
39 'click .J_closeVerifyCode': 'closeVerifyCodePopup',
40 'keyup .J_verifyCodePopup input': 'setToPayBtnStatus',
41 'click .js_flight_seat': 'selectRecommendCabin', // 選擇推薦倉位
42 'click .j_changeFlight': 'changeFlightAction', // 推薦航班彈層中更改航班
43 'focusin input:not([type=tel])': 'adjustInputPosition', // iphone5/5s ios8搜狗輸入法遮住input
44 'click .js_addr,#js_addr_div': 'editDeliverAddress',//報銷憑證,詳細地址編輯
45 'click .js_showUserInfo': 'showUserInfo', // add by hkhu v2.5.9
46 'click #logout': 'logout', // add by hkhu v2.5.9
47 'click #gotoMyOrder': 'gotoMyOrder', // add by hkhu v2.5.9
48 'touchstart #logout': function (e) { $(e.currentTarget).addClass('current'); },
49 'touchstart #gotoMyOrder': function (e) { $(e.currentTarget).addClass('current'); },
50 'click .js_buddypayConfirm': 'buddypayConfirmed',
51 'click .js_pickupTicket': 'viewPickUp', //261接送機券說明
52 'click .flt-bking-logintips': 'closelogintips'//關閉接送機券提示
53 },
View Code

就這種體量的頁面,若是須要迭代需求、打BUG補丁的話,我敢確定的說,一個BUG的修復很容易引發其它BUG,而上面還僅僅是其中一個業務頁面,後面還有強大而複雜的前端框架呢!如此複雜的前端代碼維護工做可不是開玩笑的!git

PS:說道此處,不得不爲攜程的前端水平點個贊,業內少有的單頁應用,一套代碼H5&Hybrid同時運行不說,還解決了SEO問題,嗯,很贊。github

如何維護這種頁面,如何設計這種頁面是咱們今天討論的重點,而上述是攜程合併後的代碼,他們兩個團隊的設計思路不便在此處展開。web

今天,我這裏提供一個思路,認真閱讀此文可能在如下方面對你有所幫助:面試

1 ① 如何將一個複雜的頁面拆分爲一個個獨立的頁面組件模塊
2 ② 如何將分拆後的業務組件模塊從新合爲一個完整的頁面
3 ③ 從重構角度看組件化開發帶來的好處
4 從前端優化的角度看待組件化開發

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

因爲該項目涉及到了項目拆分與合併,基本屬於一個完整的前端工程化案例了,因此將之放到了github上:https://github.com/yexiaochai/mvc

其中工程化一塊的代碼,後續會由另外一位小夥伴持續更新,若是該文對各位有所幫助的話請各位給項目點個贊、加顆星:)

我相信若是是中級水平的前端,認真閱讀此文必定會對你有一點幫助滴。

一個實際的場景

演示地址

http://yexiaochai.github.io/mvc/webapp/bus/list.html

代碼倉促,可能會有BUG哦:)

代碼地址:https://github.com/yexiaochai/mvc/

頁面基本構成

由於訂單填寫頁通常有密度,我這裏挑選相對複雜而又沒有密度的產品列表頁來作說明,其中框架以及業務代碼已經作過抽離,不會包含敏感信息,一些優化後續會同步到開源blade框架中去。

咱們這裏列表頁的首屏頁面以下:

簡單來講組成以下:

① 框架級別UI組件UIHeader,頭部組件

② 點擊日期會出框架級別UI,日曆組件UICalendar

③ 點擊出發時段、出發汽車站、到達汽車站,皆會出框架級別UI

④ header下面的日期工具欄須要做爲獨立的業務模塊

⑤ 列表區域能夠做爲獨立的業務模塊,可是與主業務靠太近,不太適合

⑥ 出發時段、出發汽車站、到達汽車站皆是獨立的業務模塊

一個頁面被咱們拆分紅了若干個小模塊,咱們只須要關注模塊內部的交互實現,而包括業務模塊的通訊,業務模塊的樣式,業務模塊的重用,暫時有如下約定:

① 單個頁面的樣式所有寫在一個文件中,好比list裏面全部模塊對應的是list.css
② 模塊之間採用觀察者模式觀察數據實體變化,以數據爲媒介通訊
③ 通常來講業務模塊不可重用,若是有重用的模塊,須要分離到common目錄中,由於咱們今天不考慮common重用,這塊暫時不予理睬

這裏有些朋友可能認爲單個模塊的CSS以及image也應該參與獨立,我這裏不太贊成,業務頁面樣式粒度太細的話會給設計帶來不小的麻煩,這裏再以通俗的話來講:尼瑪,我CSS功底通常,拆分的太細,對我來講難度過高......

很差的作法

很差的這個事情實際上是相對的,由於很差的作法通常是比較簡單的作法,對於一次性項目或者業務比較簡單的頁面來講反而是好的作法,好比這裏的業務邏輯能夠這樣寫:

 1 define(['AbstractView', 'list.layout.html', 'list.html', 'BusModel', 'BusStore', 'UICalendarBox', 'UILayerList', 'cUser', 'UIToast'],
 2 function (AbstractView, layoutHtml, listTpl, BusModel, BusStore, UICalendarBox, UILayerList, cUser, UIToast) {
 3     return _.inherit(AbstractView, {
 4         propertys: function ($super) {
 5             $super();
 6             //一堆基礎屬性定義
 7             //......
 8             //交互業務邏輯
 9             this.events = {
10                 'click .js_pre_day': 'preAction', //點擊前一天觸發
11                 'click .js_next_day': 'nextAction', //點擊後一天觸發
12                 'click .js_bus_list li': 'toBooking', //點擊列表項目觸發
13                 'click .js_show_calendar': 'showCalendar', //點擊日期項出日曆組件
14                 'click .js_show_setoutdate': 'showSetoutDate', //篩選出發時段
15                 'click .js_show_setstation': 'showStation', //篩選出發站
16                 'click .js_show_arrivalstation': 'showArrivalStation', //篩選到達站
17                 //迭代需求,增長其它頻道入口
18                 'click .js-list-tip': function () {}
19             };
20         },
21         //初始化頭部標題欄
22         initHeader: function (t) { },
23         //首次dom渲染後,初始化後續會用到的全部dom元素,以避免重複獲取
24         initElement: function () {},
25         showSetoutDate: function () {},
26         showStation: function () {},
27         showArrivalStation: function () {},
28         showCalendar: function () {},
29         preAction: function (e) {},
30         nextAction: function () {},
31         toBooking: function (e) {},
32         listInit: function () {},
33         bindScrollEvent: function () {},
34         unbindScrollEvent: function () { },
35         addEvent: function () {
36             this.on('onShow', function () {
37                 //當頁面渲染結束,須要作的初始化操做,好比渲染頁面
38                 this.listInit();
39                 //......
40             });
41             this.on('onHide', function () {
42                 this.unbindScrollEvent();
43             });
44         }
45     });
46 });

根據以前的經驗,若是僅僅包含這些業務邏輯,這樣寫代碼問題不是很是大,代碼量預計在800行左右,可是爲了完成完整的業務邏輯,咱們這裏立刻產生了新的需求。

需求迭代

由於我這裏的班次列表,最初是沒有URL參數,因此根本沒法產出班次列表,頁面上全部組件模塊都是擺設,因而這裏新增一個需求:

當url沒有出發-到達相關參數信息時,默認彈出出發城市到達城市選擇框

因而,咱們這裏會新增一個簡單的彈出層:

這個看似簡單的彈出層,背後卻隱藏了一個巨大的陷阱,由於點擊出發或者到達時會出城市列表,而城市列表自己就是一個比較複雜的業務:

因而頁面的組成發生了改變:

① 自己業務邏輯約800行代碼

② 新增出發到達篩選彈出層

③ 出發城市頁面,預計300行代碼

而彈出層的新增對業務自己形成了深遠的影響,原本url是不帶有業務參數的,可是點擊了彈出層的肯定按鈕,須要改變URL參數,而且刷新自己頁面的數據,因而簡單的一個彈出層新增直接將頁面的複雜程度提高了一倍。

因而該頁面代碼輕輕鬆鬆破千了,後續需求迭代js代碼量破2000僅僅是時間問題,到時候維護便複雜了,頁面複雜無規律的DOM操做將會令你焦頭爛額,這個時候組件化開發的優點便得以體現了,因而下面進入組件化開發的設計。

準備工做

整體架構

此次的代碼依賴於blade骨架,包括:

① MVC模塊,完成經過url獲取正確的page控制器,從而經過view.js完成渲染頁面的功能

② 數據請求模塊,完成接口請求

全站依賴於javascript的繼承功能,詳情見:【一次面試】再談javascript中的繼承,若是不太瞭解面向對象編程,文中代碼可能會有點吃力,也請各位多多瞭解。

整體業務架構如圖:

框架架構圖:

下面分別介紹下各個模塊,幫助各位在下文中能更好的瞭解代碼,首先是基本MVC的介紹,這裏請參考我這篇文章:簡單的MVC介紹

全局控制器

其實控制器可謂是變化萬千的一個對象,對於服務器端來講,控制器完成的功能是將本次請求分發到具體的代碼模塊,由代碼模塊處理後返回字符串給前端;

對於請求已經來到瀏覽器的前端來講,根據此次請求URL(或者其它判斷條件),判斷該次請求應該由哪一個前端js控制器執行,這是前端控制器乾的事情;

當真的此次處理邏輯進入一個具體的page後,這個page事實上也能夠做爲一個控制器存在......

咱們這裏的控制器,主要完成根據當前請求實例化View的功能,而且會提供一些view級別但願單例使用的接口:

  1 define([
  2   'UIHeader',
  3   'UIToast',
  4   'UILoading',
  5   'UIPageView',
  6   'UIAlert'
  7 ], function (UIHeader, UIToast, UILoading, UIPageView, UIAlert) {
  8 
  9     return _.inherit({
 10         propertys: function () {
 11             //view搜索目錄
 12             this.viewRootPath = 'views/';
 13 
 14             //默認view
 15             this.defaultView = 'index';
 16 
 17             //當前視圖路徑
 18             this.viewId;
 19             this.viewUrl;
 20 
 21             //視圖集
 22             this.views = {};
 23 
 24             //是否開啓單頁應用
 25             //      this.isOpenWebapp = _.getHybridInfo().platform == 'baidubox' ? true : false;
 26             this.isOpenWebapp = false;
 27 
 28             this.viewMapping = {};
 29 
 30             //UIHeader須要釋放出來
 31             this.UIHeader = UIHeader;
 32 
 33             this.interface = [
 34                 'forward',
 35                 'back',
 36                 'jump',
 37                 'showPageView',
 38                 'hidePageView',
 39                 'showLoading',
 40                 'hideLoading',
 41                 'showToast',
 42                 'hideToast',
 43                 'showMessage',
 44                 'hideMessage',
 45                 'showConfirm',
 46                 'hideConfirm',
 47                 'openWebapp',
 48                 'closeWebapp'
 49             ];
 50 
 51         },
 52 
 53         initialize: function (options) {
 54             this.propertys();
 55             this.setOption(options);
 56             this.initViewPort();
 57             this.initAppMapping();
 58 
 59             //開啓fastclick
 60             $.bindFastClick && $.bindFastClick();
 61 
 62         },
 63 
 64         setOption: function (options) {
 65             _.extend(this, options);
 66         },
 67 
 68         //建立dom結構
 69         initViewPort: function () {
 70 
 71             this.d_header = $('#headerview');
 72             this.d_state = $('#js_page_state');
 73             this.d_viewport = $('#main');
 74 
 75             //實例化全局使用的header,這裏好像有點不對
 76             this.header = new this.UIHeader({
 77                 wrapper: this.d_header
 78             });
 79 
 80             //非共享資源,這裏應該引入app概念了
 81             this.pageviews = {};
 82             this.toast = new UIToast();
 83             this.loading = new UILoading();
 84             this.alert = new UIAlert();
 85             this.confirm = new UIAlert();
 86         },
 87 
 88         openWebapp: function () {
 89             this.isOpenWebapp = true;
 90         },
 91 
 92         closeWebapp: function () {
 93             this.isOpenWebapp = false;
 94         },
 95 
 96         showPageView: function (name, _viewdata_, id) {
 97             var view = null, k, scope = this.curViewIns || this;
 98             if (!id) id = name;
 99             if (!_.isString(name)) return;
100             //    for (k in _viewdata_) {
101             //      if (_.isFunction(_viewdata_[k])) _viewdata_[k] = $.proxy(_viewdata_[k], scope);
102             //    }
103             view = this.pageviews[id];
104             var arr = name.split('/');
105             var getViewPath = window.getViewPath || window.GetViewPath;
106             if (!view) {
107                 view = new UIPageView({
108                     // bug fixed by zzx
109                     viewId: arr[arr.length - 1] || name,
110                     viewPath: getViewPath ? getViewPath(name) : name,
111                     _viewdata_: _viewdata_,
112                     onHide: function () {
113                         scope.initHeader();
114                     }
115                 });
116                 this.pageviews[id] = view;
117             } else {
118                 view.setViewData(_viewdata_);
119             }
120             view.show();
121 
122         },
123 
124         hidePageView: function (name) {
125             if (name) {
126                 if (this.pageviews[name]) this.pageviews[name].hide();
127             } else {
128                 for (var k in this.pageviews) this.pageviews[k].hide();
129             }
130         },
131 
132         showLoading: function () {
133             this.loading.show();
134         },
135 
136         hideLoading: function () {
137             this.loading.hide();
138         },
139 
140         showToast: function (msg, callback) {
141             this.toast.resetDefaultProperty();
142             this.toast.content = msg;
143             if (callback) this.toast.hideAction = callback;
144             this.toast.refresh();
145             this.toast.show();
146         },
147 
148         hideToast: function () {
149             this.toast.hide();
150         },
151 
152         showMessage: function (param) {
153             if (_.isString(param)) {
154                 param = { content: param };
155             }
156 
157             this.alert.resetDefaultProperty();
158             this.alert.setOption(param);
159             this.alert.refresh();
160             this.alert.show();
161         },
162 
163         hideMessage: function () {
164             this.alert.hide();
165         },
166 
167         showConfirm: function (params) {
168             if (!params) params = {};
169             if (typeof params == 'string') {
170                 params = {
171                     content: params
172                 };
173             }
174 
175             this.confirm.resetDefaultProperty();
176 
177             //與showMessage不同的地方
178             this.confirm.btns = [
179               { name: '取消', className: 'cm-btns-cancel js_cancel' },
180               { name: '肯定', className: 'cm-btns-ok js_ok' }
181             ];
182             this.confirm.setOption(params);
183             this.confirm.refresh();
184             this.confirm.show();
185         },
186 
187         hideConfirm: function () {
188             this.confirm.hide();
189         },
190 
191         //初始化app
192         initApp: function () {
193 
194             //首次加載不須要走路由控制
195             this.loadViewByUrl();
196 
197             //後面的加載所有要通過路由處理
198             if (this.isOpenWebapp === true)
199                 $(window).on('popstate.app', $.proxy(this.loadViewByUrl, this));
200 
201         },
202 
203         loadViewByUrl: function (e) {
204             this.hidePageView();
205 
206             var url = decodeURIComponent(location.href).toLowerCase();
207             var viewId = this.getViewIdRule(url);
208 
209             viewId = viewId || this.defaultView;
210             this.viewId = viewId;
211             this.viewUrl = url;
212             this.switchView(this.viewId);
213 
214         },
215 
216         //@override
217         getViewIdRule: function (url) {
218             var viewId = '', hash = '';
219             var reg = /webapp\/.+\/(.+)\.html/;
220 
221             var match = url.match(reg);
222             if (match && match[1]) viewId = match[1];
223 
224             return viewId;
225         },
226 
227         //@override
228         setUrlRule: function (viewId, param, replace, project) {
229             var reg = /(webapp\/.+\/)(.+)\.html/;
230             var url = window.location.href;
231             var match = url.match(reg);
232             var proj = project ? 'webapp/' + project : match[1];
233             var preUrl = '', str = '', i = 0, _k, _v;
234             //這裏這樣作有點過於業務了 *bug*
235             var keepParam = [
236               'us'
237             ], p;
238             if (!viewId) return;
239             if (!match || !match[1]) {
240                 preUrl = url + '/webapp/bus/' + viewId + '.html';
241             } else {
242                 preUrl = url.substr(0, url.indexOf(match[1])) + proj + viewId + '.html'; ;
243             }
244 
245             //特定的參數將會一直帶上去,渠道、來源等標誌
246             for (i = 0; i < keepParam.length; i++) {
247                 p = keepParam[i];
248                 if (_.getUrlParam()[p]) {
249                     if (!param) param = {};
250                     param[p] = _.getUrlParam()[p];
251                 }
252             }
253 
254             i = 0;
255 
256             for (k in param) {
257                 _k = encodeURIComponent(_.removeAllSpace(k));
258                 _v = encodeURIComponent(_.removeAllSpace(param[k]));
259                 if (i === 0) {
260                     str += '?' + _k + '=' + _v;
261                     i++;
262                 } else {
263                     str += '&' + _k + '=' + _v;
264                 }
265             }
266 
267             url = preUrl + str;
268 
269             if (this.isOpenWebapp === false) {
270                 window.location = url;
271                 return;
272             }
273 
274             if (replace) {
275                 history.replaceState('', {}, url);
276             } else {
277                 history.pushState('', {}, url);
278             }
279 
280         },
281 
282         switchView: function (id) {
283 
284             var curView = this.views[id];
285 
286             //切換前的當前view,立刻會隱藏
287             var tmpView = this.curView;
288 
289             if (tmpView && tmpView != curView) {
290                 this.lastView = tmpView;
291             }
292 
293             //加載view樣式,權宜之計
294             //      this.loadViewStyle(id);
295 
296             //若是當前view存在,則執行請onload事件
297             if (curView) {
298 
299                 //若是當前要跳轉的view就是當前view的話便不予處理
300                 //這裏具體處理邏輯要改*************************************
301                 if (curView == this.curView) {
302                     return;
303                 }
304 
305                 this.curView = curView;
306                 this.curView.show();
307                 this.lastView && this.lastView.hide();
308             } else {
309 
310                 //        this.showLoading();
311                 this.loadView(id, function (View) {
312                     //每次加載結束將狀態欄隱藏,這個代碼要改
313                     //          this.hideLoading();
314 
315                     this.curView = new View({
316                         viewId: id,
317                         refer: this.lastView ? this.lastView.viewId : null,
318                         APP: this,
319                         wrapper: this.d_viewport
320                     });
321 
322                     //設置網頁上的view標誌
323                     this.curView.$el.attr('page-url', id);
324 
325                     //保存至隊列
326                     this.views[id] = this.curView;
327 
328                     this.curView.show();
329                     this.lastView && this.lastView.hide();
330 
331                 });
332             }
333         },
334 
335         //加載view
336         loadView: function (path, callback) {
337             var self = this;
338             requirejs([this.buildUrl(path)], function (View) {
339                 callback && callback.call(self, View);
340             });
341         },
342 
343         //override
344         //配置可能會有的路徑擴展,爲Hybrid與各個渠道作適配
345         initAppMapping: function () {
346             //            console.log('該方法必須被重寫');
347         },
348 
349         //@override
350         buildUrl: function (path) {
351             var mappingPath = this.viewMapping[path];
352             return mappingPath ? mappingPath : this.viewRootPath + '/' + path + '/' + path;
353         },
354 
355         //此處須要一個更新邏輯,好比在index view再點擊到index view不會有反應,下次改**************************
356         forward: function (viewId, param, replace) {
357             if (!viewId) return;
358             viewId = viewId.toLowerCase();
359 
360             this.setUrlRule(viewId, param, replace);
361             this.loadViewByUrl();
362         },
363         jump: function (path, param, replace) {
364             var viewId;
365             var project;
366             if (!path) {
367                 return;
368             }
369             path = path.toLowerCase().split('/');
370             if (path.length <= 0) {
371                 return;
372             }
373             viewId = path.pop();
374             project = path.length === 1 ? path.join('') + '/' : path.join('');
375             this.setUrlRule(viewId, param, replace, project);
376             this.loadViewByUrl();
377         },
378         back: function (viewId, param, replace) {
379             if (viewId) {
380                 this.forward(viewId, param, replace)
381             } else {
382                 if (window.history.length == 1) {
383                     this.forward(this.defaultView, param, replace)
384                 } else {
385                     history.back();
386                 }
387             }
388         }
389 
390     });
391 
392 });
abstract.app

這裏屬於框架控制器層面的代碼,與今天的主題不是很是相關,有興趣的朋友能夠詳細讀讀。

頁面基類

這裏的核心是頁面級別的處理,這裏會作比較多的介紹,首先咱們爲全部的業務級View提供了一個繼承的View:

  1 define([], function () {
  2     'use strict';
  3 
  4     return _.inherit({
  5 
  6         _propertys: function () {
  7             this.APP = this.APP || window.APP;
  8             var i = 0, len = 0, k;
  9             if (this.APP && this.APP.interface) {
 10                 for (i = 0, len = this.APP.interface.length; i < len; i++) {
 11                     k = this.APP.interface[i];
 12                     if (k == 'showPageView') continue;
 13 
 14                     if (_.isFunction(this.APP[k])) {
 15                         this[k] = $.proxy(this.APP[k], this.APP);
 16                     }
 17                     else this[k] = this.APP[k];
 18                 }
 19             }
 20 
 21             this.header = this.APP.header;
 22         },
 23 
 24         showPageView: function (name, _viewdata, id) {
 25             this.APP.curViewIns = this;
 26             this.APP.showPageView(name, _viewdata, id)
 27         },
 28         propertys: function () {
 29             //這裏設置UI的根節點所處包裹層
 30             this.wrapper = $('#main');
 31             this.id = _.uniqueId('page-view-');
 32             this.classname = '';
 33 
 34             this.viewId = null;
 35             this.refer = null;
 36 
 37             //模板字符串,各個組件不一樣,如今加入預編譯機制
 38             this.template = '';
 39             //事件機制
 40             this.events = {};
 41 
 42             //自定義事件
 43             //此處須要注意mask 綁定事件先後問題,考慮scroll.radio插件類型的mask應用,考慮組件通訊
 44             this.eventArr = {};
 45 
 46             //初始狀態爲實例化
 47             this.status = 'init';
 48 
 49             this._propertys();
 50         },
 51 
 52         getViewModel: function () {
 53             //假若有datamodel的話,便直接返回,否則便重寫,這裏基本爲了兼容
 54             if (_.isObject(this.datamodel)) return this.datamodel;
 55             return {};
 56         },
 57 
 58         //子類事件綁定若想保留父級的,應該使用該方法
 59         addEvents: function (events) {
 60             if (_.isObject(events)) _.extend(this.events, events);
 61         },
 62 
 63         on: function (type, fn, insert) {
 64             if (!this.eventArr[type]) this.eventArr[type] = [];
 65 
 66             //頭部插入
 67             if (insert) {
 68                 this.eventArr[type].splice(0, 0, fn);
 69             } else {
 70                 this.eventArr[type].push(fn);
 71             }
 72         },
 73 
 74         off: function (type, fn) {
 75             if (!this.eventArr[type]) return;
 76             if (fn) {
 77                 this.eventArr[type] = _.without(this.eventArr[type], fn);
 78             } else {
 79                 this.eventArr[type] = [];
 80             }
 81         },
 82 
 83         trigger: function (type) {
 84             var _slice = Array.prototype.slice;
 85             var args = _slice.call(arguments, 1);
 86             var events = this.eventArr;
 87             var results = [], i, l;
 88 
 89             if (events[type]) {
 90                 for (i = 0, l = events[type].length; i < l; i++) {
 91                     results[results.length] = events[type][i].apply(this, args);
 92                 }
 93             }
 94             return results;
 95         },
 96 
 97         createRoot: function (html) {
 98 
 99             //若是存在style節點,而且style節點不存在的時候須要處理
100             if (this.style && !$('#page_' + this.viewId)[0]) {
101                 $('head').append($('<style id="page_' + this.viewId + '" class="page-style">' + this.style + '</style>'))
102             }
103 
104             //若是具備fake節點,須要移除
105             $('#fake-page').remove();
106 
107             //UI的根節點
108             this.$el = $('<div class="cm-view page-' + this.viewId + ' ' + this.classname + '" style="display: none; " id="' + this.id + '">' + html + '</div>');
109             if (this.wrapper.find('.cm-view')[0]) {
110                 this.wrapper.append(this.$el);
111             } else {
112                 this.wrapper.html('').append(this.$el);
113             }
114 
115         },
116 
117         _isAddEvent: function (key) {
118             if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide')
119                 return true;
120             return false;
121         },
122 
123         setOption: function (options) {
124             //這裏能夠寫成switch,開始沒有想到有這麼多分支
125             for (var k in options) {
126                 if (k == 'events') {
127                     _.extend(this[k], options[k]);
128                     continue;
129                 } else if (this._isAddEvent(k)) {
130                     this.on(k, options[k])
131                     continue;
132                 }
133                 this[k] = options[k];
134             }
135             //      _.extend(this, options);
136         },
137 
138         initialize: function (opts) {
139             //這種默認屬性
140             this.propertys();
141             //根據參數重置屬性
142             this.setOption(opts);
143             //檢測不合理屬性,修正爲正確數據
144             this.resetPropery();
145 
146             this.addEvent();
147             this.create();
148 
149             this.initElement();
150 
151             window.sss = this;
152 
153         },
154 
155         $: function (selector) {
156             return this.$el.find(selector);
157         },
158 
159         //提供屬性重置功能,對屬性作檢查
160         resetPropery: function () { },
161 
162         //各事件註冊點,用於被繼承override
163         addEvent: function () {
164         },
165 
166         create: function () {
167             this.trigger('onPreCreate');
168             //若是沒有傳入模板,說明html結構已經存在
169             this.createRoot(this.render());
170 
171             this.status = 'create';
172             this.trigger('onCreate');
173         },
174 
175         //實例化須要用到到dom元素
176         initElement: function () { },
177 
178         render: function (callback) {
179             var data = this.getViewModel() || {};
180             var html = this.template;
181             if (!this.template) return '';
182             //引入預編譯機制
183             if (_.isFunction(this.template)) {
184                 html = this.template(data);
185             } else {
186                 html = _.template(this.template)(data);
187             }
188             typeof callback == 'function' && callback.call(this);
189             return html;
190         },
191 
192         refresh: function (needRecreate) {
193             this.resetPropery();
194             if (needRecreate) {
195                 this.create();
196             } else {
197                 this.$el.html(this.render());
198             }
199             this.initElement();
200             if (this.status != 'hide') this.show();
201             this.trigger('onRefresh');
202         },
203 
204         /**
205         * @description 組件顯示方法,首次顯示會將ui對象實際由內存插入包裹層
206         * @method initialize
207         * @param {Object} opts
208         */
209         show: function () {
210             this.trigger('onPreShow');
211             //      //若是包含就不要亂搞了
212             //      if (!$.contains(this.wrapper[0], this.$el[0])) {
213             //        //若是須要清空容器的話便清空
214             //        if (this.needEmptyWrapper) this.wrapper.html('');
215             //        this.wrapper.append(this.$el);
216             //      }
217 
218             this.$el.show();
219             this.status = 'show';
220 
221             this.bindEvents();
222 
223             this.initHeader();
224             this.trigger('onShow');
225         },
226 
227         initHeader: function () { },
228 
229         hide: function () {
230             if (!this.$el || this.status !== 'show') return;
231 
232             this.trigger('onPreHide');
233             this.$el.hide();
234 
235             this.status = 'hide';
236             this.unBindEvents();
237             this.trigger('onHide');
238         },
239 
240         destroy: function () {
241             this.status = 'destroy';
242             this.unBindEvents();
243             this.$root.remove();
244             this.trigger('onDestroy');
245             delete this;
246         },
247 
248         bindEvents: function () {
249             var events = this.events;
250 
251             if (!(events || (events = _.result(this, 'events')))) return this;
252             this.unBindEvents();
253 
254             // 解析event參數的正則
255             var delegateEventSplitter = /^(\S+)\s*(.*)$/;
256             var key, method, match, eventName, selector;
257 
258             // 作簡單的字符串數據解析
259             for (key in events) {
260                 method = events[key];
261                 if (!_.isFunction(method)) method = this[events[key]];
262                 if (!method) continue;
263 
264                 match = key.match(delegateEventSplitter);
265                 eventName = match[1], selector = match[2];
266                 method = _.bind(method, this);
267                 eventName += '.delegateUIEvents' + this.id;
268 
269                 if (selector === '') {
270                     this.$el.on(eventName, method);
271                 } else {
272                     this.$el.on(eventName, selector, method);
273                 }
274             }
275 
276             return this;
277         },
278 
279         unBindEvents: function () {
280             this.$el.off('.delegateUIEvents' + this.id);
281             return this;
282         },
283 
284         getParam: function (key) {
285             return _.getUrlParam(window.location.href, key)
286         },
287 
288         renderTpl: function (tpl, data) {
289             if (!_.isFunction(tpl)) tpl = _.template(tpl);
290             return tpl(data);
291         }
292 
293 
294     });
295 
296 });
abstract.view

一個Page級別的View會有如下幾個關鍵屬性&方法:

① template,html字符串,不包含請求的基礎模塊,會構成頁面的html骨架層

② events,全部的DOM事件定義處,以事件代理的方式定義,因此沒必要擔憂執行順序

③ addEvent,用於頁面級別各個階段的監控事件註冊點,通常來講用戶只須要關注不多幾個事件,好比:

1 //寫法
2 addEvent: function () {
3    //頁面渲染結束,並顯示時候觸發的事件
4     this.on('onShow', function () {
5     });
6     //離開頁面,頁面隱藏時候觸發的事件
7     this.on('onHide', function () {
8     });
9 }

一個頁面的基本寫法:

 1 define(['AbstractView'], function (AbstractView) {
 2     return _.inherit(AbstractView, {
 3         propertys: function ($super) {
 4             $super();
 5             //一堆基礎屬性定義
 6             //......
 7             //交互業務邏輯
 8             this.events = {
 9                 'click .js_pre_day': 'preAction'
10             };
11         },
12         preAction: function (e) { },
13         addEvent: function () {
14             this.on('onShow', function () {
15                 //當頁面渲染結束,須要作的初始化操做,好比渲染頁面
16                 //......
17             });
18             this.on('onHide', function () {
19             });
20         }
21     });
22 });

只要按照這種規則寫,便能展現頁面,而且具有DOM交互事件。

頁面模塊類

所謂頁面模塊類,即是用於拆分一個頁面爲單個組件模塊所用類,這裏有這些約定:

① 一個模塊類實例必定會依賴一個Page的基類實例
② 模塊類實例經過this.view能夠訪問到依賴類的一切資源
③ 模塊類實例與模塊之間經過數據entity作通訊

這裏代碼能夠再優化,但不是咱們這裏關注的重點:

  1 define([], function () {
  2     'use strict';
  3 
  4     return _.inherit({
  5 
  6         propertys: function () {
  7             //這裏設置UI的根節點所處包裹層,必須設置
  8             this.$el = null;
  9 
 10             //用於定位dom的選擇器
 11             this.selector = '';
 12 
 13             //每一個moduleView必須有一個父view,頁面級容器
 14             this.view = null;
 15 
 16             //模板字符串,各個組件不一樣,如今加入預編譯機制
 17             this.template = '';
 18 
 19             //事件機制
 20             this.events = {};
 21 
 22             //實體model,跨模塊通訊的橋樑
 23             this.entity = null;
 24         },
 25 
 26         setOption: function (options) {
 27             //這裏能夠寫成switch,開始沒有想到有這麼多分支
 28             for (var k in options) {
 29                 if (k == 'events') {
 30                     _.extend(this[k], options[k]);
 31                     continue;
 32                 }
 33                 this[k] = options[k];
 34             }
 35             //      _.extend(this, options);
 36         },
 37 
 38         //@override
 39         initData: function () {
 40         },
 41 
 42         //若是傳入了dom便
 43         initWrapper: function (el) {
 44             if (el && el[0]) {
 45                 this.$el = el;
 46                 return;
 47             }
 48             this.$el = this.view.$(this.selector);
 49         },
 50 
 51         initialize: function (opts) {
 52 
 53             //這種默認屬性
 54             this.propertys();
 55             //根據參數重置屬性
 56             this.setOption(opts);
 57             this.initData();
 58 
 59             this.initWithoutRender();
 60 
 61         },
 62 
 63         //處理dom已經存在,不須要渲染的狀況
 64         initWithoutRender: function () {
 65             if (this.template) return;
 66             var scope = this;
 67             this.view.on('onShow', function () {
 68                 scope.initWrapper();
 69                 if (!scope.$el[0]) return;
 70                 //若是沒有父view則不能繼續
 71                 if (!scope.view) return;
 72                 scope.initElement();
 73                 scope.bindEvents();
 74             });
 75         },
 76 
 77         $: function (selector) {
 78             return this.$el.find(selector);
 79         },
 80 
 81         //實例化須要用到到dom元素
 82         initElement: function () { },
 83 
 84         //@override
 85         //收集來自各方的實體組成view渲染須要的數據,須要重寫
 86         getViewModel: function () {
 87             throw '必須重寫';
 88         },
 89 
 90         _render: function (callback) {
 91             var data = this.getViewModel() || {};
 92             var html = this.template;
 93             if (!this.template) return '';
 94             //引入預編譯機制
 95             if (_.isFunction(this.template)) {
 96                 html = this.template(data);
 97             } else {
 98                 html = _.template(this.template)(data);
 99             }
100             typeof callback == 'function' && callback.call(this);
101             return html;
102         },
103 
104         //渲染時必須傳入dom映射
105         render: function () {
106             this.initWrapper();
107             if (!this.$el[0]) return;
108 
109             //若是沒有父view則不能繼續
110             if (!this.view) return;
111 
112             var html = this._render();
113             this.$el.html(html);
114             this.initElement();
115             this.bindEvents();
116 
117         },
118 
119         bindEvents: function () {
120             var events = this.events;
121 
122             if (!(events || (events = _.result(this, 'events')))) return this;
123             this.unBindEvents();
124 
125             // 解析event參數的正則
126             var delegateEventSplitter = /^(\S+)\s*(.*)$/;
127             var key, method, match, eventName, selector;
128 
129             // 作簡單的字符串數據解析
130             for (key in events) {
131                 method = events[key];
132                 if (!_.isFunction(method)) method = this[events[key]];
133                 if (!method) continue;
134 
135                 match = key.match(delegateEventSplitter);
136                 eventName = match[1], selector = match[2];
137                 method = _.bind(method, this);
138                 eventName += '.delegateUIEvents' + this.id;
139 
140                 if (selector === '') {
141                     this.$el.on(eventName, method);
142                 } else {
143                     this.$el.on(eventName, selector, method);
144                 }
145             }
146 
147             return this;
148         },
149 
150         unBindEvents: function () {
151             this.$el.off('.delegateUIEvents' + this.id);
152             return this;
153         }
154     });
155 
156 });
module.view

數據實體類

這裏的數據實體對應着,MVC中的Model,由於以前已經使用model用做了數據請求相關的命名,這裏便使用Entity作該工做:

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

這裏的數據實體會以實例的方式注入給模塊類實例,他的工做是起一箇中樞左右,完成模塊之間的通訊,反正很是重要就是了

其它

數據請求統一使用abstract.model,數據前端緩存使用abstract.store,這裏由於目標是作頁面拆分,請求模塊不是關鍵,各位能夠把這段代碼看層一個簡單的ajax便可:

1 this.model.setParam({});
2 this.model.execute(function (data) {
3 });

業務入口

最後簡單說下業務入口文件:

 1 (function () {
 2     var project = './';
 3     var viewRoot = 'pages';
 4     require.config({
 5         paths: {
 6             //BUS相關模板根目錄
 7             IndexPath: project + 'pages/index',
 8             ListPath: project + 'pages/list',
 9 
10             BusStore: project + 'model/bus.store',
11             BusModel: project + 'model/bus.model'
12         }
13     });
14     require(['AbstractApp', 'UIHeader'], function (APP, UIHeader) {
15         window.APP = new APP({
16             UIHeader: UIHeader,
17             viewRootPath: viewRoot
18         });
19         window.APP.initApp();
20     });
21 })();

很簡單的代碼,指定了下require的path配置,最後咱們看看入口頁面的調用:

 1 <!doctype html>
 2 <html>
 3 <head>
 4   <meta charset="UTF-8">
 5   <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" />
 6   <meta content="yes" name="apple-mobile-web-app-capable" />
 7   <meta content="black" name="apple-mobile-web-app-status-bar-style" />
 8   <meta name="format-detection" content="telephone=no" />
 9   <link href="../static/css/global.css" rel="stylesheet" type="text/css" />
10   <title>班次列表</title>
11 </head>
12 <body>
13   <div id="headerview">
14     <div class="cm-header">
15       <h1 class="cm-page-title js_title">
16         正在加載...
17       </h1>
18     </div>
19   </div>
20   <div class="cm-page-wrap">
21     <div class="cm-state" id="js_page_state">
22     </div>
23     <article class="cm-page" id="main">
24     </article>
25   </div>
26   <script type="text/javascript" src="../blade/libs/zepto.js"></script>
27   <script src="../blade/libs/fastclick.js" type="text/javascript"></script>
28   <script type="text/javascript" src="../blade/libs/underscore.js"></script>
29   <script src="../blade/libs/underscore.extend.js" type="text/javascript"></script>
30   <script type="text/javascript" src="../blade/libs/require.js"></script>
31   <script type="text/javascript" src="../blade/common.js"></script>
32   <script type="text/javascript" src="main.js"></script>
33 </body>
34 </html>
35 
36 list.html
list.html
webapp
├─blade //框架目錄
│  ├─data
│  ├─libs
│  ├─mvc
│  └─ui
├─bus
│  ├─model //數據請求模塊,徹底可使用zepto ajax替換
│  └─pages
│      ├─booking
│      ├─index
│      └─list //demo代碼模塊
└─static

接下來,讓咱們真實的開始拆分頁面吧。

組件式編程

骨架設計

首先,咱們進行最簡單的骨架設計,這裏依次是其js代碼與模板代碼:

 1 define(['AbstractView', 'text!ListPath/list.css', 'text!ListPath/tpl.layout.html'], function (AbstractView, style, layoutHtml) {
 2     return _.inherit(AbstractView, {
 3         propertys: function ($super) {
 4             $super();
 5             this.style = style;
 6             this.template = layoutHtml;
 7         },
 8 
 9         initHeader: function (name) {
10             var title = '班次列表';
11             this.header.set({
12                 view: this,
13                 title: title
14             });
15         },
16 
17         addEvent: function () {
18             this.on('onShow', function () {
19                 console.log('頁面渲染結束');
20             });
21         }
22     });
23 });
 1 <div class="calendar-bar-wrapper js_calendar_wrapper">
 2     日曆工具條模塊
 3 </div>
 4 <div class="none-data js_none_data" style="display: none;">
 5     當前暫無班次可預訂</div>
 6 <div class="js_list_wrapper">
 7     列表模塊
 8 </div>
 9 <div class="js_list_loading" style="display: none; text-align: center; padding: 10px 0;">
10     正在加載...</div>
11 <ul class="bus-tabs list-filter">
12     <li class="tabs-item js_show_setoutdate">
13         <div class="line">
14             <i class="icon-time"></i>出發時段<i class="icon-sec"></i></div>
15         <div class="line js_day_sec">
16             全天</div>
17     </li>
18     <li class="tabs-item js_show_setstation">
19         <div class="line">
20             <i class="icon-circle icon-setout "></i>出發汽車站<i class="icon-sec"></i></div>
21         <div class="line js_start_sec">
22             所有車站</div>
23     </li>
24     <li class="tabs-item js_show_arrivalstation">
25         <div class="line">
26             <i class="icon-circle icon-arrival "></i>到達汽車站<i class="icon-sec"></i></div>
27         <div class="line js_arrival_sec">
28             所有車站</div>
29     </li>
30 </ul>
tpl.layout

頁面展現如圖:

日曆工具欄的實現

這裏要作的第一步是將日曆工具欄模塊實現,以數據爲先的思考,咱們先實現了一個與日曆業務有關的數據實體:

 1 define(['AbstractEntity'], function (AbstractEntity) {
 2 
 3     var Entity = _.inherit(AbstractEntity, {
 4         propertys: function ($super) {
 5             $super();
 6             var n = new Date();
 7             var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
 8             this.data = {
 9                 date: curTime,
10                 title: '當前日期'
11             };
12         },
13 
14         set: function (date) {
15             if (!date) return;
16             if (_.isDate(date)) date = date.getTime();
17             if (typeof date === 'string') date = parseInt(date);
18             this.data.date = date;
19             this.update();
20         },
21 
22         getDateStr: function () {
23             var date = new Date();
24             date.setTime(this.data.date);
25             var dateDetail = _.dateUtil.getDetail(date);
26             var name = dateDetail.year + '-' + dateDetail.month + '-' + dateDetail.day + ' ' + dateDetail.weekday + (dateDetail.day1 ? '(' + dateDetail.day1 + ')' : '');
27             return name;
28         },
29 
30         nextDay: function () {
31             this.set(this.getDate() + 86400000);
32             return true;
33         },
34 
35         getDate: function () {
36             return parseInt(this.data.date);
37         },
38 
39         //是否可以再往前一天
40         canPreDay: function () {
41             var n = new Date();
42             var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
43 
44             //若是當前日期已是第一天,則不可預訂
45             if (curTime <= this.getDate() - 86400000) {
46                 return true;
47             }
48             return false;
49         },
50 
51         preDay: function () {
52             if (!this.canPreDay()) return false;
53             this.set(this.getDate() - 86400000);
54             return true;
55         }
56 
57     });
58 
59     return Entity;
60 });
en.date

裏面完成日期工具欄全部相關數據操做,而且不包含實際的業務邏輯

而後這裏開始設計日期工具欄的模塊View:

 1 define(['ModuleView', 'UICalendarBox', 'text!ListPath/tpl.calendar.bar.html'], function (ModuleView, UICalendarBox, tpl) {
 2     return _.inherit(ModuleView, {
 3 
 4         //此處如果要使用model,處實例化時候必定要保證entity的存在,若是不存在即是業務BUG
 5         initData: function () {
 6 
 7             this.template = tpl;
 8             this.events = {
 9                 'click .js_pre_day': 'preAction',
10                 'click .js_next_day': 'nextAction',
11                 'click .js_show_calendar': 'showCalendar'
12             };
13 
14             //初始化時候須要執行的回調
15             this.dateEntity.subscribe('init', this.render, this);
16             this.dateEntity.subscribe(this.render, this);
17 
18         },
19 
20         initDate: function () {
21             var t = new Date().getTime();
22             //默認狀況下獲取當前日期,也有過了18.00就設置爲次日日期
23             //當時一旦url上有startdatetime參數的話,便須要使用之
24             if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
25             this.dateEntity.initData({
26                 date: t
27             });
28         },
29 
30         getViewModel: function () {
31             var data = this.dateEntity.get();
32             data.formatStr = this.dateEntity.getDateStr();
33             data.canPreDay = this.dateEntity.canPreDay();
34             return data;
35         },
36 
37         preAction: function () {
38             if (this.dateEntity.preDay()) return;
39             this.view.showToast('前一天不可預訂');
40         },
41 
42         nextAction: function () {
43             this.dateEntity.nextDay();
44         },
45 
46         showCalendar: function () {
47             var scope = this, endDate = new Date();
48             var secDate = new Date();
49             secDate.setTime(this.dateEntity.getDate());
50 
51             endDate.setTime(new Date().getTime() + 2592000000);
52 
53             if (!this.calendar) {
54                 this.calendar = new UICalendarBox({
55                     endTime: endDate,
56                     selectDate: secDate,
57                     onItemClick: function (date, el, e) {
58                         scope.dateEntity.set(date);
59                         this.hide();
60                     }
61                 });
62             } else {
63                 this.calendar.calendar.selectDate = secDate;
64                 this.calendar.calendar.refresh();
65             }
66             this.calendar.show();
67         }
68 
69     });
70 
71 });
mod.date

這個組件模塊幹了幾個事情:

① 首先,dateEntity實體須要由list.js這個主view注入

② 這裏爲dateEntity註冊了兩個數據響應事件:

1 this.dateEntity.subscribe('init', this.render, this);
2 this.dateEntity.subscribe(this.render, this);

render方法繼承至基類,使用template與數據生成html,其中數據產生必須重寫父類一個方法:

1 getViewModel: function () {
2     var data = this.dateEntity.get();
3     data.formatStr = this.dateEntity.getDateStr();
4     data.canPreDay = this.dateEntity.canPreDay();
5     return data;
6 },

由於這裏的日曆數據,默認取當前時間,可是url參數可能傳遞日期參數,因此定義了一個數據初始化方法:

1 initDate: function () {
2     var t = new Date().getTime();
3     //默認狀況下獲取當前日期,也有過了18.00就設置爲次日日期
4     //當時一旦url上有startdatetime參數的話,便須要使用之
5     if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
6     this.dateEntity.initData({
7         date: t
8     });
9 },

該方法在主頁面渲染結束後會第一時間調用,這個時候日曆工具欄便渲染出來,其中日曆組件的使用便不予理睬了,主控制器的代碼改變以下:

 1 define([
 2     'AbstractView',
 3     'text!ListPath/list.css',
 4 
 5     'ListPath/en.date',
 6 
 7 
 8     'ListPath/mod.date',
 9 
10     'text!ListPath/tpl.layout.html'
11 ], function (
12     AbstractView,
13     style,
14 
15     DateEntity,
16 
17     DateModule,
18 
19     layoutHtml
20 ) {
21     return _.inherit(AbstractView, {
22 
23         _initEntity: function () {
24             this.dateEntity = new DateEntity();
25         },
26 
27         _initModule: function () {
28             this.dateModule = new DateModule({
29                 view: this,
30                 selector: '.js_calendar_wrapper',
31                 dateEntity: this.dateEntity
32             });
33         },
34 
35         propertys: function ($super) {
36             $super();
37 
38             this._initEntity();
39             this._initModule();
40 
41             this.style = style;
42             this.template = layoutHtml;
43         },
44 
45         initHeader: function (name) {
46             var title = '班次列表';
47             this.header.set({
48                 view: this,
49                 title: title
50             });
51         },
52 
53         addEvent: function () {
54             this.on('onShow', function () {
55 
56                 //初始化date數據
57                 this.dateModule.initDate();
58 
59 
60             });
61         }
62     });
63 
64 });
list.js
 1 _initEntity: function () {
 2     this.dateEntity = new DateEntity();
 3 },
 4 
 5 _initModule: function () {
 6     this.dateModule = new DateModule({
 7         view: this,
 8         selector: '.js_calendar_wrapper',
 9         dateEntity: this.dateEntity
10     });
11 },
1 addEvent: function () {
2     this.on('onShow', function () {
3         //初始化date數據
4         this.dateModule.initDate();
5 
6     });
7 }

因而,整個界面變成了這個樣子:

這裏是對應的日曆工具模板文件tpl.calendar.html:

1 <ul class="bus-tabs calendar-bar">
2     <li class="tabs-item  js_pre_day <%=!canPreDay ? 'disabled' : '' %>">前一天</li>
3     <li class="tabs-item js_show_calendar" style="-webkit-flex: 2; flex: 2;"><%=formatStr %></li>
4     <li class="tabs-item js_next_day">後一天</li>
5 </ul>

搜索工具欄的實現

咱們如今的頁面,就算不傳任何URL參數,已經能渲染出部分頁面了,可是下面出發站汽車等業務數據必須等待班次列表數據請求結束才能替換數據,可是這些數據若是沒有出發城市和到達城市是不能發起請求的,因此這裏先實現搜索工具欄功能:

在出發城市或者到達城市不存在的話便彈出搜索工具欄,引導用戶選擇城市,這裏新增彈出層須要在主頁面控制器(檢測主控制器)中使用一個UI組件:

  1 define([
  2     'AbstractView',
  3     'text!ListPath/list.css',
  4 
  5     'ListPath/en.date',
  6 
  7 
  8     'ListPath/mod.date',
  9 
 10     'text!ListPath/tpl.layout.html',
 11     'text!ListPath/tpl.search.box.html',
 12     'UIScrollLayer'
 13 ], function (
 14     AbstractView,
 15     style,
 16 
 17     DateEntity,
 18 
 19     DateModule,
 20 
 21     layoutHtml,
 22     searchBoxHtml,
 23     UIScrollLayer
 24 ) {
 25     return _.inherit(AbstractView, {
 26 
 27         _initEntity: function () {
 28             this.dateEntity = new DateEntity();
 29         },
 30 
 31         _initModule: function () {
 32             this.dateModule = new DateModule({
 33                 view: this,
 34                 selector: '.js_calendar_wrapper',
 35                 dateEntity: this.dateEntity
 36             });
 37         },
 38 
 39         propertys: function ($super) {
 40             $super();
 41 
 42             this._initEntity();
 43             this._initModule();
 44 
 45             this.style = style;
 46             this.template = layoutHtml;
 47         },
 48 
 49         initHeader: function (name) {
 50             var title = '班次列表';
 51             this.header.set({
 52                 view: this,
 53                 title: title,
 54                 back: function () {
 55                     console.log('回退');
 56                 },
 57                 right: [
 58                     {
 59                         tagname: 'search-bar',
 60                         value: '搜索',
 61                         callback: function () {
 62                             console.log('彈出搜索框');
 63                             this.showSearchBox();
 64                         }
 65                     }
 66                 ]
 67             });
 68         },
 69 
 70 
 71 
 72         //搜索工具彈出層
 73         showSearchBox: function () {
 74             var scope = this;
 75             if (!this.searchBox) {
 76                 this.searchBox = new UIScrollLayer({
 77                     title: '請選擇搜索條件',
 78                     html: searchBoxHtml,
 79                     events: {
 80                         'click .js-start': function () {
 81 
 82                         },
 83                         'click .js-arrive': function () {
 84 
 85                         },
 86                         'click .js_search_list': function () {
 87 
 88                             console.log('查詢列表');
 89                         }
 90                     }
 91                 });
 92             }
 93             this.searchBox.show();
 94         },
 95 
 96         addEvent: function () {
 97             this.on('onShow', function () {
 98                 //初始化date數據
 99                 this.dateModule.initDate();
100 
101                 //這裏判斷是否須要彈出搜索彈出層
102                 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
103                     this.showSearchBox();
104                     return;
105                 }
106 
107 
108             });
109         }
110     });
111 
112 });
list.js

對應搜索彈出層html模板:

 1 <div class="c-row search-line" data-flag="start">
 2     <div class="c-span3">
 3         出發</div>
 4     <div class="c-span9 js-start search-line-txt">
 5         請選擇出發地</div>
 6 </div>
 7 <div class="c-row search-line" data-flag="arrive">
 8     <div class="c-span3">
 9         到達</div>
10     <div class="c-span9 js-arrive search-line-txt">
11         請選擇到達地</div>
12 </div>
13 <div class="c-row " data-flag="arrive">
14     <span class="btn-primary full-width js_search_list">查詢</span>
15 </div>
tpl.search.box.html

這裏核心代碼是:

 1 //搜索工具彈出層
 2 showSearchBox: function () {
 3     var scope = this;
 4     if (!this.searchBox) {
 5         this.searchBox = new UIScrollLayer({
 6             title: '請選擇搜索條件',
 7             html: searchBoxHtml,
 8             events: {
 9                 'click .js-start': function () {
10 
11                 },
12                 'click .js-arrive': function () {
13 
14                 },
15                 'click .js_search_list': function () {
16 
17                     console.log('查詢列表');
18                 }
19             }
20         });
21     }
22     this.searchBox.show();
23 },

因而當URL什麼參數都沒有的時候,就會彈出這個搜索框

這裏也迎來了一個難點,由於城市列表事實上應該是一個獨立的可訪問的頁面,可是這裏是想用彈出層的方式調用他,因此我在APP層實現了一個方法能夠用彈出層的方式調起一個獨立的頁面。

注意:
這裏city城市列表未徹底採用組件化的方式開發,有興趣的朋友能夠本身嘗試着開發

這裏有一個不一樣的地方是,由於咱們點擊查詢的時候纔會作實體數據更新,這裏是單純的作DOM操做了,這裏不設置數據實體一個緣由就是:

這個搜索彈出層是一個頁面級DOM以外的部分,數據實體變化通常只應該影響Page級別的DOM,除非真的有兩個頁面級View會公用一個數據實體。

  1 define([
  2     'AbstractView',
  3     'text!ListPath/list.css',
  4 
  5     'ListPath/en.date',
  6 
  7 
  8     'ListPath/mod.date',
  9 
 10     'text!ListPath/tpl.layout.html',
 11     'text!ListPath/tpl.search.box.html',
 12     'UIScrollLayer'
 13 ], function (
 14     AbstractView,
 15     style,
 16 
 17     DateEntity,
 18 
 19     DateModule,
 20 
 21     layoutHtml,
 22     searchBoxHtml,
 23     UIScrollLayer
 24 ) {
 25     return _.inherit(AbstractView, {
 26 
 27         _initEntity: function () {
 28             this.dateEntity = new DateEntity();
 29 
 30 
 31         },
 32 
 33         _initModule: function () {
 34             this.dateModule = new DateModule({
 35                 view: this,
 36                 selector: '.js_calendar_wrapper',
 37                 dateEntity: this.dateEntity
 38             });
 39 
 40         },
 41 
 42         propertys: function ($super) {
 43             $super();
 44 
 45             this._initEntity();
 46             this._initModule();
 47 
 48             this.style = style;
 49             this.template = layoutHtml;
 50 
 51             //主控制器業務屬性
 52             this.urlData = {
 53                 start: {},
 54                 end: {}
 55             };
 56 
 57 
 58         },
 59 
 60         initHeader: function (name) {
 61             var title = '班次列表';
 62             this.header.set({
 63                 view: this,
 64                 title: title,
 65                 back: function () {
 66                     console.log('回退');
 67                 },
 68                 right: [
 69                     {
 70                         tagname: 'search-bar',
 71                         value: '搜索',
 72                         callback: function () {
 73                             console.log('彈出搜索框');
 74                             this.showSearchBox();
 75                         }
 76                     }
 77                 ]
 78             });
 79         },
 80 
 81         //搜索工具彈出層
 82         showSearchBox: function () {
 83             var scope = this;
 84             if (!this.searchBox) {
 85                 this.searchBox = new UIScrollLayer({
 86                     title: '請選擇搜索條件',
 87                     html: searchBoxHtml,
 88                     events: {
 89                         'click .js-start': function (e) {
 90                             scope._showCityView('start', $(e.currentTarget));
 91                         },
 92                         'click .js-arrive': function (e) {
 93                             scope._showCityView('end', $(e.currentTarget));
 94                         },
 95                         'click .js_search_list': function () {
 96                             var param = {};
 97 
 98                             if (!scope.urlData.start.id) {
 99                                 scope.showToast('請先選擇出發城市');
100                                 return;
101                             }
102 
103                             if (!scope.urlData.end.id) {
104                                 scope.showToast('請先選擇到達城市');
105                                 return;
106                             }
107 
108                             //這裏必定會有出發城市與到達城市等數據
109                             param.startcityid = scope.urlData.start.id;
110                             param.arrivalcityid = scope.urlData.end.id;
111                             param.startdatetime = scope.dateEntity.getDate();
112                             param.startname = scope.urlData.start.name;
113                             param.arrivename = scope.urlData.end.name;
114 
115                             if (scope.urlData.start.station) {
116                                 param.startstationid = scope.urlData.start.station
117                             }
118 
119                             if (scope.urlData.end.station) {
120                                 param.arrivalstationid = end_station
121                             }
122 
123                             scope.forward('list', param);
124                             this.hide();
125                         }
126                     }
127                 });
128             }
129             this.searchBox.show();
130         },
131 
132         _showCityView: function (key, el) {
133             var scope = this;
134 
135             if (key == 'end') {
136                 //由於到達車站會依賴出發車站的數據,因此這裏得先作判斷
137                 if (!this.urlData.start.id) {
138                     this.showToast('請先選擇出發城市');
139                     return;
140                 }
141             }
142 
143             this.showPageView('city', {
144                 flag: key,
145                 startId: this.urlData.start.id,
146                 type: this.urlData.start.type,
147                 onCityItemClick: function (id, name, station, type) {
148                     scope.urlData[key] = {};
149                     scope.urlData[key]['id'] = id;
150                     scope.urlData[key]['type'] = type;
151                     scope.urlData[key]['name'] = name;
152                     if (station) scope.urlData[key]['name'] = station;
153                     el.text(name);
154                     scope.hidePageView();
155                 },
156                 onBackAction: function () {
157                     scope.hidePageView();
158                 }
159             });
160         },
161 
162         addEvent: function () {
163             this.on('onShow', function () {
164                 //初始化date數據
165                 this.dateModule.initDate();
166 
167                 //這裏判斷是否須要彈出搜索彈出層
168                 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
169                     this.showSearchBox();
170                     return;
171                 }
172 
173             });
174         }
175     });
176 
177 });
list.js

搜索功能完成後,咱們這裏即可以進入真正的數據請求功能渲染列表了。

其他模塊

在實現數據請求以前,我按照日期模塊的方式將下面三個模塊的功能也一併完成了,這裏惟一不一樣的是,這些模塊的DOM已經存在,咱們不須要渲染了,完成後的代碼大概是這樣的:

  1 define([
  2     'AbstractView',
  3     'text!ListPath/list.css',
  4 
  5     'ListPath/en.station',
  6     'ListPath/en.date',
  7     'ListPath/en.time',
  8 
  9     'ListPath/mod.date',
 10     'ListPath/mod.time',
 11     'ListPath/mod.setout',
 12     'ListPath/mod.arrive',
 13 
 14     'text!ListPath/tpl.layout.html',
 15     'text!ListPath/tpl.search.box.html',
 16     'UIScrollLayer'
 17 ], function (
 18     AbstractView,
 19     style,
 20 
 21     StationEntity,
 22     DateEntity,
 23     TimeEntity,
 24 
 25     DateModule,
 26     TimeModule,
 27     SetoutModule,
 28     ArriveModule,
 29 
 30     layoutHtml,
 31     searchBoxHtml,
 32     UIScrollLayer
 33 ) {
 34     return _.inherit(AbstractView, {
 35 
 36         _initEntity: function () {
 37             this.dateEntity = new DateEntity();
 38 
 39             this.timeEntity = new TimeEntity();
 40             this.timeEntity.subscribe('init', this.renderTime, this);
 41             this.timeEntity.subscribe(this.renderTime, this);
 42 
 43             this.setoutEntity = new StationEntity();
 44             this.setoutEntity.subscribe('init', this.renderSetout, this);
 45             this.setoutEntity.subscribe(this.renderSetout, this);
 46 
 47             this.arriveEntity = new StationEntity();
 48             this.arriveEntity.subscribe('init', this.renderArrive, this);
 49             this.arriveEntity.subscribe(this.renderArrive, this);
 50 
 51         },
 52 
 53         _initModule: function () {
 54             this.dateModule = new DateModule({
 55                 view: this,
 56                 selector: '.js_calendar_wrapper',
 57                 dateEntity: this.dateEntity
 58             });
 59 
 60             this.timeModule = new TimeModule({
 61                 view: this,
 62                 selector: '.js_show_setoutdate',
 63                 timeEntity: this.timeEntity
 64             });
 65 
 66             this.setOutModule = new SetoutModule({
 67                 view: this,
 68                 selector: '.js_show_setstation',
 69                 setoutEntity: this.setoutEntity
 70             });
 71 
 72             this.arriveModule = new ArriveModule({
 73                 view: this,
 74                 selector: '.js_show_arrivalstation',
 75                 arriveEntity: this.arriveEntity
 76             });
 77 
 78         },
 79 
 80         propertys: function ($super) {
 81             $super();
 82 
 83             this._initEntity();
 84             this._initModule();
 85 
 86             this.style = style;
 87             this.template = layoutHtml;
 88 
 89             //主控制器業務屬性
 90             this.urlData = {
 91                 start: {},
 92                 end: {}
 93             };
 94 
 95 
 96         },
 97 
 98         initHeader: function (name) {
 99             var title = '班次列表';
100             this.header.set({
101                 view: this,
102                 title: title,
103                 back: function () {
104                     console.log('回退');
105                 },
106                 right: [
107                     {
108                         tagname: 'search-bar',
109                         value: '搜索',
110                         callback: function () {
111                             console.log('彈出搜索框');
112                             this.showSearchBox();
113                         }
114                     }
115                 ]
116             });
117         },
118 
119         initElement: function () {
120             this.d_list_wrapper = this.$('.js_list_wrapper');
121             this.d_none_data = this.$('.js_none_data');
122 
123             this.d_js_show_setoutdate = this.$('.js_show_setoutdate');
124             this.d_js_show_setstation = this.$('.js_show_setstation');
125             this.d_js_show_arrivalstation = this.$('.js_show_arrivalstation');
126             this.d_js_list_loading = this.$('.js_list_loading');
127             this.d_js_tabs = this.$('.js_tabs');
128 
129             this.d_js_day_sec = this.$('.js_day_sec');
130             this.d_js_start_sec = this.$('.js_start_sec');
131             this.d_js_arrival_sec = this.$('.js_arrival_sec');
132         },
133 
134         //搜索工具彈出層
135         showSearchBox: function () {
136             var scope = this;
137             if (!this.searchBox) {
138                 this.searchBox = new UIScrollLayer({
139                     title: '請選擇搜索條件',
140                     html: searchBoxHtml,
141                     events: {
142                         'click .js-start': function (e) {
143                             scope._showCityView('start', $(e.currentTarget));
144                         },
145                         'click .js-arrive': function (e) {
146                             scope._showCityView('end', $(e.currentTarget));
147                         },
148                         'click .js_search_list': function () {
149                             var param = {};
150 
151                             if (!scope.urlData.start.id) {
152                                 scope.showToast('請先選擇出發城市');
153                                 return;
154                             }
155 
156                             if (!scope.urlData.end.id) {
157                                 scope.showToast('請先選擇到達城市');
158                                 return;
159                             }
160 
161                             //這裏必定會有出發城市與到達城市等數據
162                             param.startcityid = scope.urlData.start.id;
163                             param.arrivalcityid = scope.urlData.end.id;
164                             param.startdatetime = scope.dateEntity.getDate();
165                             param.startname = scope.urlData.start.name;
166                             param.arrivename = scope.urlData.end.name;
167 
168                             if (scope.urlData.start.station) {
169                                 param.startstationid = scope.urlData.start.station
170                             }
171 
172                             if (scope.urlData.end.station) {
173                                 param.arrivalstationid = end_station
174                             }
175 
176                             scope.forward('list', param);
177                             this.hide();
178                         }
179                     }
180                 });
181             }
182             this.searchBox.show();
183         },
184 
185         _showCityView: function (key, el) {
186             var scope = this;
187 
188             if (key == 'end') {
189                 //由於到達車站會依賴出發車站的數據,因此這裏得先作判斷
190                 if (!this.urlData.start.id) {
191                     this.showToast('請先選擇出發城市');
192                     return;
193                 }
194             }
195 
196             this.showPageView('city', {
197                 flag: key,
198                 startId: this.urlData.start.id,
199                 type: this.urlData.start.type,
200                 onCityItemClick: function (id, name, station, type) {
201                     scope.urlData[key] = {};
202                     scope.urlData[key]['id'] = id;
203                     scope.urlData[key]['type'] = type;
204                     scope.urlData[key]['name'] = name;
205                     if (station) scope.urlData[key]['name'] = station;
206                     el.text(name);
207                     scope.hidePageView();
208                 },
209                 onBackAction: function () {
210                     scope.hidePageView();
211                 }
212             });
213         },
214 
215         //初始化出發車站,該數據會隨着數據加載結束而變化
216         //若是url具備出發站名稱以及id,須要特殊處理
217         initSetoutEntity: function () {
218             var data = {};
219             if (_.getUrlParam().startstationid) {
220                 //出發車站可能並無傳,兼容老代碼
221                 data.name = _.getUrlParam().startname || '所有車站';
222                 data.id = _.getUrlParam().startstationid;
223             }
224 
225             this.setoutEntity.initData(data, data.id);
226         },
227 
228         //初始化到達站
229         initArriveEntity: function () {
230 
231             var data = {};
232             if (_.getUrlParam().arrivalstationid) {
233                 //出發車站可能並無傳,兼容老代碼
234                 data.name = _.getUrlParam().arrivename || '所有車站';
235                 data.id = _.getUrlParam().arrivalstationid;
236             }
237 
238             this.arriveEntity.initData(data, data.id);
239         },
240 
241         //時段只有變化時候才具備顯示狀態
242         renderTime: function () {
243             var name = this.timeEntity.getName();
244             this.d_js_day_sec.html(name);
245         },
246 
247         renderSetout: function () {
248             var name = this.setoutEntity.getName();
249             this.d_js_start_sec.html(name);
250         },
251 
252         renderArrive: function () {
253             var name = this.arriveEntity.getName();
254             this.d_js_arrival_sec.html(name);
255         },
256 
257         addEvent: function () {
258             this.on('onShow', function () {
259                 //初始化date數據
260                 this.dateModule.initDate();
261 
262                 //這裏判斷是否須要彈出搜索彈出層
263                 if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
264                     this.showSearchBox();
265                     return;
266                 }
267 
268                 //初始化時段選擇
269                 this.timeEntity.initData();
270                 this.initSetoutEntity();
271                 this.initArriveEntity();
272 
273             });
274         }
275     });
276 
277 });
list.js

這個時候整個邏輯結構大概出來了:

注意:
由於該文耗時過長,致使我如今體力有點虛脫,因此這裏的代碼不必定最優

最後功能:

到此,demo結束了,最後造成的目錄:

一個js即可以拆分紅這麼多的小組件模塊,若是是更加複雜的頁面,這裏的文件會不少,好比訂單填寫頁的組件模塊是這裏的三倍。

組件化的優缺點

組件化帶來的幾個優勢十分明顯:

① 組件化拆分,使得主控制業務邏輯清晰簡單
② 各個業務組件模塊功能相對獨立,可維護性可測試性大大提高
③ 組件之間能夠任意組合,有必定可重用性
④ 增刪模塊不會怕打斷骨頭連着筋
⑤ 一個業務模塊所需代碼所有在一個目錄,比較好操做(有點湊數嫌疑)

缺點

事實上,組件化不會帶來什麼不足,對於不瞭解的朋友可能會認爲代碼複雜度有所增長,其實不這樣作代碼才真正叫一個難呢!

真正的美中不足的要挑一個毛病的話,這種分拆可能會比單個文件代碼量稍大

從性能優化角度看組件化

不管什麼前端優化,最後的瓶頸必定是在請求量上作文章:壓縮、緩存、僅僅作首屏渲染、將jQuery緩存zepto......

說都會說,可是不少場景由不得你那樣作,項目足夠複雜,而UI又提供給了不一樣團隊使用的話,有一天前端作了一次UI優化,而如何將此次UI優化反應到線上纔是考驗架構設計的時候,若是是很差的設計的話,想將此次優化推上線,會發生兩個事情:

① 業務團隊大改代碼

② 框架資源(js&css)膨脹

這種頭疼的問題是通常人作優化考慮不到的,而業務團隊不會由於你的更新而去修改代碼,因此通常會以代碼膨脹爲代價將此次優化強推上線,那每每會讓狀況更加複雜:

新老代碼融合,半年後你根本不知道哪些代碼能夠刪,哪些代碼能夠留,很大時候這個問題會體如今具備公共特性的CSS中
若是你的CSS同時服務於多個團隊,而各個團隊的框架版本不一致,那麼UI升級對你來講多是一個噩夢!
若是你想作第三輪的UI升級,那仍是算了吧......

事實上,我評價一個前端是否足夠厲害,每每就會從這裏考慮:

當一個項目足夠複雜後,你私下作好了優化,可是你的優化代碼不能無縫的讓業務團隊使用,而須要業務團隊作不少改變,你如何解決這種問題

不少前端作一個優化,即是從新作了一個東西,剛開始確定比線上的好,但半年後,那個代碼質量還未必有之前的好呢,因此咱們這裏應該解決的是:

如何設計一個機制,讓業務團隊以最小的修改,而能夠用上新的UI(樣式、特性),而不會增長CSS(JS)體積
這個多是組件化真正要解決的事情!

理想狀況下,一個H5的資源組成狀況是這樣的:

① 公共核心CSS文件(200行左右)

② 框架核心文件(包含框架核心和第三方庫)

③ UI組件(有不少獨立的UI組件組成,每一個UI組件又包含完整的HTML&CSS)

④ 公共業務模塊(提供業務級別公共服務,好比登陸、城市列表等業務相關功能)

⑤ 業務頻道一個頁面,也就是咱們這裏的list頁的代碼

由於框架核心通常來講是不常常改變的,就算改變也是對錶現層透明的,UI採用增量與預加載機制,這樣作會對後續樣式升級,UI升級有莫大的好處,而業務組件化後自己要作什麼滾動加載也是垂手可得

好的前端架構設計應該知足不停的UI升級需求,而不增長業務團隊下載量

結語

本文就如何分解複雜的前端頁面提出了一些本身的想法,而且給予了實現,但願對各位有所幫助。

關於合併

前端代碼有分拆就有合併,由於最終一個完整的頁面須要全部資源才能運行,但考慮到此文已經很長了,關於合併一塊的工做留待下文分析吧

關於代碼

爲了方便各位理解組件化開發的思想,我這裏寫了一個完整的demo幫助各位分析,因爲精力有限,代碼不免會有BUG,各位多多包涵:

https://github.com/yexiaochai/mvc

可能會瀏覽的代碼:

https://github.com/yexiaochai/blade

重要的事情

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

相關文章
相關標籤/搜索