項目地址(http://sack.doraemoney.com)javascript
6月14號,和另外兩個同事商量着不能再像最近這幾個月這樣了,彷佛每個公司的產品經理與碼農們都是死對頭,我也沒有逃出這個怪圈,天天在對產品的「精雕細琢」中,讓我對產品愈加的反感,不經意間,看了看本身的 Git Commits List,好長啊,天天都有好多,而後就想着看看本身的幹了些什麼,忽然之間,發現這就是一個循環啊,基本上是下面這樣的:前端
for var keepGoing = true; keepGoing { // 4B中 }
不行啊,咱們得本身整一個,可是不能在上班時間整,由於這是一個只有咱們參與的事情,並且也不但願他人對咱們的指指點點,因此,決定天天的空餘時間抽出幾個小時,計劃着一個星期以內整一個新的東西出來,恩,是的,App,最後仍是花了咱們3我的十天的時間。java
這仍是一個借款給有須要的人的App,沒有風控模型,可是咱們有完善的催債模型和真實性模型,咱們只作一件事情,讓借款給你的人更快的相信你在按時還款,因此,咱們選擇了通信錄、通話記錄、地理位置、手機型號等這些經過一個App能快速獲取到的數據。web
而後咱們開始了規劃,簡單的設計,接口的定義以及數據結構的定義,這花了一天的時間,咱們按着花了三天時間把整個系統開發完了,也就是全部的功能都有了,接着花了兩天時間把全部的功能接口串連上,最後再連了四天的時間測試、調試與Bug修復,Ok,一個全新的App就這麼出來了。json
技術使用的是:後端
選擇Java的緣由很簡單,也很純粹,咱們的核心業務系統就是Java的,爲了能更快速的開發,咱們仍是直接使用Java,這樣不少接口的代碼能夠直接複製過來改改就能使用,這爲咱們節省了不少開發的時間。api
這個不用想,簡單的App開發中的神器,有了這個東西,即便我對App開發一無所知,我也能僅使用我本身會的前端技術實現一個完善的App。數組
這爲咱們的App兼容到各類平臺 iOA/Andoird等提供支持。promise
由於數據量不多,因此直接使用了 LocalStorage
,我本身寫了一個 AngularJS
與 LocalStorage
的數據綁定的 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{ "apiVersion" : "0.0.1", "token" : "TOKEN_STRING", "requestId" : "ID_STRING", "data" : { // Data goes here } }
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
只有 code
與 message
兩個值,可是有一些狀況下可能會須要提供一些額外的錯誤信息,那麼都放入了 error.errors
這個數組中。
App前端是下面這樣的判斷的:
error
爲 null
時,表示請求成功,此時從 data
中取數據;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比如今的簡單就又開始作了
而後我又想到一種可能
反正,彷佛都跟我沒半毛錢關係了,除非這個App運營的很差。