花了十天时间作了一个App,取名一麻贷,想着一麻袋一麻袋的放款,可是……

图片描述

项目地址(http://sack.doraemoney.comjavascript

6月14号,和另外两个同事商量着不能再像最近这几个月这样了,彷佛每个公司的产品经理与码农们都是死对头,我也没有逃出这个怪圈,天天在对产品的“精雕细琢”中,让我对产品愈加的反感,不经意间,看了看本身的 Git Commits List,好长啊,天天都有好多,而后就想着看看本身的干了些什么,忽然之间,发现这就是一个循环啊,基本上是下面这样的:前端

for var keepGoing = true; keepGoing  {
    // 4B中
}

不行啊,咱们得本身整一个,可是不能在上班时间整,由于这是一个只有咱们参与的事情,并且也不但愿他人对咱们的指指点点,因此,决定天天的空余时间抽出几个小时,计划着一个星期以内整一个新的东西出来,恩,是的,App,最后仍是花了咱们3我的十天的时间。java

这仍是一个借款给有须要的人的App,没有风控模型,可是咱们有完善的催债模型和真实性模型,咱们只作一件事情,让借款给你的人更快的相信你在按时还款,因此,咱们选择了通信录、通话记录、地理位置、手机型号等这些经过一个App能快速获取到的数据。web

而后咱们开始了规划,简单的设计,接口的定义以及数据结构的定义,这花了一天的时间,咱们按着花了三天时间把整个系统开发完了,也就是全部的功能都有了,接着花了两天时间把全部的功能接口串连上,最后再连了四天的时间测试、调试与Bug修复,Ok,一个全新的App就这么出来了。json

技术使用的是:后端

  • Java
  • Ionic
  • Cordova
  • 一些必要的插件

Java

选择Java的缘由很简单,也很纯粹,咱们的核心业务系统就是Java的,为了能更快速的开发,咱们仍是直接使用Java,这样不少接口的代码能够直接复制过来改改就能使用,这为咱们节省了不少开发的时间。api

Ionic

这个不用想,简单的App开发中的神器,有了这个东西,即便我对App开发一无所知,我也能仅使用我本身会的前端技术实现一个完善的App。数组

Cordova

这为咱们的App兼容到各类平台 iOA/Andoird等提供支持。promise

我是怎么作的

关于本地的数据存储

由于数据量不多,因此直接使用了 LocalStorage,我本身写了一个 AngularJSLocalStorage 的数据绑定的 Angular Module,代码以下:服务器

javascript/**
 * 本地存储
 */
app.factory('$storage', [
  '$rootScope',
  '$window',
  function(
      $rootScope,
      $window
  ){

    var webStorage = $window['localStorage'] || (console.warn('This browser does not support Web Storage!'), {}),
        storage = {
          $default: function(items) {
            for (var k in items) {
              angular.isDefined(storage[k]) || (storage[k] = items[k]);
            }

            return storage;
          },
          $reset: function(items) {
            for (var k in storage) {
              '$' === k[0] || delete storage[k];
            }

            return storage.$default(items);
          }
        },
        _laststorage,
        _debounce;

    for (var i = 0, k; i < webStorage.length; i++) {


      (k = webStorage.key(i)) && 'storage-' === k.slice(0, 8) && (storage[k.slice(8)] = angular.fromJson(webStorage.getItem(k)));
    }

    _laststorage = angular.copy(storage);

    $rootScope.$watch(function() {
      _debounce || (_debounce = setTimeout(function() {
        _debounce = null;

        if (!angular.equals(storage, _laststorage)) {
          angular.forEach(storage, function(v, k) {
            angular.isDefined(v) && '$' !== k[0] && webStorage.setItem('storage-' + k, angular.toJson(v));

            delete _laststorage[k];
          });

          for (var k in _laststorage) {
            webStorage.removeItem('storage-' + k);
          }

          _laststorage = angular.copy(storage);
        }
      }, 100));
    });


    'localStorage' === 'localStorage' && $window.addEventListener && $window.addEventListener('storage', function(event) {
      if ('storage-' === event.key.slice(0, 10)) {
        event.newValue ? storage[event.key.slice(10)] = angular.fromJson(event.newValue) : delete storage[event.key.slice(10)];

        _laststorage = angular.copy(storage);

        $rootScope.$apply();
      }
    });

    return storage;
  }
]);

使用起来很简单:

javascript$storage.token = 'TOKEN_STRING'; // 这就会在localStorage 中存储一个 `key` 为 `storage-token` 而 `value` 为 `TOKEN_STRING` 的键值对,这是一个单向存储的过程,也就是咱们再手工修改 `localStorage` 里面的值是没有用的,`100ms` 以后就会被 `$storage.token` 的值覆盖,这是一个更新存储的时间。

数据请求

由于咱们这边的接口走的不是 AngularJS 的默认请求方式,数据结构为相似表单提交,因此,我还修改了 Angular 中的 $http,转换对象为 x-www-form-urlencoded 序列代的字符串:

javascript/**
 * 配置
 */
app.config([
  '$ionicConfigProvider',
  '$logProvider',
  '$httpProvider',
  function(
      $ionicConfigProvider,
      $logProvider,
      $httpProvider
  ) {
    // .. 其它代码
    // 开启日志
    $logProvider.debugEnabled(true);

    /**
     * 服务器接口端要求在发起请求时,同时发送 Content-Type 头信息,且其值必须为: application/x-www-form-urlencoded
     * 可选添加字符编码,在此处我默认将编码设置为 utf-8
     *
     * @type {string}
     */

    $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
    $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

    /**
     * 请求只接受服务器端返回 JSON 数据
     * @type {string}
     */
    $httpProvider.defaults.headers.post['Accept'] = 'application/json';

    /**
     * AngularJS 对默认提交的数据结构为 json 格式的,可是咱们NiuBilitity的服务器端不能解析 JSON 数据,因此
     * 咱们经 x-www-form-urlencoded 的方式提交,此处将对数据进行封装为 foo=bar&bar=other 的方式
     * @type {*[]}
     */
    $httpProvider.defaults.transformRequest = [function(data) {
      /**
       * 转换对象为 x-www-form-urlencoded 序列代的字符串
       * @param {Object} obj
       * @return {String}
       */
      var param = function(obj) {
        var query = '';
        var name, value, fullSubName, subName, subValue, innerObj, i;

        for (name in obj) {
          value = obj[name];

          if (value instanceof Array) {
            for (i = 0; i < value.length; ++i) {
              subValue = value[i];
              fullSubName = name + '[' + i + ']';
              innerObj = {};
              innerObj[fullSubName] = subValue;
              query += param(innerObj) + '&';
            }
          } else if (value instanceof Object) {
            for (subName in value) {
              subValue = value[subName];
              fullSubName = name + '[' + subName + ']';
              innerObj = {};
              innerObj[fullSubName] = subValue;
              query += param(innerObj) + '&';
            }
          } else if (value !== undefined && value !== null) {
            query += encodeURIComponent(name) + '='
                + encodeURIComponent(value) + '&';
          }
        }

        return query.length ? query.substr(0, query.length - 1) : query;
      };

      return angular.isObject(data) && String(data) !== '[object File]'
          ? param(data)
          : data;
    }];

  }
]);

JSON 请求数据结构

咱们的数据结构是下面这样的:

Request

json{
  "apiVersion" : "0.0.1",
  "token" : "TOKEN_STRING",
  "requestId" : "ID_STRING",
  "data" : {
    // Data goes here
  }
}

Response

json{
  "apiVersion" : "0.0.1",
  "data" : {},
  "error" : {
    "code" : ERROR_CODE_NUMBER,
    "message" : "Error Message Here",
    "errors" : [
      {
        "code" : 0,
        "message" : "",
        "location" : ""
      }
    ]
  }
}

说明

在上面的这些数据结构中,请求的很好理解,响应的 json 结构只有三个字段, apiVersion 表示了当前请求的接口版本号, data 就是数据对象, error 则是错误对象,通常状况下,一个 error 只有 codemessage 两个值,可是有一些状况下可能会须要提供一些额外的错误信息,那么都放入了 error.errors 这个数组中。

App前端是下面这样的判断的:

  1. errornull 时,表示请求成功,此时从 data 中取数据;
  2. error 不为 null 时,表示请求失败,此时从 error 中取错误信息,而彻底无论 data ,我采起的方式是直接抛弃(其实先后端已经约定了,因此不存在 error 不为 null 时,data 中还有数据的状况出现。

关于 $http

我没有直接将接口的 url 地址、$http 请求等暴露给 Controller,而是作了一层封装,我叫做为 sack(也就是 App 的名称):

javascriptapp.factory('sack', [
  '$http',
  '$q',
  '$log',
  '$location',
  '$ionicPopup',
  '$storage',
  'API_VERSION',
  'API_PROTOCOL',
  'API_HOSTNAME',
  'API_URI_MAP',
  'util',
  function(
      $http,
      $q,
      $log,
      $location,
      $ionicPopup,
      $storage,
      API_VERSION,
      API_PROTOCOL,
      API_HOSTNAME,
      API_URI_MAP,
      util
  ){
    var HTTPUnknownError = {code: -1, message: '出现未知错误'};
    var HTTPAuthFaildError = {code: -1, message: '受权失败'};
    var APIPanicError = {code: -1, message: '服务器端出现未知错误'};
    var _host = API_PROTOCOL + '://' + API_HOSTNAME + '/',
        _map = API_URI_MAP,
        _apiVersion = API_VERSION,
        _token = (function(){return $storage.token;}()) ;

    setInterval(function(){
      _token = (function(){return $storage.token;}());
      //$log.info("Got Token: " + _token);
    }, 1000);

    var appendTransform = function(defaultFunc, transFunc) {
      // We can't guarantee that the default transformation is an array
      defaultFunc = angular.isArray(defaultFunc) ? defaultFunc : [defaultFunc];

      // Append the new transformation to the defaults
      return defaultFunc.concat(transFunc);
    };

    var _prepareRequestData = function(originData) {
      originData.token = _token;
      originData.apiVersion = _apiVersion;
      originData.requestId = util.getRandomUniqueRequestId();
      return originData;
    };

    var _prepareRequestJson = function(originData) {
      return angular.toJson({
        apiVersion: _apiVersion,
        token: _token,
        requestId: util.getRandomUniqueRequestId(),
        data: originData
      });
    };

    var _getUriObject = function(uon) {
      // 若传入的参数带有 _host 头
      if((typeof uon === 'string' && (uon.indexOf(_host) == 0) ) || uon === '') {
        return {
          uri: uon.replace(_host, ''),
          methods: ['post']
        };
      }

      if(typeof _map === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      var _uon = uon.split('.'),
          _ns,
          _n;

      if(_uon.length == 1) {
        return {
          uri: '',
          methods: ['post']
        };
      }
      _ns = _uon[0];
      _n = _uon[1];

      _mod = _map[_ns];

      if(typeof _mod === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      _uriObject = _mod[_n];

      if(typeof _uriObject === 'undefined') {
        return {
          uri: '',
          methods: ['post']
        };
      }

      return _uriObject;
    };

    var _getUri = function(uon) {
      return _getUriObject(uon).uri;
    };

    var _getUrl = function(uon) {
      return _host + _getUri(uon);
    };

    var _auth = function(uon) {
      var _uo = _getUriObject(uon),
          _authed = false;
      $log.log('Check Auth of : ' + uon);
      $log.log('Is this api need auth: ' + angular.toJson(_uo.needAuth));
      $log.log('Is check passed: ' + angular.toJson(!(!_token && _uo.needAuth)));
      $log.log('Token is: ' + _token);
      if(!_token && _uo.needAuth) {

        $ionicPopup.alert({
          title: '提示',
          subTitle: '您当前的登陆状态已失效,请从新登陆。'
        }).then(function(){
          $location.path('/sign');
        });

        $location.path('/sign');
      } else {
        _authed = true;
      }
      return _authed;
    };

    var get = function(uon) {
      return $http.get(_getUrl(uon));
    };

    var post = function(uon, data, headers) {
      var _url = _getUrl(uon),
          _data = _prepareRequestData(data);
      $log.info('========> POST START [ ' + uon + ' ] ========>');
      $log.log('REQUEST URL  : ' + _url);
      $log.log('REQUEST DATA : ' + angular.toJson(_data));

      return $http.post(_url, _data, {
        transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
          $log.log('RECEIVED JSON : ' + angular.toJson(value));
          if(typeof value.ex != 'undefined') {
            return {
              error: APIPanicError
            };
          }
          return value;
        })
      });
    };

    var promise = function(uon, data, headers) {
      var defer = $q.defer();

      if(!_auth(uon)) {
        defer.reject(HTTPAuthFaildError);
        return defer.promise;
      }

      post(uon, data, headers).success(function(res){
        if(res.error) {
          defer.reject(res.error);
        } else {
          defer.resolve(res.data);
        }
      }).error(function(res){
        defer.reject(HTTPUnknownError);
      });
      return defer.promise;
    };

    var postJson = function(uon, data, headers) {
      var _url = _getUrl(uon),
          _json = _prepareRequestJson(data);
      $log.info('========> POST START [ ' + uon + ' ] ========>');
      $log.log('REQUEST URL  : ' + _url);
      $log.log('REQUEST JSON : ' + _json);
      return $http.post(_url, _json, {
        transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
          $log.log('RECEIVED JSON : ' + angular.toJson(value));
          if(typeof value.ex != 'undefined') {
            return {
              error: APIPanicError
            };
          }
          return value;
        })
      });
    };

    var promiseJson = function(uon, data, headers) {
      var defer = $q.defer();

      if(!_auth(uon)) {
        defer.reject(HTTPAuthFaildError);
        return defer.promise;
      }

      postJson(uon, data, headers).success(function(res){
        if(res.error) {
          defer.reject(res.error);
        } else {
          defer.resolve(res.data);
        }
      }).error(function(res){
        defer.reject(HTTPUnknownError);
      });
      return defer.promise;
    };

    return {
      get: get,
      post: post,
      promise: promise,

      postJson: postJson,
      promiseJson: promiseJson,
      _auth: _auth,
      HTTPAuthFaildError: HTTPAuthFaildError
    };
  }
]);

这样里面最主要是使用一个方法: sack.promiseJson,这个方法是以 json 数据向服务器发送请求,而后返回一个 promise 的。

上面的 API_URI_MAP 的数据结构相似于下面这样的:

javascriptapp.constant('API_URI_MAP', {
  user : {
    sign : {
      needAuth: false,
      uri : 'sack/user/sign.json',
      methods: [
        'post'
      ],
      params: {
        mobile: 'string', // 手机号码
        captcha: 'string' // 验证码
      }
    },
    unsign: {
      needAuth: true,
      uri: 'sack/user/unsign.json',
      methods: [
        'post'
      ],
      params: {
        token: 'string'
      }
    },
    //...
  }
  //...
});

而后,更具体的,在 Controller 中也不直接使用 sack.promiseJson 这个方法,而是使用封装好的服务进行,好比下面这个服务:

javascriptapp.factory('UserService', function($rootScope, $q, $storage, API_CACHE_TIME, sack) {
  var sign = function(data) {
    return sack.promiseJson('user.sign', data);
  };

  return {
    sign: sign
  }
});

这样的好处是,我能够直接使用相似下面这样发起请求:

UserService.sign({mobile:'xxxxxxxxxxx',captcha:'000000'}).then(function(res){
  // 受权成功
}, function(err){
  // 受权失败
});

可是

好吧,又来可是了,App作完了以后,咱们可爱的领导们感受这个还能够,而后就又要开始发挥他们的各类NB的指导了,还好从一开始咱们就没有使用上班时间,这使得咱们有理由拒绝领导的指导,可是,公司却说了,不接受指导那就不让上,好吧,那就不上呗,这彷佛惹怒了咱们的领导们,因此,就直接没有跟咱们通气的开始招兵买马要上App了,我瞬间就想问:

咱们的战略不是说不作App么?如今怎么看到App比如今的简单就又开始作了

而后我又想到一种可能

  1. 咱们把App上了,
  2. 另外一个领导带招一些新人把也作了一个App
  3. 若是App还能够的话,把咱们的功能直接复制过去,而后让咱们的下线
  4. 而后领导又能够邀功了
  5. 若是App不能够的话,那咱们是在浪费时间,把咱们的下线,而后……

反正,彷佛都跟我没半毛钱关系了,除非这个App运营的很差。

相关文章
相关标签/搜索