花了十天時間做了一個App,取名一麻貸,想著一麻袋一麻袋的放款,但是……

jopen 9年前發布 | 19K 次閱讀 APP

6 月14號,和另外兩個同事商量著不能再像最近這幾個月這樣了,似乎每一個公司的產品經理與碼農們都是死對頭,我也沒有逃出這個怪圈,每天在對產品的“精雕細琢”中,讓我對產品越發的反感,不經意間,看了看自己的 Git Commits List,好長啊,每天都有好多,然后就想著看看自己的干了些什么,突然之間,發現這就是一個循環啊,基本上是下面這樣的:

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

不行啊,我們得自己整一個,但是不能在上班時間整,因為這是一個只有我們參與的事情,而且也不希望他人對我們的指指點點,所以,決定每天的空余時間抽出幾個小時,計劃著一個星期之內整一個新的東西出來,恩,是的,App,最后還是花了我們3個人十天的時間。

這還是一個借款給有需要的人的App,沒有風控模型,但是我們有完善的催債模型和真實性模型,我們只做一件事情,讓借款給你的人更快的相信你在按時還款,所以,我們選擇了通訊錄、通話記錄、地理位置、手機型號等這些通過一個App能快速獲取到的數據。

然后我們開始了規劃,簡單的設計,接口的定義以及數據結構的定義,這花了一天的時間,我們按著花了三天時間把整個系統開發完了,也就是所有的功能都有了,接著花了兩天時間把所有的功能接口串連上,最后再連了四天的時間測試、調試與Bug修復,Ok,一個全新的App就這么出來了。

技術使用的是:

  • Java
  • Ionic
  • Cordova
  • 一些必要的插件
  • </ul>

    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; } ]); </code></pre>

      使用起來很簡單:

      $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; }];

      } ]); </code></pre>

      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" : ""
      

      } ] } } </code></pre>

      說明

      在上面的這些數據結構中,請求的很好理解,響應的 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 }; } ]); </code></pre>

      這樣里面最主要是使用一個方法: 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'
      

      } }, //... } //... }); </code></pre>

      然后,更具體的,在 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 } }); </code></pre>

      這樣的好處是,我可以直接使用類似下面這樣發起請求:

      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運營的不好。

     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!