zoukankan      html  css  js  c++  java
  • ipad&mobile通用webapp框架前哨战

    响应式设计的意义

    随着移动设备的发展,移动设备以迅猛的势头分刮着PC的占有率,ipad或者android pad的市场占有率稳步提升,所以我们的程序需要在ipad上很好的运行,对于公司来说有以下负担:设备系统上来说主要分为android ios;尺寸上看又以手机与pad为一个分界线,如果再加一个H5站点,其开发所投入资源不可谓不小!

    Hybrid的出现,解决了大部分问题,针对尺寸上的问题有一种东西叫做响应式设计,这个响应式设计似乎可以解决我们的问题,所以今天我就来告诉大家什么是响应式设计,或者说我这种外行以为的响应式设计。

    响应式Web设计(Responsive Web design)的理念是:集中创建页面的图片排版大小,可以智能地根据用户行为以及使用的设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相对应的布局。

    以我粗浅的理解,响应式的提出,其实就是单纯的根据不同的尺寸,以最优的展示方式呈现罢了,仅仅而已,不能再多了,如果真要更多点,便是根据不同的尺寸对静态资源加载上有所控制,节约流量,换句话说,响应式设计不涉及业务逻辑,jser神马都不需要做,css同事便可完全解决,但事实上最近碰到的需求完全不是这么回事嘛。

    以最简单图片轮播来说,手机上是这个样子的:

    而在ipad横屏上,却变成了这个样子了:

    我当时就醉了,iPad竖着保持手机样式,横着iPad样式,什么CSS有这么伟大,可以完成这个功能,而真实的场景是这个样子的:

    手机端:首页搜索页->list页面->详情页->预定页

    但是到了ipad横屏上:首页左屏是搜索页,右边是日期选择/城市选择/......,然后到了list页面,左边是list,右边是详情页,单击左边的list左边详情直接变化!

    其实单独页面做的话,好像没有什么问题,但是手机业务早已铺开了,老板的意思是,代码要重用,还是全局改改CSS实现就好啦,我当时真为我们的UED捏了一把汗。到了具体业务实现的同事那里情况又变了,UED只是给出了两个设计好了的静态html+css,要怎么玩还得那个业务同事自己搞。

    那天我去支援时,看到了其牛逼的实现,不由的菊花一紧,里面媒体查询都没有用,直接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

  • 相关阅读:
    API
    Object constructor
    function()
    For语句的衍生对象
    编程语言发展史
    将Paul替换成Ringo
    Document.write和 getElementById(ID)
    姓名,电话号码,邮箱的正则检测
    JavaScript-BOM与DOM
    管理心得
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/4148235.html
Copyright © 2011-2022 走看看