隨着移動設備的發展,移動設備以迅猛的勢頭分颳着PC的佔有率,ipad或者android pad的市場佔有率穩步提高,因此咱們的程序須要在ipad上很好的運行,對於公司來講有如下負擔:設備系統上來講主要分爲android ios;尺寸上看又以手機與pad爲一個分界線,若是再加一個H5站點,其開發所投入資源不可謂不小!css
Hybrid的出現,解決了大部分問題,針對尺寸上的問題有一種東西叫作響應式設計,這個響應式設計彷佛能夠解決咱們的問題,因此今天我就來告訴你們什麼是響應式設計,或者說我這種外行覺得的響應式設計。html
響應式Web設計(Responsive Web design)的理念是:集中建立頁面的圖片排版大小,能夠智能地根據用戶行爲以及使用的設備環境(系統平臺、屏幕尺寸、屏幕定向等)進行相對應的佈局。
以我粗淺的理解,響應式的提出,其實就是單純的根據不一樣的尺寸,以最優的展現方式呈現罷了,僅僅而已,不能再多了,若是真要更多點,即是根據不一樣的尺寸對靜態資源加載上有所控制,節約流量,換句話說,響應式設計不涉及業務邏輯,jser神馬都不須要作,css同事即可徹底解決,但事實上最近碰到的需求徹底不是這麼回事嘛。node
以最簡單圖片輪播來講,手機上是這個樣子的:android
而在ipad橫屏上,卻變成了這個樣子了:ios
我當時就醉了,iPad豎着保持手機樣式,橫着iPad樣式,什麼CSS有這麼偉大,能夠完成這個功能,而真實的場景是這個樣子的:git
手機端:首頁搜索頁->list頁面->詳情頁->預約頁github
可是到了ipad橫屏上:首頁左屏是搜索頁,右邊是日期選擇/城市選擇/......,而後到了list頁面,左邊是list,右邊是詳情頁,單擊左邊的list左邊詳情直接變化!web
其實單獨頁面作的話,好像沒有什麼問題,可是手機業務早已鋪開了,老闆的意思是,代碼要重用,仍是全局改改CSS實現就好啦,我當時真爲咱們的UED捏了一把汗。到了具體業務實現的同事那裏狀況又變了,UED只是給出了兩個設計好了的靜態html+css,要怎麼玩還得那個業務同事本身搞。app
那天我去支援時,看到了其牛逼的實現,不禁的菊花一緊,裏面媒體查詢都沒有用,直接display: none 搞定一切問題了,這個對手機程序帶來了很大的負擔:原來一個view就是用於手機,如今無故的在裏面加入了大量的pad端程序,直接形成了兩個結果:框架
① 業務邏輯變得複雜,容易出BUG
② js尺寸變大,對手機端來講,流量很寶貴
雖然知道他那種作法不可取,當時忙於其它事情,而且天意難違,天意難測也只有聽之任之,可是這裏要說一點,響應式佈局不太適合業務複雜的webapp,各位要慎重!
雖然如此,問題仍是須要解決,而且須要在框架層作出解決,這類需求本不該強加與CSS,好在曾經咱們業務層的View設計基本是知足條件的,如今只須要擴展便可,仍然以blade框架爲例:
每一個頁面片完成的工做僅僅依賴了一個View類,既然View是類,那麼繼承mobile的View,實現ipad的View,彷佛是可能的,這一切的基石即是繼承
咱們這裏的View Controller index.js開始是不徹底知足咱們的需求的,咱們作一些調整,這裏是調整前的代碼:
1 define(['View', getViewTemplatePath('index'), 'UIGroupList'], function (View, viewhtml, UIGroupList) { 2 3 return _.inherit(View, { 4 onCreate: function () { 5 this.$el.html(viewhtml); 6 this.initElement(); 7 8 this.TXTTIMERRES = null; 9 10 }, 11 12 initElement: function () { 13 this.cancelBtn = this.$('.cui-btn-cancle'); 14 this.searchBox = this.$('.cui-input-box'); 15 this.txtWrapper = this.$('.cui-citys-hd'); 16 this.searchList = this.$('.seach-list'); 17 18 }, 19 20 events: { 21 'focus .cui-input-box': 'seachTxtFocus', 22 'click .cui-btn-cancle': function () { 23 this.closeSearch(); 24 }, 25 'click .seach-list>li': function (e) { 26 var gindex = $(e.currentTarget).attr('data-group'); 27 var index = $(e.currentTarget).attr('data-index'); 28 29 this.forward(this.uidata[gindex].data[index].uiname); 30 } 31 }, 32 33 seachTxtFocus: function (e) { 34 this.openSeach(); 35 }, 36 37 closeSearch: function () { 38 this.txtWrapper.removeClass('cui-input-focus'); 39 this.groupList.show(); 40 this.searchList.hide(); 41 this.searchBox.val(''); 42 }, 43 44 //開啓搜索狀態 45 openSeach: function () { 46 if (this.TXTTIMERRES) return; 47 48 this.TXTTIMERRES = setInterval($.proxy(function () { 49 // console.log(1); 50 //若是當前獲取焦點的不是input元素的話便清除定時器 51 if (!this.isInputFocus()) { 52 if (this.TXTTIMERRES) { 53 clearInterval(this.TXTTIMERRES); 54 this.TXTTIMERRES = null; 55 } 56 } 57 58 var txt = this.searchBox.val().toLowerCase(); 59 if (txt == '') { 60 setTimeout($.proxy(function () { 61 if (!this.isInputFocus()) { 62 this.closeSearch(); 63 } 64 }, this), 500); 65 return; 66 } 67 68 this.txtWrapper.addClass('cui-input-focus'); 69 this.groupList.hide(); 70 this.searchList.show(); 71 72 var list = this.groupList.getFilterList(txt); 73 this.searchList.html(list); 74 75 }, this)); 76 77 78 }, 79 80 isInputFocus: function () { 81 if (document.activeElement.nodeName == 'INPUT' && document.activeElement.type == 'text') 82 return true; 83 return false; 84 }, 85 86 initGoupList: function () { 87 if (this.groupList) return; 88 var scope = this; 89 90 //提示類 91 var groupList1 = [ 92 { 'uiname': 'alert', 'name': '警告框' }, 93 { 'uiname': 'toast', 'name': 'toast框' }, 94 { 'uiname': 'reloading', 'name': 'loading框' }, 95 { 'uiname': 'bubble.layer', 'name': '氣泡框提示' }, 96 { 'uiname': 'warning404', 'name': '404提醒' }, 97 { 'uiname': 'layerlist', 'name': '彈出層list' } 98 ]; 99 100 var groupList2 = [ 101 102 { 'uiname': 'identity', 'name': '身份證鍵盤' }, 103 { 'uiname': 'imageslider', 'name': '圖片輪播' }, 104 { 'uiname': 'num', 'name': '數字組件' }, 105 { 'uiname': 'select', 'name': 'select組件' }, 106 { 'uiname': 'switch', 'name': 'switch組件' }, 107 { 'uiname': 'tab', 'name': 'tab組件' }, 108 { 'uiname': 'calendar', 'name': '日曆組件' }, 109 { 'uiname': 'group.list', 'name': '分組列表' }, 110 { 'uiname': 'group.list', 'name': '搜索列表(城市搜索,地址搜索,待補充)' } 111 ]; 112 113 var groupList3 = [ 114 { 'uiname': 'radio.list', 'name': '單列表選擇組件' }, 115 { 'uiname': 'scroll.layer', 'name': '滾動層組件(可定製化彈出層,比較經常使用)' }, 116 { 'uiname': 'group.select', 'name': '日期選擇類組件' }, 117 { 'uiname': 'scroll', 'name': '滾動組件/橫向滾動' }, 118 ]; 119 120 var groupList4 = [ 121 { 'uiname': 'lazyload', 'name': '圖片延遲加載' }, 122 { 'uiname': 'inputclear', 'name': '帶刪除按鈕的文本框(todo...)' }, 123 { 'uiname': 'validate1', 'name': '工具類表單驗證' }, 124 { 'uiname': 'validate2', 'name': '集成表單驗證(todo...)' }, 125 { 'uiname': 'filp', 'name': '簡單flip手勢工具' } 126 ]; 127 128 var uidata = [ 129 { name: '彈出層類組件', data: groupList1 }, 130 { name: '經常使用組件', data: groupList2 }, 131 { name: '滾動類組件', data: groupList3 }, 132 { name: '全局類', data: groupList4 } 133 ]; 134 135 this.uidata = uidata; 136 137 this.groupList = new UIGroupList({ 138 datamodel: { 139 data: uidata, 140 filter: 'uiname,name' 141 }, 142 wrapper: this.$('.cui-citys-bd'), 143 onItemClick: function (item, groupIndex, index, e) { 144 scope.forward(item.uiname); 145 } 146 }); 147 148 149 this.groupList.show(); 150 151 }, 152 153 onPreShow: function () { 154 this.turning(); 155 }, 156 157 onShow: function () { 158 this.initGoupList(); 159 }, 160 161 onHide: function () { 162 163 } 164 165 }); 166 });
1 <div id="headerview" style="height: 48px;"> 2 <header> 3 <h1> 4 UI組件demo列表</h1> 5 </header></div> 6 7 <section class="cui-citys-hd "> 8 <div class="cui-input-bd"> 9 <input type="text" class="cui-input-box" placeholder="中文/拼音/首字母"> 10 </div> 11 <button type="button" class="cui-btn-cancle">取消</button> 12 </section> 13 <ul class="cui-city-associate seach-list"></ul> 14 15 <section class="cui-citys-bd"> 16 </section>
調整後的代碼以下:
1 define(['View', getViewTemplatePath('index'), 'UIGroupList'], function (View, viewhtml, UIGroupList) { 2 3 return _.inherit(View, { 4 onCreate: function () { 5 this.$el.html(viewhtml); 6 this.initElement(); 7 8 this.TXTTIMERRES = null; 9 10 }, 11 12 initElement: function () { 13 this.cancelBtn = this.$('.cui-btn-cancle'); 14 this.searchBox = this.$('.cui-input-box'); 15 this.txtWrapper = this.$('.cui-citys-hd'); 16 this.searchList = this.$('.seach-list'); 17 18 }, 19 20 events: { 21 'focus .cui-input-box': 'seachTxtFocus', 22 'click .cui-btn-cancle': 'closeSearchAction', 23 'click .seach-list>li': 'searchItemAction' 24 }, 25 26 searchItemAction: function (e) { 27 var gindex = $(e.currentTarget).attr('data-group'); 28 var index = $(e.currentTarget).attr('data-index'); 29 this.forward(this.uidata[gindex].data[index].uiname); 30 }, 31 32 closeSearchAction: function () { 33 this.closeSearch(); 34 }, 35 36 demoItemAction: function (item, groupIndex, index, e) { 37 scope.forward(item.uiname); 38 }, 39 40 seachTxtFocus: function (e) { 41 this.openSeach(); 42 }, 43 44 closeSearch: function () { 45 this.txtWrapper.removeClass('cui-input-focus'); 46 this.groupList.show(); 47 this.searchList.hide(); 48 this.searchBox.val(''); 49 }, 50 51 //開啓搜索狀態 52 openSeach: function () { 53 if (this.TXTTIMERRES) return; 54 55 this.TXTTIMERRES = setInterval($.proxy(function () { 56 // console.log(1); 57 //若是當前獲取焦點的不是input元素的話便清除定時器 58 if (!this.isInputFocus()) { 59 if (this.TXTTIMERRES) { 60 clearInterval(this.TXTTIMERRES); 61 this.TXTTIMERRES = null; 62 } 63 } 64 65 var txt = this.searchBox.val().toLowerCase(); 66 if (txt == '') { 67 setTimeout($.proxy(function () { 68 if (!this.isInputFocus()) { 69 this.closeSearch(); 70 } 71 }, this), 500); 72 return; 73 } 74 75 this.txtWrapper.addClass('cui-input-focus'); 76 this.groupList.hide(); 77 this.searchList.show(); 78 79 var list = this.groupList.getFilterList(txt); 80 this.searchList.html(list); 81 82 }, this)); 83 84 85 }, 86 87 isInputFocus: function () { 88 if (document.activeElement.nodeName == 'INPUT' && document.activeElement.type == 'text') 89 return true; 90 return false; 91 }, 92 93 initGoupList: function () { 94 if (this.groupList) return; 95 var scope = this; 96 97 //提示類 98 var groupList1 = [ 99 { 'uiname': 'alert', 'name': '警告框' }, 100 { 'uiname': 'toast', 'name': 'toast框' }, 101 { 'uiname': 'reloading', 'name': 'loading框' }, 102 { 'uiname': 'bubble.layer', 'name': '氣泡框提示' }, 103 { 'uiname': 'warning404', 'name': '404提醒' }, 104 { 'uiname': 'layerlist', 'name': '彈出層list' } 105 ]; 106 107 var groupList2 = [ 108 109 { 'uiname': 'identity', 'name': '身份證鍵盤' }, 110 { 'uiname': 'imageslider', 'name': '圖片輪播' }, 111 { 'uiname': 'num', 'name': '數字組件' }, 112 { 'uiname': 'select', 'name': 'select組件' }, 113 { 'uiname': 'switch', 'name': 'switch組件' }, 114 { 'uiname': 'tab', 'name': 'tab組件' }, 115 { 'uiname': 'calendar', 'name': '日曆組件' }, 116 { 'uiname': 'group.list', 'name': '分組列表' }, 117 { 'uiname': 'group.list', 'name': '搜索列表(城市搜索,地址搜索,待補充)' } 118 ]; 119 120 var groupList3 = [ 121 { 'uiname': 'radio.list', 'name': '單列表選擇組件' }, 122 { 'uiname': 'scroll.layer', 'name': '滾動層組件(可定製化彈出層,比較經常使用)' }, 123 { 'uiname': 'group.select', 'name': '日期選擇類組件' }, 124 { 'uiname': 'scroll', 'name': '滾動組件/橫向滾動' }, 125 ]; 126 127 var groupList4 = [ 128 { 'uiname': 'lazyload', 'name': '圖片延遲加載' }, 129 { 'uiname': 'inputclear', 'name': '帶刪除按鈕的文本框(todo...)' }, 130 { 'uiname': 'validate1', 'name': '工具類表單驗證' }, 131 { 'uiname': 'validate2', 'name': '集成表單驗證(todo...)' }, 132 { 'uiname': 'filp', 'name': '簡單flip手勢工具' } 133 ]; 134 135 var uidata = [ 136 { name: '彈出層類組件', data: groupList1 }, 137 { name: '經常使用組件', data: groupList2 }, 138 { name: '滾動類組件', data: groupList3 }, 139 { name: '全局類', data: groupList4 } 140 ]; 141 142 this.uidata = uidata; 143 144 this.groupList = new UIGroupList({ 145 datamodel: { 146 data: uidata, 147 filter: 'uiname,name' 148 }, 149 wrapper: this.$('.cui-citys-bd'), 150 onItemClick: function (item, groupIndex, index, e) { 151 scope.demoItemAction(item.uiname); 152 } 153 }); 154 155 this.groupList.show(); 156 }, 157 158 onPreShow: function () { 159 this.turning(); 160 }, 161 162 onShow: function () { 163 this.initGoupList(); 164 }, 165 166 onHide: function () { 167 168 } 169 170 }); 171 });
PS:上面的代碼是我幾個月前寫的,今天一看又以爲能夠優化,當真優化無極限啊!!!
變化的關鍵點是每次我點擊的事件所有放到了Index這個類的prototype上:
1 searchItemAction: function (e) { 2 var gindex = $(e.currentTarget).attr('data-group'); 3 var index = $(e.currentTarget).attr('data-index'); 4 this.forward(this.uidata[gindex].data[index].uiname); 5 }, 6 7 closeSearchAction: function () { 8 this.closeSearch(); 9 }, 10 11 demoItemAction: function (item, groupIndex, index, e) { 12 scope.demoItemAction(item, groupIndex, index, e); 13 },
這裏粒度到哪一個程度與具體業務相關,我這裏不作論述,因而我這裏繼承至index產生一個新的index類:index.ipad.js,這個是其基本實現:
1 define([getViewClass('index'), getViewTemplatePath('index'), 'UIGroupList'], function (View, viewhtml, UIGroupList) { 2 return _.inherit(View, { 3 4 onCreate: function ($super) { 5 $super(); 6 }, 7 8 onPreShow: function ($super) { 9 $super(); 10 this.turning(); 11 }, 12 13 onShow: function ($super) { 14 $super(); 15 this.initGoupList(); 16 }, 17 18 onHide: function ($super) { 19 $super(); 20 }, 21 22 events: { 23 24 }, 25 26 searchItemAction: function (e) { 27 var gindex = $(e.currentTarget).attr('data-group'); 28 var index = $(e.currentTarget).attr('data-index'); 29 this.forward(this.uidata[gindex].data[index].uiname); 30 }, 31 32 demoItemAction: function (item, groupIndex, index, e) { 33 scope.forward(item.uiname); 34 } 35 36 }); 37 });
這個時候直接運行blade/ipad/debug.html#index.ipad的話,頁面與原來index保持一致:
第二步即是重寫其事件的關鍵位置了,好比要跳出的兩個事件點:
1 searchItemAction: function (e) { 2 var gindex = $(e.currentTarget).attr('data-group'); 3 var index = $(e.currentTarget).attr('data-index'); 4 this.forward(this.uidata[gindex].data[index].uiname); 5 }, 6 7 demoItemAction: function (item, groupIndex, index, e) { 8 scope.forward(item.uiname); 9 } 10 11 //簡單改變 12 13 searchItemAction: function (e) { 14 var gindex = $(e.currentTarget).attr('data-group'); 15 var index = $(e.currentTarget).attr('data-index'); 16 alert(this.uidata[gindex].data[index].uiname); 17 }, 18 19 demoItemAction: function (item, groupIndex, index, e) { 20 alert(item.uiname); 21 }
這個時候原版本的跳轉,變成了alert:
這個時候便須要進一步重寫了,好比這裏:我點擊alert,事實上是想在右邊加載那個子view,因此框架全局控制器APP須要新增loadSubView的接口了:
loadSubView要實現實例化某一View很是簡單,可是該接口的工做並不輕鬆,換句話說會很是複雜,由於:
History與路由歸一化是mobile與pad版本整合的難點
mobile的view與ipadview是公用的,因此自己不存在主次關係,是業務給予了其主次,這裏須要一個管理關係
子View的實例化會涉及到複雜的History與路由管理,咱們這裏先繞過去,下個階段再處理,由於完成pad版本,框架的MVC核心要通過一次重構
1 //這裏暫時不處理History邏輯,也無論子View的管理,先單純實現功能 2 //這樣會致使back的錯亂,View重複實例化,這裏先不予關注 3 loadSubView: function (viewId, wrapper, callback) { 4 5 //子View要在哪裏顯示須要處理 6 if (!wrapper[0]) return; 7 8 this.loadView(viewId, function (View) { 9 10 var curView = new View(this, viewId, wrapper); 11 12 //這個是惟一須要改變的 13 curView.turning = $.proxy(function () { 14 curView.show(); 15 curView.$el.show(); 16 }, this); 17 curView.onPreShow(); 18 callback && callback(curView); 19 20 }); 21 22 },
在樣式上再作一點調整就變成這個樣子了:
這裏History管理仍是亂的,可是整個這個方案是可行的,因此咱們前哨戰是成功的,方案可行的話便須要詳細的設計了
今天,咱們對ipad與mobile統一使用一套view代碼作了研究,有如下收穫與問題:
① 繼承可實現ipad與mobile代碼複用,而且不會彼此污染,至少不會污染mobile程序
② pad版本中History與路由管理須要重構
③ MVC須要重構,特別是View一塊,甚至須要徹底從新寫
④ 樣式方面還須要處理優化
總而言之,今天的收穫仍是有的,剩下的問題,須要在覈心框架上動大動做了,最後的目標是可以出一套同用於ipad與mobile的框架。
源碼:
https://github.com/yexiaochai/blade
demo在此:
http://yexiaochai.github.io/blade/ipad/debug.html#index.ipad