项目地址(http://sack.doraemoney.com)
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
,我自己写了一个 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 请求数据结构
我们的数据结构是下面这样的:
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
只有 code
与 message
两个值,但是有一些情况下可能会需要提供一些额外的错误信息,那么都放入了 error.errors
这个数组中。
App前端是下面这样的判断的:
- 当
error
为null
时,表示请求成功,此时从data
中取数据; - 当
error
不为null
时,表示请求失败,此时从error
中取错误信息,而完全不管data
,我采取的方式是直接抛弃(其实前后端已经约定了,所以不存在error
不为null
时,data
中还有数据的情况出现。
关于 $http
我没有直接将接口的 url
地址、$http
请求等暴露给 Controller
,而是做了一层封装,我叫作为 sack
(也就是 App 的名称):
javascript
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
的数据结构类似于下面这样的:
javascript
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
这个方法,而是使用封装好的服务进行,比如下面这个服务:
javascript
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比现在的简单就又开始做了
然后我又想到一种可能
- 我们把App上了,
- 另一个领导带招一些新人把也做了一个App
- 如果App还可以的话,把我们的功能直接复制过去,然后让我们的下线
- 然后领导又可以邀功了
- 如果App不可以的话,那我们是在浪费时间,把我们的下线,然后……
反正,似乎都跟我没半毛钱关系了,除非这个App运营的不好。