【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知

20 篇文章 0 订阅
13 篇文章 0 订阅

前言

不知不觉来百度已有半年之久,这半年是996的半年,是孤军奋战的半年,是跌跌撞撞的半年,一个字:真的是累死人啦!

我所进入的团队相当于公司内部创业团队,人员基本全部是新招的,最初开发时连数据库都没设计,当时评审需求的时候居然有一个产品经理拿了一份他设计的数据库,当时我作为一个前端就惊呆了......

最初的前端只有我1人,这事实上与我想来学习学习的愿望是背道而驰的,但既然来都来了也只能独挑大梁,马上投入开发,当时涉及的项目有:

① H5站点

② PC站点

③ Mis后台管理系统

④ 各种百度渠道接入

第一阶段的重点为H5站点与APP,我们便需要在20天内从无到有的完成第一版的产品,而最初的Native人力严重不足,很多页面依赖于H5这边,所以前端除了本身业务之外还得约定与Native的交互细节。

这个情况下根本无暇思考其它框架,熟悉的就是最好的!便将自己git上的开源框架直接拿来用了起来:[置顶]【blade利刃出鞘】一起进入移动端webapp开发吧

因为之前的经验积累,工程化、Hybrid交互、各种兼容、体验问题已经处理了很多了,所以基础架构一层比较完备,又有完善的UI组件可以使用,这个是最初的设计构想:

构想总是美好的,而在巨大的业务压力面前任何技术愿景都是苍白的,最初我在哪里很傻很天真的用CSS3画图标,然后产品经理天天像一个苍蝇一样在我面前嗡嗡嗡,他们事实上是不关注页面性能是何物的,我也马上意识的到工期不足,于是便直接用图标了!

依赖于完善的框架,20天不到的时间,第一版的项目便结束了,业务代码有点不堪入目,页面级的代码也没有太遵循MVC规则,这导致了后续的迭代,全部在那里操作dom。

其实初期这样做问题不大,如果项目比较小(比如什么一次性的活动页面)问题也不大,但是核心项目便最好不要这样玩了,因为新需求、新场景,会让你在原基础上不断的改代码,如果页面没有一个很好的规范,那么他将不再稳定,也不再容易维护,如何编写一个可稳定、扩展性高、可维护性高的项目,是我们今天讨论的重点。

认真阅读此文可能会在以下方面对你有所帮助:

复制代码
① 网站初期需要统计什么数据?产品需要的业务数据,你该如何设计你的网站才能收集到这些数据,提供给他
② 完整的请求究竟应该如何发出,H5应该如何在前端做缓存,服务器给出的数据应该在哪里做校验,前端错误日志应该关注js错误还是数据错误?
③ 你在写业务代码时犯了什么错误,如何编写高效可维护的业务代码(页面级别),MVC到底是个什么东西?
④ 网站规模大了如何复用一些模块?
⑤ 站在业务角度应该如何做性能优化(这个可能不是本文的重点)
复制代码

文中是我半年以来的一些业务开发经验,希望对各位有用,也希望各位多多支持讨论,指出文中不足以及提出您的一些建议

统计需求

通用统计需求

对于服务器端来说,后期最重要的莫过于监控日志,对于前端来说,统计无疑是初期最重要的,通用的统计需求包括:

① PV/UV统计

② 机型/浏览器/系统统计

③ 各页面载入速度统计

④ 某些按钮的点击统计

⑤ ......

这类统计直接通过百度统计之类的工具即可,算是最基础的统计需求。百度产品的文档、支持团队烂估计是公认的事情了,我便只能挖掘很少一部分用法。但是这类数据也是非常重要了,对于产品甚至是老板判断整个产品的发展有莫大的帮助与引导作用,如果产品死了,任何技术都是没有意义的,所以站点没有这类统计的速度加上吧!

http://tongji.baidu.com/web/welcome/login

渠道统计

所谓渠道统计便是这次订单来源是哪里,就我们产品的渠道有:

① 手机百度APP入口(由分为生活+入口、首页banner入口、广告入口......)

② 百度移动站点入口

③ 百度地图入口(包括H5站点)

④ wise卡片入口(包括:唯一答案、白卡片、极速版、点到点卡片......)

⑤ 各种大礼包、活动入口

⑥ SEM入口

⑦ ......

你永远不能预料到你究竟有多少入口,但是这种渠道的统计的重要性直接关乎了产品的存亡,产品需要知道自己的每次的活动,每次的引流是有意义的,比如一次活动便需要得到这次活动每天产生的订单量,如果你告诉产品,爷做不到,那么产品会真叫你爷爷。

当然,渠道的统计前端单方面是完成不了的,需要和服务器端配合,一般而言可以这样做,前端与服务器端约定,每次请求皆会带特定的参数,我一般会与服务器约定以下参数:

复制代码
var param = {
    head: {
        us: '渠道',
        version: '1.0.0'
    }
};
复制代码

这个head参数是每次ajax请求都会带上的,而us参数一般由url而来,他要求每次由其它渠道落地到我们的站点一定要带有us参数,us参数拿到后便是我们自己的事情了,有几种操作方法:

① 直接种到cookie,这个需要服务器端特殊处理

② 存入localstorage,每次请求拿出来,组装请求参数

③ 因为我们H5站点的每一次跳转都会经过框架中转,所以我直接将us数据放到了url上,每次跳转都会带上,一直到跳出网站。

SEM需求

SEM其实属于渠道需求的一类,这里会独立出来是因为,他需要统计的数据更多,还会包含一个投放词之类的数据,SEM投放人员需要确切的知道某个投放词每天的订单量,这个时候上面的参数可能就要变化了:

复制代码
复制代码
var param = {
    head: {
        us: '渠道',
        version: '1.0.0',
        extra: '扩展字段'
    }
};
复制代码
复制代码

这个时候可能便需要一个extra的扩展字段记录投放词是什么,当然SEM落地到我们网站的特殊参数也需要一直传下去,这个需要做框架层的处理,这里顺便说下我的处理方案吧

统一跳转

首先我们H5站点基本不关注SEO,对于SEO我们有特殊的处理方案,所以在我们的H5站点上基本不会出现a标签,我们站点的每次跳转皆是由js控制,我会在框架封装几个方法处理跳转:

复制代码
复制代码
forward: function (view) {
     //处理频道内跳转
}

back: function (view) {
}

jump: function (project, view) {
     //处理跨频道跳转
}
复制代码
复制代码

这样做的好处是:

① 统一封装跳转会让前端控制力增加,比如forward可以是location变化,也可以是pushState/hash的方式做单页跳转,甚至可以做Hybrid中多Webview的跳转

② 诚如上述,forward时可以由url获取渠道参数带到下一个页面

③ 统一跳转也可以统一为站点做一些打点的操作,比如单页应用时候的统一加统计代码

最简单的理解就是:封装一个全局方法做跳转控制,所有的跳转由他发出。

请求模块

ajax是前端到服务器端的基石,但是前端和服务器端的交互:

每个接口必须要写文档!
每个接口必须要写文档!
每个接口必须要写文档!
重要的事情说三遍!!!

如果不写文档的话,你就等着吧,因为端上是入口,一旦出问题,老板会直观认为是前端的问题,如果发现是服务器的字段不统一导致,而服务器端打死不承认,你就等着吧!

无论什么时候,前端请求模块的设计是非常关键的,因为前端只是数据的搬运工,负责展现数据而已:)

封装请求模块

与封装统一跳转一致,所有的请求必须收口,最烂的做法也是封装一个全局的方法处理全站请求,这样做的好处是:

① 处理公共参数

比如每次请求必须带上上面所述head业务参数,便必须在此做处理

② 处理统一错误码

服务器与前端一般会有一个格式约定,一般而言是这样的:

{
  data: {},
  errno: 0,
  msg: "success"
}

比如错误码为1的情况就代表需要登录,系统会引导用户进入登录页,比如非0的情况下,需要弹出一个提示框告诉用户出了什么问题,你不可能在每个地方都做这种错误码处理吧

③ 统一缓存处理

有些请求数据不会经常改变,比如城市列表,比如常用联系人,这个时候便需要将之存到localstorage中做缓存

④ 数据处理、日志处理

这里插一句监控的问题,因为前端代码压缩后,js错误监控变得不太靠谱,而前端的错误有很大可能是搬运数据过程中出了问题,所以在请求model层做对应的数据校验是十分有意义的
如果发现数据不对便发错误日志,好过被用户抓住投诉,而这里做数据校验也为模板中使用数据做了基础检查

服务器端给前端的数据可能是松散的,前端真实使用时候会对数据做处理,同一请求模块如果在不同地方使用,就需要多次处理,这个是不需要的,比如:

//这个判断应该放在数据模块中
if(data.a) ...
if(data.a.b) ...

这里我说下blade框架中请求模块的处理:

blade的请求模块

我们现在站点主要还是源于blade框架,实际使用时候做了点改变,后续会回归到blade框架,项目目录结构为:

其中store依赖于storage模块,是处理localstorage缓存的,他与model是独立的,以下为核心代码:

复制代码
复制代码
define([], function () {

  var Model = _.inherit({
    //默认属性
    propertys: function () {
      this.protocol = 'http';
      this.domain = '';
      this.path = '';
      this.url = null;
      this.param = {};
      this.validates = [];
      //      this.contentType = 'application/json';

      this.ajaxOnly = true;

      this.contentType = 'application/x-www-form-urlencoded';
      this.type = 'GET';
      this.dataType = 'json';
    },

    setOption: function (options) {
      _.extend(this, options);
    },

    assert: function () {
      if (this.url === null) {
        throw 'not override url property';
      }
    },

    initialize: function (opts) {
      this.propertys();
      this.setOption(opts);
      this.assert();

    },

    pushValidates: function (handler) {
      if (typeof handler === 'function') {
        this.validates.push($.proxy(handler, this));
      }
    },

    setParam: function (key, val) {
      if (typeof key === 'object') {
        _.extend(this.param, key);
      } else {
        this.param[key] = val;
      }
    },

    removeParam: function (key) {
      delete this.param[key];
    },

    getParam: function () {
      return this.param;
    },

    //构建url请求方式,子类可复写,我们的model如果localstorage设置了值便直接读取,但是得是非正式环境
    buildurl: function () {
      //      var baseurl = AbstractModel.baseurl(this.protocol);
      //      return this.protocol + '://' + baseurl.domain + '/' + baseurl.path + (typeof this.url === 'function' ? this.url() : this.url);
      throw "[ERROR]abstract method:buildurl, must be override";

    },

    onDataSuccess: function () {
    },

    /**
    *    取model数据
    *    @param {Function} onComplete 取完的回调函
    *    传入的第一个参数为model的数第二个数据为元数据,元数据为ajax下发时的ServerCode,Message等数
    *    @param {Function} onError 发生错误时的回调
    *    @param {Boolean} ajaxOnly 可选,默认为false当为true时只使用ajax调取数据
    * @param {Boolean} scope 可选,设定回调函数this指向的对象
    * @param {Function} onAbort 可选,但取消时会调用的函数
    */
    execute: function (onComplete, onError, ajaxOnly, scope) {
      var __onComplete = $.proxy(function (data) {
        var _data = data;
        if (typeof data == 'string') _data = JSON.parse(data);

        // @description 开发者可以传入一组验证方法进行验证
        for (var i = 0, len = this.validates.length; i < len; i++) {
          if (!this.validates[i](data)) {
            // @description 如果一个验证不通过就返回
            if (typeof onError === 'function') {
              return onError.call(scope || this, _data, data);
            } else {
              return false;
            }
          }
        }

        // @description 对获取的数据做字段映射
        var datamodel = typeof this.dataformat === 'function' ? this.dataformat(_data) : _data;

        if (this.onDataSuccess) this.onDataSuccess.call(this, datamodel, data);
        if (typeof onComplete === 'function') {
          onComplete.call(scope || this, datamodel, data);
        }

      }, this);

      var __onError = $.proxy(function (e) {
        if (typeof onError === 'function') {
          onError.call(scope || this, e);
        }
      }, this);

      this.sendRequest(__onComplete, __onError);

    },

    sendRequest: function (success, error) {
      var url = this.buildurl();
      var params = _.clone(this.getParam() || {});
      var crossDomain = {
        'json': true,
        'jsonp': true
      };

      //      if (this.type == 'json')
      //      if (this.type == 'POST') {
      //        this.dataType = 'json';
      //      } else {
      //        this.dataType = 'jsonp';
      //      }

      if (this.type == 'POST') {
        this.dataType = 'json';
      }

      //jsonp与post互斥
      $.ajax({
        url: url,
        type: this.type,
        data: params,
        dataType: this.dataType,
        contentType: this.contentType,
        crossDomain: crossDomain[this.dataType],
        timeout: 50000,
        xhrFields: {
          withCredentials: true
        },
        success: function (res) {
          success && success(res);
        },
        error: function (err) {
          error && error(err);
        }
      });

    }

  });

  Model.getInstance = function () {
    if (this.instance) {
      return this.instance;
    } else {
      return this.instance = new this();
    }
  };

  return Model;
});

model
复制代码

 

复制代码
define(['AbstractStorage'], function (AbstractStorage) {

  var Store = _.inherit({
    //默认属性
    propertys: function () {

      //每个对象一定要具有存储键,并且不能重复
      this.key = null;

      //默认一条数据的生命周期,S为秒,M为分,D为天
      this.lifeTime = '30M';

      //默认返回数据
      //      this.defaultData = null;

      //代理对象,localstorage对象
      this.sProxy = new AbstractStorage();

    },

    setOption: function (options) {
      _.extend(this, options);
    },

    assert: function () {
      if (this.key === null) {
        throw 'not override key property';
      }
      if (this.sProxy === null) {
        throw 'not override sProxy property';
      }
    },

    initialize: function (opts) {
      this.propertys();
      this.setOption(opts);
      this.assert();
    },

    _getLifeTime: function () {
      var timeout = 0;
      var str = this.lifeTime;
      var unit = str.charAt(str.length - 1);
      var num = str.substring(0, str.length - 1);
      var Map = {
        D: 86400,
        H: 3600,
        M: 60,
        S: 1
      };
      if (typeof unit == 'string') {
        unit = unit.toUpperCase();
      }
      timeout = num;
      if (unit) timeout = Map[unit];

      //单位为毫秒
      return num * timeout * 1000 ;
    },

    //缓存数据
    set: function (value, sign) {
      //获取过期时间
      var timeout = new Date();
      timeout.setTime(timeout.getTime() + this._getLifeTime());
      this.sProxy.set(this.key, value, timeout.getTime(), sign);
    },

    //设置单个属性
    setAttr: function (name, value, sign) {
      var key, obj;
      if (_.isObject(name)) {
        for (key in name) {
          if (name.hasOwnProperty(key)) this.setAttr(k, name[k], value);
        }
        return;
      }

      if (!sign) sign = this.getSign();

      //获取当前对象
      obj = this.get(sign) || {};
      if (!obj) return;
      obj[name] = value;
      this.set(obj, sign);

    },

    getSign: function () {
      return this.sProxy.getSign(this.key);
    },

    remove: function () {
      this.sProxy.remove(this.key);
    },

    removeAttr: function (attrName) {
      var obj = this.get() || {};
      if (obj[attrName]) {
        delete obj[attrName];
      }
      this.set(obj);
    },

    get: function (sign) {
      var result = [], isEmpty = true, a;
      var obj = this.sProxy.get(this.key, sign);
      var type = typeof obj;
      var o = { 'string': true, 'number': true, 'boolean': true };
      if (o[type]) return obj;

      if (_.isArray(obj)) {
        for (var i = 0, len = obj.length; i < len; i++) {
          result[i] = obj[i];
        }
      } else if (_.isObject(obj)) {
        result = obj;
      }

      for (a in result) {
        isEmpty = false;
        break;
      }
      return !isEmpty ? result : null;
    },

    getAttr: function (attrName, tag) {
      var obj = this.get(tag);
      var attrVal = null;
      if (obj) {
        attrVal = obj[attrName];
      }
      return attrVal;
    }

  });

  Store.getInstance = function () {
    if (this.instance) {
      return this.instance;
    } else {
      return this.instance = new this();
    }
  };

  return Store;
});

store
复制代码

 

复制代码
复制代码
复制代码
define([], function () {

  var Storage = _.inherit({
    //默认属性
    propertys: function () {

      //代理对象,默认为localstorage
      this.sProxy = window.localStorage;

      //60 * 60 * 24 * 30 * 1000 ms ==30天
      this.defaultLifeTime = 2592000000;

      //本地缓存用以存放所有localstorage键值与过期日期的映射
      this.keyCache = 'SYSTEM_KEY_TIMEOUT_MAP';

      //当缓存容量已满,每次删除的缓存数
      this.removeNum = 5;

    },

    assert: function () {
      if (this.sProxy === null) {
        throw 'not override sProxy property';
      }
    },

    initialize: function (opts) {
      this.propertys();
      this.assert();
    },

    /*
    新增localstorage
    数据格式包括唯一键值,json字符串,过期日期,存入日期
    sign 为格式化后的请求参数,用于同一请求不同参数时候返回新数据,比如列表为北京的城市,后切换为上海,会判断tag不同而更新缓存数据,tag相当于签名
    每一键值只会缓存一条信息
    */
    set: function (key, value, timeout, sign) {
      var _d = new Date();
      //存入日期
      var indate = _d.getTime();

      //最终保存的数据
      var entity = null;

      if (!timeout) {
        _d.setTime(_d.getTime() + this.defaultLifeTime);
        timeout = _d.getTime();
      }

      //
      this.setKeyCache(key, timeout);
      entity = this.buildStorageObj(value, indate, timeout, sign);

      try {
        this.sProxy.setItem(key, JSON.stringify(entity));
        return true;
      } catch (e) {
        //localstorage写满时,全清掉
        if (e.name == 'QuotaExceededError') {
          //            this.sProxy.clear();
          //localstorage写满时,选择离过期时间最近的数据删除,这样也会有些影响,但是感觉比全清除好些,如果缓存过多,此过程比较耗时,100ms以内
          if (!this.removeLastCache()) throw '本次数据存储量过大';
          this.set(key, value, timeout, sign);
        }
        console && console.log(e);
      }
      return false;
    },

    //删除过期缓存
    removeOverdueCache: function () {
      var tmpObj = null, i, len;

      var now = new Date().getTime();
      //取出键值对
      var cacheStr = this.sProxy.getItem(this.keyCache);
      var cacheMap = [];
      var newMap = [];
      if (!cacheStr) {
        return;
      }

      cacheMap = JSON.parse(cacheStr);

      for (i = 0, len = cacheMap.length; i < len; i++) {
        tmpObj = cacheMap[i];
        if (tmpObj.timeout < now) {
          this.sProxy.removeItem(tmpObj.key);
        } else {
          newMap.push(tmpObj);
        }
      }
      this.sProxy.setItem(this.keyCache, JSON.stringify(newMap));

    },

    removeLastCache: function () {
      var i, len;
      var num = this.removeNum || 5;

      //取出键值对
      var cacheStr = this.sProxy.getItem(this.keyCache);
      var cacheMap = [];
      var delMap = [];

      //说明本次存储过大
      if (!cacheStr) return false;

      cacheMap.sort(function (a, b) {
        return a.timeout - b.timeout;
      });

      //删除了哪些数据
      delMap = cacheMap.splice(0, num);
      for (i = 0, len = delMap.length; i < len; i++) {
        this.sProxy.removeItem(delMap[i].key);
      }

      this.sProxy.setItem(this.keyCache, JSON.stringify(cacheMap));
      return true;
    },

    setKeyCache: function (key, timeout) {
      if (!key || !timeout || timeout < new Date().getTime()) return;
      var i, len, tmpObj;

      //获取当前已经缓存的键值字符串
      var oldstr = this.sProxy.getItem(this.keyCache);
      var oldMap = [];
      //当前key是否已经存在
      var flag = false;
      var obj = {};
      obj.key = key;
      obj.timeout = timeout;

      if (oldstr) {
        oldMap = JSON.parse(oldstr);
        if (!_.isArray(oldMap)) oldMap = [];
      }

      for (i = 0, len = oldMap.length; i < len; i++) {
        tmpObj = oldMap[i];
        if (tmpObj.key == key) {
          oldMap[i] = obj;
          flag = true;
          break;
        }
      }
      if (!flag) oldMap.push(obj);
      //最后将新数组放到缓存中
      this.sProxy.setItem(this.keyCache, JSON.stringify(oldMap));

    },

    buildStorageObj: function (value, indate, timeout, sign) {
      var obj = {
        value: value,
        timeout: timeout,
        sign: sign,
        indate: indate
      };
      return obj;
    },

    get: function (key, sign) {
      var result, now = new Date().getTime();
      try {
        result = this.sProxy.getItem(key);
        if (!result) return null;
        result = JSON.parse(result);

        //数据过期
        if (result.timeout < now) return null;

        //需要验证签名
        if (sign) {
          if (sign === result.sign)
            return result.value;
          return null;
        } else {
          return result.value;
        }

      } catch (e) {
        console && console.log(e);
      }
      return null;
    },

    //获取签名
    getSign: function (key) {
      var result, sign = null;
      try {
        result = this.sProxy.getItem(key);
        if (result) {
          result = JSON.parse(result);
          sign = result && result.sign
        }
      } catch (e) {
        console && console.log(e);
      }
      return sign;
    },

    remove: function (key) {
      return this.sProxy.removeItem(key);
    },

    clear: function () {
      this.sProxy.clear();
    }
  });

  Storage.getInstance = function () {
    if (this.instance) {
      return this.instance;
    } else {
      return this.instance = new this();
    }
  };

  return Storage;

});

storage
复制代码

 

  storage
复制代码

真实的使用场景业务model首先得做一层业务封装,然后才是真正的使用:

复制代码
复制代码
define(['AbstractModel', 'AbstractStore', 'cUser'], function (AbstractModel, AbstractStore, cUser) {

    var ERROR_CODE = {
        'NOT_LOGIN': '00001'
    };

    //获取产品来源
    var getUs = function () {
        var us = 'webapp';
        //其它操作......

        //如果url具有us标志,则首先读取
        if (_.getUrlParam().us) {
            us = _.getUrlParam().us;
        }
        return us;
    };

    var BaseModel = _.inherit(AbstractModel, {

        initDomain: function () {
            var host = window.location.host;

            this.domain = host;

            //开发环境
            if (host.indexOf('yexiaochai.baidu.com') != -1) {
                this.domain = 'xxx';
            }

            //qa环境
            if (host.indexOf('baidu.com') == -1) {
                this.domain = 'xxx';
            }

            //正式环境
            if (host.indexOf('xxx.baidu.com') != -1 || host.indexOf('xxx.baidu.com') != -1) {
                this.domain = 'api.xxx.baidu.com';
            }

        },

        propertys: function ($super) {
            $super();

            this.initDomain();

            this.path = '';

            this.cacheData = null;
            this.param = {
                head: {
                    us: getUs(),
                    version: '1.0.0'
                }
            };
            this.dataType = 'jsonp';

            this.errorCallback = function () { };

            //统一处理分返回验证
            this.pushValidates(function (data) {
                return this.baseDataValidate(data);
            });

        },

        //首轮处理返回数据,检查错误码做统一验证处理
        baseDataValidate: function (data) {
            if (!data) {
                window.APP.showToast('服务器出错,请稍候再试', function () {
                    window.location.href = 'xxx';
                });
                return;
            }

            if (_.isString(data)) data = JSON.parse(data);
            if (data.errno === 0) return true;

            //处理统一登录逻辑
            if (data.errno == ERROR_CODE['NOT_LOGIN']) {
                cUser.login();
            }

            //其它通用错误码的处理逻辑
            if (data.errno == xxxx) {
                this.errorCallback();
                return false;
            }

            //如果出问题则打印错误
            if (window.APP && data && data.msg) window.APP.showToast(data.msg, this.errorCallback);

            return false;
        },

        dataformat: function (data) {
            if (_.isString(data)) data = JSON.parse(data);
            if (data.data) return data.data;
            return data;
        },

        buildurl: function () {
            return this.protocol + '://' + this.domain + this.path + (typeof this.url === 'function' ? this.url() : this.url);
        },

        getSign: function () {
            var param = this.getParam() || {};
            return JSON.stringify(param);
        },

        onDataSuccess: function (fdata, data) {
            if (this.cacheData && this.cacheData.set)
                this.cacheData.set(fdata, this.getSign());
        },

        //重写父类getParam方法,加入方法签名
        getParam: function () {
            var param = _.clone(this.param || {});

            //此处对参数进行特殊处理
            //......

            return this.param;
        },

        execute: function ($super, onComplete, onError, ajaxOnly, scope) {
            var data = null;
            if (!ajaxOnly && !this.ajaxOnly && this.cacheData && this.cacheData.get) {
                data = this.cacheData.get(this.getSign());
                if (data) {
                    onComplete(data);
                    return;
                }
            }

            //记录请求发出
            $super(onComplete, onError, ajaxOnly, scope);
        }

    });

    //localstorage存储类
    var Store = {
        RequestStore: _.inherit(AbstractStore, {
            //默认属性
            propertys: function ($super) {
                $super();
                this.key = 'BUS_RequestStore';
                this.lifeTime = '1D'; //缓存时间
            }
        })
    };

    //返回真实的业务类
    return {
        //真实的业务请求
        requestModel: _.inherit(BaseModel, {
            //默认属性
            propertys: function ($super) {
                $super();
                this.url = '/url';
                this.ajaxOnly = false;
                this.cacheData = Store.RequestStore.getInstance();
            }
        })
    };
});

业务封装
复制代码

 

  业务封装
复制代码
复制代码
复制代码
 1 define(['BusinessModel'], function (Model) {
 2     var model = Model.requestModel.getInstance();
 3 
 4     //设置请求参数
 5     model.setParam();
 6     model.execute(function (data) {
 7         //这里的data,如果model设置的完善,则前端使用可完全信任其可用性不用做判断了
 8 
 9         //这个是不需要的
10         if (data.person && data.person.name) {
11             //...
12         }
13 
14         //根据数据渲染页面
15         //......
16     });
17 })
复制代码
复制代码

复杂的前端页面

我觉得三端的开发中,前端的业务是最复杂的,因为IOS与Andriod的落地页往往都是首页,而前端的落地页可能是任何页面(产品列表页,订单填写页,订单详情页等),因为用户完全可能把这个url告诉朋友,让朋友直接进入这个产品填写页。

而随着业务发展、需求迭代,前端的页面可能更加复杂,最初稳定的页面承受了来自多方的挑战。这个情况在我们团队大概是这样的:

在第一轮产品做完后,产品马上安排了第二轮迭代,这次迭代的重点是订单填写页,对订单填写有以下需求:

① 新增优惠券功能

② 优惠券在H5站点下默认不使用,在IOS、andriod下默认使用(刚好这个时候IOS还在用H5的页面囧囧囧)

③ 默认自动填入用户上一次的信息(站点常用功能)

这里1、3是正常功能迭代,但是需求2可以说是IOS APP 暂时使用H5站点的页面,因为当时IOS已经招到了足够的人,也正在进行订单填写的开发,事实上一个月以后他们APP便换掉了H5的订单填写,那么这个时候将对应IOS的逻辑写到自己的主逻辑中是非常愚蠢的,而且后续的发展更是超出了所料,因为H5站点的容器变成了:

① IOS APP装载部分H5页面

② Andriod APP装载部分H5页面

PS:这里之所以把andriod和ios分开,因为andriod都开发了20多天了,ios才招到一个人,他们对H5页面的需求完全是两回事囧!

③ 手机百度装载H5页面(基本与H5站点逻辑一致,有一些特殊需求,比如登录、支付需要使用clouda调用apk)

④ 百度地图webview容器

于是整个人就一下傻逼了,因为主逻辑基本相似,总有容器会希望一点特殊需求,从重构角度来说,我们不会希望我们的业务中出现上述代码太多的if else;

从性能优化角度来说,就普通浏览器根本不需要理睬Hybrid交互相关,这个时候我们完善的框架便派上了用场,抽离公共部分了:

H5仍然只关注主逻辑,并且将内部的每部操作尽可能的细化,比如初始化操作,对某一个按钮的点击行为等都应该尽可能的分解到一个个独立的方法中,真实项目大概是这个样子的:

依赖框架自带的继承抽象,以及控制器路由层的按环境加载的机制,可以有效解决此类问题,也有效降低了页面的复杂度,但是他改变不了页面越来越复杂的事实,并且这个时候迎来了第三轮迭代:

① 加入保险功能

② H5站点在某些渠道下默认开启使用优惠券功能(囧囧囧!!!)

③ 限制优惠券必须达到某些条件才能使用

④ 订单填写页作为某一合作方的落地页,请求参数和url有所变化,但是返回的字段一致,交互一致......

因为最初20天的慌乱处理,加之随后两轮的迭代,我已经在订单填写页中买下了太多坑,而且网页中随处可见的dom操作让代码可维护程度大大降低,而点击某一按钮而导致的连锁变化经常发生,比如,用户增减购买商品数量时:

① 会改变本身商品数量的展示

② 会根据当前条件去刷新优惠卷使用数据

③ 改变支付条上的最终总额

④ ......

于是这次迭代后,你会发现订单填写页尼玛经常出BUG,每次改了又会有地方出BUG,一段时间不在,同事帮助修复了一个BUG,又引起了其它三个BUG,这个时候迎来了第四轮迭代,而这种种迹象表明:

如果一个页面开始频繁的出BUG,如果一个页面逻辑越来越复杂,如果一个页面的代码你觉得不好维护了,那么意味着,他应该得到应有的重构了!

前端的MVC

不太MVC的做法

如果在你的页面(会长久维护的项目)中有以下情况的话,也许你应该重构你的页面或者换掉你框架了:

① 在js中大规模的拼接HTML,比如这样:

复制代码
复制代码
 1 for (i = 0; i < len; i++) {
 2     for (key in data[i]) {
 3         item = data[i][key];
 4         len2 = item.length;
 5         if (len2 === 0) continue;
 6         str += '<h2 class="wa-xxx-groupname">' + key + '</h2>';
 7         str += '<ul class=" wa-xxx-city-list-item ">';
 8         for (j = 0; j < len2; j++) {
 9             str += '<li data-type="' + item[j].type + '" data-city="' + item[j].regionid + '">' + item[j].cnname + '</li>';
10         }
11         str += '</ul>';
12         break;
13     }
14     if (str !== '')
15         html.push('<div class="wa-xxx-city-list">' + str + '</div>');
16     str = '';
17 }
复制代码
复制代码

对于这个情况,你应该使用前端模板引擎

② 在js中出现大规模的获取非文本框元素的值

③ 在html页面中看到了大规模的数据钩子,比如这个样子:

④ 你在js中发现,一个数据由js变量可获取,也可以由dom获取,并你对从哪获取数据犹豫不决

⑤ 在你的页面中,click事件分散到一个页面的各个地方

⑥ 当你的js文件超过1000行,并且你觉得没法拆分

以上种种迹象表明,哟!这个页面好像要被玩坏了,好像可以用MVC的思想重构一下啦!

什么是MVC

其实MVC这个东西有点悬,一般人压根都不知道他是干嘛的,就知道一个model-view-controller;

知道一点的又说不清楚;

真正懂的人要么喜欢东扯西扯,要么不愿意写博客或者博客一来便很难,曲高和寡。

所以前端MVC这个东西一直是一个玄之又玄的东西,很多开发了很久的朋友都不能了解什么是MVC。

今天我作为一个自认为懂得一点的人,便来说一说我对MVC在前端的认识,希望对大家有帮助。

前端给大家的认识便是页面,页面由HTML+CSS实现,如果有交互便需要JS的介入,其中:

对于真实的业务来说,HTML&CSS是零件,JS是搬运工,数据是设计图与指令。
JS要根据数据指令将零件组装为玩具,用户操作了玩具导致了数据变化,于是JS又根据数据指令重新组装玩具
我们事实上不写代码,我们只是数据的搬运工

上述例子可能不一定准确,但他可以表达一些中心思想,那就是:

对于页面来说,要展示的只是数据

所以,数据才是我们应该关注的核心,这里回到我们MVC的基本概念:

MVC即Model-View-Controller三个词的缩写

Model

是数据模型,是客观事物的一种抽象,比如机票订单填写的常用联系人模块便可以抽象为一个Model类,他会有一次航班最多可选择多少联系人这种被当前业务限制的属性,并且会有增减联系人、获取联系人、获取最大可设置联系人等业务数据。

Model应该是一个比较稳定的模块,不会经常变化并且可被重用的模块;当然最重要的是,每一次数据变化便会有一个通知机制,通知所有的controller对数据变化做出响应

View

View就是视图,在前端中甚至可简单理解为html模板,Controller会根据数据组装为最终的html字符串,然后展示给我们,至于怎么展示是CSS的事情,我们这里不太关注。

PS:一般来说,过于复杂的if else流程判断,不应该出现在view中,那是controller该做的事情

当然并不是每次model变化controller都需要完整的渲染页面,也有可能一次model改变,其响应的controller只是操作了一次dom,只要model的controller足够细分,每个controller就算是在操作dom也是无所谓的

Controller

控制器其实就是负责与View以及Model打交道的,因为View与Model应该没有任何交互,model中不会出现html标签,html标签也不应该出现完整的model对应数据,更不会有model数据的增删

PS:html标签当然需要一些关键model值用于controller获取model相关标志了

这里拷贝一个图示来帮助我们解析:

这个图基本可以表达清楚MVC是干嘛的,但是却不能帮助新手很好的了解什么是MVC,因为真实的场景可能是这样的:

一个model实例化完毕,通知controller1去更新了view

view发生了click交互通过controller2改变了model的值

model马上通知了controller3、controller4、controller5响应数据变化

所以这里controller影响的model可能不止一个,而model通知的controller也不止一个,会引起的界面连锁反应,上图可能会误导初学者只有一个controller在做这些事情。

这里举一个简单的例子说明情况:

① 大家看到新浪微博首页,你发了一条微博,这个时候你关注的好友转发了该微博

② 服务器响应这次微博,并且将这次新增微博推送给了你(也有可能是页面有一个js不断轮询去拉取数据),总之最后数据变了,你的微博Model马上将这次数据变化通知了至少以下响应程序:

1)消息通知控制器,他引起了右上角消息变化,用户看见了有人转发我的weib

2)微博主页面显示多了一条微博,让我们点击查看

3)......

这是一条微博新增产生的变化,如果页面想再多一个模块响应变化,只需要在微博Model的控制器集合中新增一个控制器即可

MVC的实现

千言不如一码,我这里临时设计一个例子并书写代码来说明自己对MVC的认识,,考虑到简单,便不使用模块化了,我们设计了一个博客页面,大概是这个样子的:

无论什么功能,都需要第三方库,我们这里选择了:

① zepto

② underscore

这里依旧用到了我们的继承机制,如果对这个不熟悉的朋友烦请看看我之前的博客:【一次面试】再谈javascript中的继承

Model的实现

我们只是数据的搬运工,所以要以数据为先,这里先设计了Model的基类:

复制代码
复制代码
var AbstractModel = _.inherit({
  initialize: function (opts) {
    this.propertys();
    this.setOption(opts);
  },

  propertys: function () {
    //只取页面展示需要数据
    this.data = {};

    //局部数据改变对应的响应程序,暂定为一个方法
    //可以是一个类的实例,如果是实例必须有render方法
    this.controllers = {};

    //全局初始化数据时候调用的控制器
    this.initController = null;

    this.scope = null;

  },

  addController: function (k, v) {
    if (!k || !v) return;
    this.controllers[k] = v;
  },

  removeController: function (k) {
    if (!k) return;
    delete this.controllers[k];
  },

  setOption: function (opts) {
    for (var k in opts) {
      this[k] = opts[k];
    }
  },

  //首次初始化时,需要矫正数据,比如做服务器适配
  //@override
  handleData: function () { },

  //一般用于首次根据服务器数据源填充数据
  initData: function (data) {
    var k;
    if (!data) return;

    //如果默认数据没有被覆盖可能有误
    for (k in this.data) {
      if (data[k]) this.data[k] = data[k];
    }

    this.handleData();

    if (this.initController && this.get()) {
      this.initController.call(this.scope, this.get());
    }

  },

  //验证data的有效性,如果无效的话,不应该进行以下逻辑,并且应该报警
  //@override
  validateData: function () {
    return true;
  },

  //获取数据前,可以进行格式化
  //@override
  formatData: function (data) {
    return data;
  },

  //获取数据
  get: function () {
    if (!this.validateData()) {
      //需要log
      return {};
    }
    return this.formatData(this.data);
  },

  _update: function (key, data) {
    if (typeof this.controllers[key] === 'function')
      this.controllers[key].call(this.scope, data);
    else if (typeof this.controllers[key].render === 'function')
      this.controllers[key].render.call(this.scope, data);
  },

  //数据跟新后需要做的动作,执行对应的controller改变dom
  //@override
  update: function (key) {
    var data = this.get();
    var k;
    if (!data) return;

    if (this.controllers[key]) {
      this._update(key, data);
      return;
    }

    for (k in this.controllers) {
      this._update(k, data);
    }
  }
});
复制代码

 

  View Code
复制代码

然后我们开始设计真正的博客相关model:

复制代码
复制代码
 1 //博客的model模块应该是完全独立与页面的主流层的,并且可复用
 2 var Model = _.inherit(AbstractModel, {
 3   propertys: function () {
 4     this.data = {
 5       blogs: []
 6     };
 7   },
 8   //新增博客
 9   add: function (title, type, label) {
10     //做数据校验,具体要多严格由业务决定
11     if (!title || !type) return null;
12 
13     var blog = {};
14     blog.id = 'blog_' + _.uniqueId();
15     blog.title = title;
16     blog.type = type;
17     if (label) blog.label = label.split(',');
18     else blog.label = [];
19 
20     this.data.blogs.push(blog);
21 
22     //通知各个控制器变化
23     this.update();
24 
25     return blog;
26   },
27   //删除某一博客
28   remove: function (id) {
29     if (!id) return null;
30     var i, len, data;
31     for (i = 0, len = this.data.blogs.length; i < len; i++) {
32       if (this.data.blogs[i].id === id) {
33         data = this.data.blogs.splice(i, 1)
34         this.update();
35         return data;
36       }
37     }
38     return null;
39   },
40   //获取所有类型映射表
41   getTypeInfo: function () {
42     var obj = {};
43     var i, len, type;
44     for (i = 0, len = this.data.blogs.length; i < len; i++) {
45       type = this.data.blogs[i].type;
46       if (!obj[type]) obj[type] = 1;
47       else obj[type] = obj[type] + 1;
48     }
49     return obj;
50   },
51   //获取标签映射表
52   getLabelInfo: function () {
53     var obj = {}, label;
54     var i, len, j, len1, blog, label;
55     for (i = 0, len = this.data.blogs.length; i < len; i++) {
56       blog = this.data.blogs[i];
57       for (j = 0, len1 = blog.label.length; j < len1; j++) {
58         label = blog.label[j];
59         if (!obj[label]) obj[label] = 1;
60         else obj[label] = obj[label] + 1;
61       }
62     }
63     return obj;
64   },
65   //获取总数
66   getNum: function () {
67     return this.data.blogs.length;
68   }
69 
70 });
复制代码
复制代码

这个时候再附上业务代码:

复制代码
复制代码
var AbstractView = _.inherit({
  propertys: function () {
    this.$el = $('#main');
    //事件机制
    this.events = {};
  },
  initialize: function (opts) {
    //这种默认属性
    this.propertys();
  },
  $: function (selector) {
    return this.$el.find(selector);
  },
  show: function () {
    this.$el.show();
    this.bindEvents();
  },
  bindEvents: function () {
    var events = this.events;

    if (!(events || (events = _.result(this, 'events')))) return this;
    this.unBindEvents();

    // 解析event参数的正则
    var delegateEventSplitter = /^(\S+)\s*(.*)$/;
    var key, method, match, eventName, selector;

    // 做简单的字符串数据解析
    for (key in events) {
      method = events[key];
      if (!_.isFunction(method)) method = this[events[key]];
      if (!method) continue;

      match = key.match(delegateEventSplitter);
      eventName = match[1], selector = match[2];
      method = _.bind(method, this);
      eventName += '.delegateUIEvents' + this.id;

      if (selector === '') {
        this.$el.on(eventName, method);
      } else {
        this.$el.on(eventName, selector, method);
      }
    }
    return this;
  },

  unBindEvents: function () {
    this.$el.off('.delegateUIEvents' + this.id);
    return this;
  }

});

View的基类
复制代码

 

  View的基类
复制代码
复制代码
复制代码
 1 //页面主流程
 2 var View = _.inherit(AbstractView, {
 3   propertys: function ($super) {
 4     $super();
 5     this.$el = $('#main');
 6 
 7     //统合页面所有点击事件
 8     this.events = {
 9       'click .js_add': 'blogAddAction',
10       'click .js_blog_del': 'blogDeleteAction'
11     };
12 
13     //实例化model并且注册需要通知的控制器
14     //控制器务必做到职责单一
15     this.model = new Model({
16       scope: this,
17       controllers: {
18         numController: this.numController,
19         typeController: this.typeController,
20         labelController: this.labelController,
21         blogsController: this.blogsController
22       }
23     });
24   },
25   //总博客数
26   numController: function () {
27     this.$('.js_num').html(this.model.getNum());
28   },
29   //分类数
30   typeController: function () {
31     var html = '';
32     var tpl = document.getElementById('js_tpl_kv').innerHTML;
33     var data = this.model.getTypeInfo();
34     html = _.template(tpl)({ objs: data });
35     this.$('.js_type_wrapper').html(html);
36 
37 
38   },
39   //label分类
40   labelController: function () {
41     //这里的逻辑与type基本一致,但是真实情况不会这样
42     var html = '';
43     var tpl = document.getElementById('js_tpl_kv').innerHTML;
44     var data = this.model.getLabelInfo();
45     html = _.template(tpl)({ objs: data });
46     this.$('.js_label_wrapper').html(html);
47 
48   },
49   //列表变化
50   blogsController: function () {
51     console.log(this.model.get());
52     var html = '';
53     var tpl = document.getElementById('js_tpl_blogs').innerHTML;
54     var data = this.model.get();
55     html = _.template(tpl)(data);
56     this.$('.js_blogs_wrapper').html(html);
57   },
58   //添加博客点击事件
59   blogAddAction: function () {
60     //此处未做基本数据校验,因为校验的工作应该model做,比如字数限制,标签过滤什么的
61     //这里只是往model中增加一条数据,事实上这里还应该写if预计判断是否添加成功,略去
62     this.model.add(
63       this.$('.js_title').val(),
64       this.$('.js_type').val(),
65       this.$('.js_label').val()
66     );
67 
68   },
69   blogDeleteAction: function (e) {
70     var el = $(e.currentTarget);
71     this.model.remove(el.attr('data-id'));
72   }
73 });
74 
75 var view = new View();
76 view.show();
复制代码
复制代码

完整代码&示例

复制代码
复制代码

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>前端MVC</title>
<script src="zepto.js" type="text/javascript"></script>
<script src="underscore.js" type="text/javascript"></script>
<style>
li {
list-style: none;
margin: 5px 0;
}
fieldset {
margin: 5px 0;
}
</style>
</head>
<body>
<div id="main">
<fieldset>
<legend>文章总数</legend>
<div class="js_num">
0
</div>
</fieldset>
<fieldset>
<legend>分类</legend>
<div class="js_type_wrapper">
</div>
</fieldset>
<fieldset>
<legend>标签</legend>
<div class="js_label_wrapper">
</div>
</fieldset>
<fieldset>
<legend>博客列表</legend>
<div class="js_blogs_wrapper">
</div>
</fieldset>
<fieldset>
<legend>新增博客</legend>
<ul>
<li>标题 </li>
<li>
<input type="text" class="js_title" />
</li>
<li>类型 </li>
<li>
<input type="text" class="js_type" />
</li>
<li>标签(逗号隔开) </li>
<li>
<input type="text" class="js_label" />
</li>
<li>
<input type="button" class="js_add" value="新增博客" />
</li>
</ul>
</fieldset>
</div>
<script type="text/template" id="js_tpl_kv">
<ul>
<%for(var k in objs){ %>
<li><%=k %>(<%=objs[k] %>)</li>
<%} %>
</ul>
</script>
<script type="text/template" id="js_tpl_blogs">
<ul>
<%for(var i = 0, len = blogs.length; i < len; i++ ){ %>
<li><%=blogs[i].title %> - <span class="js_blog_del" data-id="<%=blogs[i].id %>">删除</span></li>
<%} %>
</ul>
</script>
<script type="text/javascript">

//继承相关逻辑
(function () {

// 全局可能用到的变量
var arr = [];
var slice = arr.slice;
/**
* inherit方法,js的继承,默认为两个参数
*
* @param {function} origin 可选,要继承的类
* @param {object} methods 被创建类的成员,扩展的方法和属性
* @return {function} 继承之后的子类
*/
_.inherit = function (origin, methods) {

// 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
if (arguments.length === 0 || arguments.length > 2) throw '参数错误';

var parent = null;

// 将参数转换为数组
var properties = slice.call(arguments);

// 如果第一个参数为类(function),那么就将之取出
if (typeof properties[0] === 'function')
parent = properties.shift();
properties = properties[0];

// 创建新类用于返回
function klass() {
if (_.isFunction(this.initialize))
this.initialize.apply(this, arguments);
}

klass.superclass = parent;

// 父类的方法不做保留,直接赋给子类
// parent.subclasses = [];

if (parent) {
// 中间过渡类,防止parent的构造函数被执行
var subclass = function () { };
subclass.prototype = parent.prototype;
klass.prototype = new subclass();

// 父类的方法不做保留,直接赋给子类
// parent.subclasses.push(klass);
}

var ancestor = klass.superclass && klass.superclass.prototype;
for (var k in properties) {
var value = properties[k];

//满足条件就重写
if (ancestor && typeof value == 'function') {
var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/g, '').split(',');
//只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
if (argslist[0] === '$super' && ancestor[k]) {
value = (function (methodName, fn) {
return function () {
var scope = this;
var args = [
function () {
return ancestor[methodName].apply(scope, arguments);
}
];
return fn.apply(this, args.concat(slice.call(arguments)));
};
})(k, value);
}
}

//此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
//原型链是共享的,这里处理逻辑要改
var temp = {};
_.extend(temp, klass.prototype[k]);
_.extend(temp, value);
klass.prototype[k] = temp;
} else {
klass.prototype[k] = value;
}
}

//静态属性继承
//兼容代码,非原型属性也需要进行继承
for (key in parent) {
if (parent.hasOwnProperty(key) && key !== 'prototype' && key !== 'superclass')
klass[key] = parent[key];
}

if (!klass.prototype.initialize)
klass.prototype.initialize = function () { };

klass.prototype.constructor = klass;

return klass;
};

})();
</script>
<script type="text/javascript">
//基类view设计
var AbstractView = _.inherit({
propertys: function () {
this.$el = $('#main');
//事件机制
this.events = {};
},
initialize: function (opts) {
//这种默认属性
this.propertys();
},
$: function (selector) {
return this.$el.find(selector);
},
show: function () {
this.$el.show();
this.bindEvents();
},
bindEvents: function () {
var events = this.events;

if (!(events || (events = _.result(this, 'events')))) return this;
this.unBindEvents();

// 解析event参数的正则
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
var key, method, match, eventName, selector;

// 做简单的字符串数据解析
for (key in events) {
method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;

match = key.match(delegateEventSplitter);
eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateUIEvents' + this.id;

if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
return this;
},

unBindEvents: function () {
this.$el.off('.delegateUIEvents' + this.id);
return this;
}

});

//基类Model设计
var AbstractModel = _.inherit({
initialize: function (opts) {
this.propertys();
this.setOption(opts);
},

propertys: function () {
//只取页面展示需要数据
this.data = {};

//局部数据改变对应的响应程序,暂定为一个方法
//可以是一个类的实例,如果是实例必须有render方法
this.controllers = {};

//全局初始化数据时候调用的控制器
this.initController = null;

this.scope = null;

},

addController: function (k, v) {
if (!k || !v) return;
this.controllers[k] = v;
},

removeController: function (k) {
if (!k) return;
delete this.controllers[k];
},

setOption: function (opts) {
for (var k in opts) {
this[k] = opts[k];
}
},

//首次初始化时,需要矫正数据,比如做服务器适配
//@override
handleData: function () { },

//一般用于首次根据服务器数据源填充数据
initData: function (data) {
var k;
if (!data) return;

//如果默认数据没有被覆盖可能有误
for (k in this.data) {
if (data[k]) this.data[k] = data[k];
}

this.handleData();

if (this.initController && this.get()) {
this.initController.call(this.scope, this.get());
}

},

//验证data的有效性,如果无效的话,不应该进行以下逻辑,并且应该报警
//@override
validateData: function () {
return true;
},

//获取数据前,可以进行格式化
//@override
formatData: function (data) {
return data;
},

//获取数据
get: function () {
if (!this.validateData()) {
//需要log
return {};
}
return this.formatData(this.data);
},

_update: function (key, data) {
if (typeof this.controllers[key] === 'function')
this.controllers[key].call(this.scope, data);
else if (typeof this.controllers[key].render === 'function')
this.controllers[key].render.call(this.scope, data);
},

//数据跟新后需要做的动作,执行对应的controller改变dom
//@override
update: function (key) {
var data = this.get();
var k;
if (!data) return;

if (this.controllers[key]) {
this._update(key, data);
return;
}

for (k in this.controllers) {
this._update(k, data);
}
}
});

</script>
<script type="text/javascript">

//博客的model模块应该是完全独立与页面的主流层的,并且可复用
var Model = _.inherit(AbstractModel, {
propertys: function () {
this.data = {
blogs: []
};
},
//新增博客
add: function (title, type, label) {
//做数据校验,具体要多严格由业务决定
if (!title || !type) return null;

var blog = {};
blog.id = 'blog_' + _.uniqueId();
blog.title = title;
blog.type = type;
if (label) blog.label = label.split(',');
else blog.label = [];

this.data.blogs.push(blog);

//通知各个控制器变化
this.update();

return blog;
},
//删除某一博客
remove: function (id) {
if (!id) return null;
var i, len, data;
for (i = 0, len = this.data.blogs.length; i < len; i++) {
if (this.data.blogs[i].id === id) {
data = this.data.blogs.splice(i, 1)
this.update();
return data;
}
}
return null;
},
//获取所有类型映射表
getTypeInfo: function () {
var obj = {};
var i, len, type;
for (i = 0, len = this.data.blogs.length; i < len; i++) {
type = this.data.blogs[i].type;
if (!obj[type]) obj[type] = 1;
else obj[type] = obj[type] + 1;
}
return obj;
},
//获取标签映射表
getLabelInfo: function () {
var obj = {}, label;
var i, len, j, len1, blog, label;
for (i = 0, len = this.data.blogs.length; i < len; i++) {
blog = this.data.blogs[i];
for (j = 0, len1 = blog.label.length; j < len1; j++) {
label = blog.label[j];
if (!obj[label]) obj[label] = 1;
else obj[label] = obj[label] + 1;
}
}
return obj;
},
//获取总数
getNum: function () {
return this.data.blogs.length;
}

});

//页面主流程
var View = _.inherit(AbstractView, {
propertys: function ($super) {
$super();
this.$el = $('#main');

//统合页面所有点击事件
this.events = {
'click .js_add': 'blogAddAction',
'click .js_blog_del': 'blogDeleteAction'
};

//实例化model并且注册需要通知的控制器
//控制器务必做到职责单一
this.model = new Model({
scope: this,
controllers: {
numController: this.numController,
typeController: this.typeController,
labelController: this.labelController,
blogsController: this.blogsController
}
});
},
//总博客数
numController: function () {
this.$('.js_num').html(this.model.getNum());
},
//分类数
typeController: function () {
var html = '';
var tpl = document.getElementById('js_tpl_kv').innerHTML;
var data = this.model.getTypeInfo();
html = _.template(tpl)({ objs: data });
this.$('.js_type_wrapper').html(html);


},
//label分类
labelController: function () {
//这里的逻辑与type基本一致,但是真实情况不会这样
var html = '';
var tpl = document.getElementById('js_tpl_kv').innerHTML;
var data = this.model.getLabelInfo();
html = _.template(tpl)({ objs: data });
this.$('.js_label_wrapper').html(html);

},
//列表变化
blogsController: function () {
console.log(this.model.get());
var html = '';
var tpl = document.getElementById('js_tpl_blogs').innerHTML;
var data = this.model.get();
html = _.template(tpl)(data);
this.$('.js_blogs_wrapper').html(html);
},
//添加博客点击事件
blogAddAction: function () {
//此处未做基本数据校验,因为校验的工作应该model做,比如字数限制,标签过滤什么的
//这里只是往model中增加一条数据,事实上这里还应该写if预计判断是否添加成功,略去
this.model.add(
this.$('.js_title').val(),
this.$('.js_type').val(),
this.$('.js_label').val()
);

},
blogDeleteAction: function (e) {
var el = $(e.currentTarget);
this.model.remove(el.attr('data-id'));
}
});

var view = new View();
view.show();

</script>
</body>
</html>

复制代码

 

  View Code
复制代码

http://sandbox.runjs.cn/show/bvux03nx

分析

这里注释写的很详细,例子也很简单很完整,其实并不需要太多的分析,对MVC还不太理解的朋友可以换自己方式实现以上代码,然后再加入评论模块,或者其它模块后,体会下开发难度,然后再用这种方式开发试试,体会不同才能体会真理,道不证不明嘛,这里的代码组成为:

① 公共的继承方法

② 公共的View抽象类,主要来说完成了view的事件绑定功能,可以将所有click事件全部写在events中

PS:这个view是我阉割便于各位理解的view,真实情况会比较复杂

③ 公共的Model抽象类,主要完成model的骨架相关,其中比较关键的是update后的通知机制

④ 业务model,这个是关于博客model的功能体现,单纯的数据操作

⑤ 业务View,这个为类实例化后执行了show方法,便绑定了各个事件

这里以一次博客新增为例说明一下程序流程:

① 用户填好数据后,点击增加博客,会触发相应js函数

② js获取文本框数据,为model新增数据

③ model数据变化后,分发事件通知各个控制器响应变化

④ 各个controller执行,并根据model产生view的变化

好了,这个例子就到此为止,希望对帮助各位了解MVC有所帮助

优势与不足

对于移动端的页面来说,一个页面对应着一个View.js,即上面的业务View,其中model可以完全的分离出来,如果以AMD模块化的做法的话,View.js的体积会非常小,而主要逻辑又基本拆分到了Model业务中,controller做的工作由于前端模板的介入反而变得简单

不足之处,便是所有的controller全部绑定到了view上,交互的触发点也全部在view身上,而更好的做法,可能是组件化,但是这类模块包含太多业务数据,做成组件化似乎重用性不高,于是就有了业务组件的诞生。

业务组件&公共频道

所谓业务组件或者公共频道都是网站上了一定规模会实际遇到的问题,我这里举一个例子:

最初我们是做机票项目于是目录结构为:

blade 框架目录

flight 机票业务频道

static 公共样式文件

然后逐渐我们多了酒店项目以及用车项目目录结构变成了:

blade 框架目录

car 用车频道

hotel 酒店频道

flight 机票业务频道

static 公共样式文件

于是一个比较实际的问题出现了,最初机票频道的城市列表模块以及登录模块与常用联系人模块好像其他两个频道也能用,但是问题也出现了:

① 将他们抽离为UI组件,但他们又带有业务数据

② 其它两个频道并不想引入机票频道的模块配置,而且也不信任机票频道

这个时候便会出现一个叫公共频道的东西,他完成的工作与框架类似,但是他会涉及到业务数据,并且除了该公司,也许便不能重用:

blade 框架目录

common 公共频道

car 用车频道

hotel 酒店频道

flight 机票业务频道

static 公共样式文件

各个业务频道引入公共频道的产品便可解决重用问题,但这样也同时发生了耦合,如果公共频道的页面做的不够灵活可配置,业务团队使用起来会是一个噩梦!

于是更好的方案似乎是页面模块化,尽可能的将页面分为一个个可重用的小模块,有兴趣的朋友请到这里看看:

【前端优化之拆分CSS】前端三剑客的分分合合

【shadow dom入UI】web components思想如何应用于实际项目

网站慢了

关于系统优化的建议我之前写了很多文章,有兴趣的朋友可以移驾至这里看看:

浅谈移动前端的最佳实践

我这里补充一点业务优化点:

① ajax请求剥离无意义的请求,命名使用短拼

这条比较适用于新团队,服务器端的同事并不会关注网络请求的耗时,所以请求往往又臭又长,一个真实的例子就是,上周我推动服务器端同事将城市列表的无意义字段删除后容量由90k降到了50k,并且还有优化空间!!!

② 工程化打包时候最好采用MD5的方式,这样可做到比较舒服的application cache效果,十分推崇!

③ ......

结语&核心点

半年了,项目由最初的无趣到现在可以在上面玩MVC、玩ABTesting等高端东西了,而看着产品订单破一,破百,破千,破万,虽然很累,但是这个时候还是觉得是值得的。

只可惜我厂的一些制度有点过于恶心,跨团队交流跟吃屎一样,工作量过大,工资又低,这些点滴还是让人感到失望的。

好了,抱怨结束,文章浅谈了一些自己对移动端从0到1做业务开发的一些经验及建议,没有什么高深的知识,也许还有很多错误的地方,请各位不吝赐教,多多指点,接下来时间学习的重点应该还是IOS,偶尔会穿插MVVM框架(angularJS等)的相关学习,有兴趣的朋友可以一起关注,也希望自己尽快打通端到端吧,突破自身瓶颈。

最后,我的微博粉丝及其少,如果您觉得这篇博客对您哪怕有一丝丝的帮助,微博求粉博客求赞!!!

转自http://www.cnblogs.com/yexiaochai/p/4840512.html

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
222.jpg 北京时间2016年11月16日,国内领先的WEB移动内核软件研发厂商-Zoomla!逐浪CMS团队发布其年度最后一个大作,也是目前国内首个基于MVC架构的厂商级dotNET框架CMS- Zoomla!逐浪2 x3.8 众所周知,目前面向云与大数据是今天互联网的大势所趋,而MVC框架则是目前最流行的开发框架之一。 ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。 ASP.NET 支持三种不同的开发模式: Web Pages(Web 页面)、MVC(Model View Controller 模型-视图-控制器)、Web Forms(Web 窗体) MVC 编程模式 MVC 是三种 ASP.NET 编程模式中的一种。 MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式: Model(模型)表示应用程序核心(比如数据库记录列表)。 View(视图)显示数据(数据库记录)。 Controller(控制器)处理输入(写入数据库记录)。 MVC 模式同时提供了对 HTML、CSS 和 JavaScript 的完全控制。 MVC 模式定义 Web 应用程序 带有三个逻辑层: 27 (1).jpg 业务层(模型逻辑) 显示层(视图逻辑) 输入控制(控制器逻辑) Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。 通常模型对象负责在数据库中存取数据。 View(视图)是应用程序中处理数据显示的部分。 通常视图是依据模型数据创建的。 Controller(控制器)是应用程序中处理用户交互的部分。 通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。 MVC 分层有助于管理复杂的应用程序,因为您可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。 MVC 分层同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑 未标题-1.jpg 基于这一思维开发的产品,具有更易维护、更加简洁目录,同时加上全新的逐浪CMS架构和自主表现引擎,整体效率与运行脚本也更具上乘。 Zoomla!逐浪CMS2 x3.8系统是逐浪软件团队年度大作,也是有史以来最大的一次加构更新,我们不仅重写了全局代码、后台引擎,同时就整个底层架构进行优化,整体效率提升了三倍以上。 同时融入了新的办公系统、移动功能、H5模块、移动开发引擎,具备良好的扩展性。 主要更新有: 全新后台表现体系,完美支持移动设置和Surface book、ipad等触控设备应用 全新智能模板引擎,引入我们为猪八戒网等平台提供的模板引擎,从而有更好的设计体验 新增FTP管理模板,可以更好的管理云主机 HTML5表单问券系统 场景复制功能 全新会员特许商品功能 全新会员层级邀请码,通过层级进行B2B分销推荐 增加:插件式开发方式,用于在发布后的项目增加新的mvc页面 增加:加固的安全防护,config下数据库链接不再明文,而是加密,并可通过官方Help.z01.com工具进行解密,从而提升平台的安全性。 修复:用户云备用功能。 增加:扩展商品支持最大购买数、最小购买数、购买倍数 改进:验证码改为点击后会自动更新防抓图破解 增加:增加:微信公众号子商户支付功能(wxpay_submp) 扩展:/Tools/ 维护工具,增加对加密文本的维护功能 增加:内容管理、商品、商城新增二维码,后台一键分享更方便 修改:修改CMS密钥机制 修改:webup多文件上传组件,增加图片压缩功能 场景:增加相册功能,并扩展支持图片压缩 增加:提现申请支持费率(需要在系统-配置-商城参数中设置费率) 修改:场景--相册,增加新建场景提示,微信分享图片默认为第一张图 扩展:ueditor已升级为1.4.3.3,解决一个安全溢出缺陷 增加:微信红包功能(/User/Money/RedPacket) *管理员在后台--微信--生成红包 *用户通过红包码在前台领取红包(用户必须关注公众号才可发送红包) 扩展:场景增加访问密码功能 *如设置,则非创建者访问需要密码 *输入一次密码后,只要不关浏览器,即可直接访问 扩展:资金赠送可根据用户名或ID选择赠送人 扩展:订单管理新增导出Excel 新增:全新订单管理样式 新增:快递打单功能,支持顺风、EMS、中通、安能物流等快递直接打出订单并匹配快递订单 扩展:内容支持中文URL,示例:/Item/标题,从而提升SEO的效率 *需要在后台--节点--栏目选项--中文URL *IE下需要对中文编码,否则无法解析标题 扩展:在线设计增加了对标签的支持,提现支持手续费率 扩展:相册模式增加新的模板,修复PPT模式 相册Bug 修复:OA模型添加无效Bug,用户检测Bug,专题路径无效Bug,OA事务模板Bug *编辑器模板:后台--办公--套红管理--模板类型=事务 修复:重新校验了手机注册流程,将注册流程修复 可视设计:增加记录复制 可视设计:增加标签复制,仅首行显示添加按钮 修复:后台--办公--OA,选择用户无效Bug(OA统一使用组织结构) 优化:开发中心Runsql增加快捷键,并优化关键词显示颜色 优化:后台商城-推广中心-用户明细列表显示用户ID、推荐人,新增按ID、真实姓名搜索, 并可逐级查看推广用户 增加:会员展示和会员详情新样式,更加简洁明亮 移除:移除后台SNS好友管理、虚拟商品等无用页面 修复:邀请码生成功能,并检验邀请码逻辑 修复:微博绑定Bug,修改密码链接Bug 修复:用户中心广告申请、节日提醒功能 增加:MVC页面索引功能,用户中心已可搜索页面 增加:会员支付二级密码功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值