ipad&mobile通用webapp框架前哨戰

響應式設計的意義

隨着移動設備的發展,移動設備以迅猛的勢頭分颳着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,各位要慎重!

ipad版本應該怎麼作?

雖然如此,問題仍是須要解決,而且須要在框架層作出解決,這類需求本不該強加與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>
對應HTML模板

調整後的代碼以下:

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

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

相關文章
相關標籤/搜索