花了十天時間作了一個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運營的很差。

相關文章
相關標籤/搜索