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

jopen 9年前

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

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

不行啊,我们得自己整一个,但是不能在上班时间整,因为这是一个只有我们参与的事情,而且也不希望他人对我们的指指点点,所以,决定每天的空余时间抽出几个小时,计划着一个星期之内整一个新的东西出来,恩,是的,App,最后还是花了我们3个人十天的时间。

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

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

技术使用的是:

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

Java

选择Java的原因很简单,也很纯粹,我们的核心业务系统就是Java的,为了能更快速的开发,我们还是直接使用Java,这样很多接口的代码可以直接复制过来改改就能使用,这为我们节省了很多开发的时间。

Ionic

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

Cordova

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

我是怎么做的

关于本地的数据存储

因为数据量很少,所以直接使用了 LocalStorage ,我自己写了一个 AngularJSLocalStorage 的数据绑定的 Angular Module ,代码如下:

/**   * 本地存储   */  app.factory('$storage', [    '$rootScope',    '$window',    function(        $rootScope,        $window    ){      // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app      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++) {        // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty)          (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));      });        // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent`      '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;    }  ]); 

使用起来很简单:

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

数据请求

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

/**   * 配置   */  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 的名称):

app.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 的数据结构类似于下面这样的:

app.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 这个方法,而是使用封装好的服务进行,比如下面这个服务:

app.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运营的不好。