花了十天時間做了一個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
,我自己寫了一個 AngularJS
與 LocalStorage
的數據綁定的 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
只有 code
與 message
兩個值,但是有一些情況下可能會需要提供一些額外的錯誤信息,那么都放入了 error.errors
這個數組中。
App前端是下面這樣的判斷的:
- 當
error
為 null
時,表示請求成功,此時從 data
中取數據;
- 當
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比現在的簡單就又開始做了
然后我又想到一種可能
- 我們把App上了,
- 另一個領導帶招一些新人把也做了一個App
- 如果App還可以的話,把我們的功能直接復制過去,然后讓我們的下線
- 然后領導又可以邀功了
- 如果App不可以的話,那我們是在浪費時間,把我們的下線,然后……
反正,似乎都跟我沒半毛錢關系了,除非這個App運營的不好。
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!