欢迎大家到我的博客关注我学习Ionic 1和Ionic 2的历程,共同学习,共同进步。
注:本文是译文,难免有错误或理解不足之处,请大家多多指正,大家也可挪步原文。由于本文讲解十分精彩,非常推荐大家查看原文,由于原文内容十分丰富,所以将其分为2部分,这是Part 2(教程篇),戳这里查看Part 1(基础篇)。
$q Service promise使用教程
现在假设我们的app上需要增加一个注册的功能,用户注册需要提供:用户当前的坐标信息、用户的照片和用户名。为了完成这个注册的操作,我们的后台需要获得如下信息:
- 提供当前的坐标信息,经度和维度;
- 用户上传的照片信息保存到服务器中,并返回一个表示此照片的url;
- 保存用户名并返回一个用户名保存的回执id。
为了做到以上3点,我们使用下面的方法(这里我会分开讲解以便更加清晰)。注意下面用到的方法均是异步方法,所以promise就要出场了:
获取当前位置信息函数
function getGeolocationCoordinates() {
var deferred = $q.defer();
navigator.geolocation.getCurrentPosition(
function(position) { deferred.resolve(position.coords); },
function(error) { deferred.resolve(null); }
);
return deferred.promise;
}
getGeolocationCoordinates()声明了一个deferred对象,然后向浏览器询问当前的位置信息。因为位置信息不是必须的,所以成功回调和失败回调都进行了resolve()操作,不过在失败回调中传入null,成功回调中传入位置信息。最后,返回deferred的promise对象。
读取本地文件并返回其内容函数
function readFile(fileBlob) {
var deferred = $q.defer();
var reader = new FileReader();
reader.onload = function () { deferred.resolve(reader.result); };
reader.onerror = function () { deferred.reject(); };
try {
reader.readAsDataURL(fileBlob);
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
readFile()
需要传入二进制对象(blob)(可能是从<input type="file">
标签中获得),然后使用使用FileReader读取其内容。在读取文件内容并返回promise之前,readFile()
还定义了onload
和onerror
回调函数。注意这里我使用了try...catch
包装了reader.readAsDataURL(fileBlob);
操作,以便能够处理运行时的异常。如果有异常发生,这里只是简单的对deferred进行了reject()处理。
获取文件内容并上传函数
function uploadFile(fileData) {
var jQueryPromise = $.ajax({
method: 'POST',
url: '<endpoint for our files storage upload action>',
data: fileData
});
return $q.when(jQueryPromise);
}
因为几乎所有人都了解jQuery,所以这里我在uploadFile()
函数中使用了$.ajax()
方法。$.ajax()
方法返回一个我们期望的promise,不过这个promise是jQuery的实现方式,而不是$q Service
的实现方式。幸好AngularJS帮我们想到了这点,这里我们可以使用$q.when(value)
方法将这个promise转换为$q Service
的promise。
保存用户名并返回保留回执id函数
function reserveUsername(username) {
return $http.post('<endpoint for username reservation action>', {
username: username
});
}
这里我使用了AngularJS的$http
Service。$http.post
返回一个promise,表示了$http.post
方法执行后的状态,此promise其实也是通过$http
Service中的$q
Service而来的。
现在我们已经完成了注册相关的全部功能函数,现在我们可以将其封装到一个Service中,并命名为appService(教程最后会有完整版的app-service.js)。
实现controller
我们的应用使用的controller非常简单,只注入了$scope
, $q
和 appService
三个依赖,当然其中也实现了一些处理数据的方法(教程最后会有完整版的controller)。
经度和维度
我们不能让用户输入经度和维度,我们要获取经度和维度并填充到用户界面中,这里我定义了两个只读的<input
标签,并且定义了其ng-model
属性:
<div>
Longitude
<input type="text" readonly="readonly" ng-model="coords.longitude" />
</div>
<div>
Latitude
<input type="text" readonly="readonly" ng-model="coords.latitude" />
</div>
在controller中,我们需要调用getGeolocationCoordinates()
函数得到坐标值,并保存:
appService.getGeolocationCoordinates()
.then(function setCoords(coordsData) {
$scope.coords = coordsData;
});
用户名
用户名的处理上,我同样使用了<input>
标签,不过增加了“输入正确性指示”,一旦username
输入框内容改变,$scope.reserveUsername()
就会被触发:
<div ng-class="{ error: usernameError }">
User Name
<div>
<input type="text" ng-model="username" ng-change="reserveUsername()" />
<div ng-bind="usernameError"></div>
</div>
</div>
$scope.reserveUsername()
需要使用appService
保存新用户名:
var reservationPromise = $q.reject('No username reservation had made');
$scope.reserveUsername = function() {
var newUsername = $scope.username;
reservationPromise = appService.reserveUsername(newUsername)
.then(function setUsernameReservation(reservation) {
$scope.reservation = reservation;
})
.catch(function setUsernameError() {
$scope.usernameError = error;
return $q.reject($scope.usernameError);
});
}
首先reservationPromise
被初始化为rejected promise。
然后,当$scope.reserveUsername()
函数被调用时,后台进行保存动作。在成功的回调函数中,setUsernameReservation()
并没有返回promise,不过随后reservationPromise
将被resolve(promise链中值的传递)。在失败的回调函数中,setUsernameError()
返回一个rejected promise,随后reservationPromise
将被reject(promise链的处理结果依赖于内层promise的处理结果)。
用户照片
用户照片的界面分为如下几个部分:<input type="file">
文件选择部分、照片url指示部分(带默认值)、用户照片指示部分(带默认值)以及错误指示部分(带默认值)。这里还用到了我自定的directive:filePathChanged
,用来当用户选择了一个文件时触发特定函数,教程底部可以找到filePathChanged
的实现方式:
<div ng-class="{ error: photoError }">
Select Photo
<input type="file" file-path-changed="fileSelected(files)">
<span ng-bind="photoError"></span>
<span ng-if="photoUrl" ng-bind="photoUrl"></span>
<img ng-if="photoData" ng-src="{{ photoData }}" />
</div>
看一下$scope.fileSelected(files)
的实现:
var photoPromise = $q.reject('No user photo selected');
$scope.fileSelected = function(files) {
if (files && files.length > 0) {
var filePath = files[0];
photoPromise = appService.readFile(filePath)
.then(function setPhotoData(photoData) {
$scope.photoData = photoData;
return photoData;
})
.then(appService.uploadFile)
.then(function setPhotoUrl(photoUrl) {
$scope.photoUrl = photoUrl;
})
.catch(function setPhotoError(error) {
$scope.photoError = 'An error has occurred: ' + error;
return $q.reject($scope.photoError);
});
}
};
代码逻辑很简答,首先我们确认了文件已经存在,然后我们使用appService.readFile()
函数读取文件内容,并将数据绑定到model上。然后我们上传图片,得到图片的url,并将其绑定到model上。如果有错误发生,我们将错误信息绑定到model上,并返回rejected promise。
注册
上文中我们已经实现了“经度和维度的获取”,“用户名的保存”和“用户照片的上传”的功能,下一步就可以实现“注册”的功能了。注意用户的位置信息不是必须的,所以即使没有获取到用户的位置信息,注册也应该能继续:
$scope.register = function() {
$q.all([
reservationPromise,
photoPromise
]).then(function doRegistrationCall() {
var longitude = $scope.data.coords && $scope.data.coords.longitude;
var latitude = $scope.data.coords && $scope.data.coords.latitude;
var reservationId = $scope.data.reservation.token;
var photoUrl = $scope.data.photoUrl;
doRegistration(longitude, latitude, reservationId, photoUrl);
}, function setSubmitError(error) {
$scope.submitError = error;
});
};
这里我们使用了$q.all()
方法,因为我们希望用户信息都被成功处理后才能进行注册,如果有错误产生,会将错误信息绑定到submitError
model,并反映到用户界面。doRegistration()
方法用来和后台进行通信进行注册过程。
到此,我们的注册过程就完成了,下面是我们的源码:
app-service.js
window.module.factory('appService', ['jquery', '$http', '$q', function($, $http, $q) {
function getGeolocationCoordinates() {
var deferred = $q.defer();
navigator.geolocation.getCurrentPosition(
function(position) { deferred.resolve(position.coords); },
function(error) { deferred.resolve(null); }
);
return deferred.promise;
}
function readFile(fileBlob) {
var deferred = $q.defer();
var reader = new FileReader();
reader.onload = function () { deferred.resolve(reader.result); };
reader.onerror = function () { deferred.reject(); };
try {
reader.readAsDataURL(fileBlob);
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
function uploadFile(fileData) {
// var jQueryPromise = $.ajax({
// method: 'POST',
// url: '<endpoint for our files storage upload action>',
// data: fileData
// });
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('www.myimage.com/123');
}, 200);
var jQueryPromise = deferred.promise();
return $q.when(jQueryPromise);
}
var reserveCount = 0;
function reserveUsername(username) {
// return $http.post('<endpoint for username reservation action>', {
// username: username
// });
var deferred = $q.defer();
setTimeout(function() {
if (reserveCount > 0 && reserveCount % 3 === 0) {
deferred.reject('error reserving "' + username + '"');
} else {
var token = 'token' + reserveCount;
deferred.resolve({
token: token,
username: username
});
}
reserveCount ++;
}, 300);
return deferred.promise;
}
return {
getGeolocationCoordinates: getGeolocationCoordinates,
readFile: readFile,
uploadFile: uploadFile,
reserveUsername: reserveUsername
};
}]);
注:为了在没有后台的情况下模拟uploadFile()
和reserveUsername()
方法可能的执行结果,这里我设定有时候被deferred被resolve,有时候deferred被reject。
app-controller.js
window.module.controller('appController', ['$scope', '$q', 'appService', function($scope, $q, appService) {
$scope.data = { errors: { } };
function setCoords(coordsData) {
$scope.data.coords = coordsData;
}
function setPhotoData(photoData) {
return $scope.data.photoData = photoData;
}
function setPhotoUrl(photoUrl) {
return $scope.data.photoUrl = photoUrl;
}
function clearPhotoError() {
delete $scope.data.errors.photo;
}
function setPhotoError(error) {
$scope.data.errors.photo = 'An error has occurred: ' + error;
return $q.reject($scope.data.errors.photo);
}
function clearUsernameError() {
delete $scope.data.errors.username;
}
function setUsernameError(error) {
$scope.data.errors.username = error;
return $q.reject($scope.data.errors.username);
}
function setUsernameReservation(reservation) {
$scope.data.reservation = reservation;
}
function setSubmitError(error) {
$scope.data.errors.submit = error;
}
function clearSubmitError() {
delete $scope.data.errors.submit;
}
function doRegistration(longitude, latitude, reservationId, photoUrl) {
$scope.data.success = true;
$scope.storedJSON = JSON.stringify({
longitude: longitude,
latitude: latitude,
reservationId: reservationId,
photoUrl: photoUrl
});
}
appService.getGeolocationCoordinates()
.then(setCoords);
var photoPromise = $q.reject('No user photo selected');
$scope.fileSelected = function(files) {
if (files && files.length > 0) {
var filePath = files[0];
clearPhotoError();
photoPromise = appService.readFile(filePath)
.then(setPhotoData)
.then(appService.uploadFile)
.then(setPhotoUrl)
.catch(setPhotoError);
}
};
var reservationPromise = $q.reject('No username reservation had made');
$scope.reserveUsername = function() {
var newUsername = $scope.data.username;
clearUsernameError();
reservationPromise = appService.reserveUsername(newUsername)
.then(setUsernameReservation)
.catch(setUsernameError);
}
$scope.register = function() {
$q.all([
reservationPromise,
photoPromise
]).then(function() {
var longitude = $scope.data.coords && $scope.data.coords.longitude;
var latitude = $scope.data.coords && $scope.data.coords.latitude;
var reservationId = $scope.data.reservation.token;
var photoUrl = $scope.data.photoUrl;
clearSubmitError();
doRegistration(longitude, latitude, reservationId, photoUrl);
}, function(error) {
setSubmitError(error);
});
};
}]);
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script type="text/javascript">
window.jQuery || document.write('<script src="/scripts/libs/jquery.js"><\/script>');
</script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
<script type="text/javascript">
window.angular || document.write('<script src="/scripts/libs/angular.js"><\/script>');
</script>
<link rel="stylesheet" href="/style/semantic.css" />
<link rel="stylesheet" href="/style/app.css" />
</head>
<body ng-app="demo-app">
<form class="ui form segment" ng-controller="appController">
<div class="two fields">
<div class="field">
<label for="longitude">Longitude</label>
<input id="longitude" type="text" readonly="readonly" ng-model="data.coords.longitude" placeholder="No Longitude" />
</div>
<div class="field">
<label for="latitude">Latitude</label>
<input id="latitude" type="text" readonly="readonly" ng-model="data.coords.latitude" placeholder="No Latitude" />
</div>
</div>
<div class="field username" ng-class="{ error: data.errors.username }">
<label for="username">User Name</label>
<div class="ui labeled icon input">
<input id="username" type="text" ng-model="data.username" ng-change="reserveUsername()" placeholder="User Name" />
<div class="ui red label pointing above" ng-bind="data.errors.username"></div>
<i class="circular ban circle icon"></i>
<i class="circular checkmark icon" ng-if="data.reservation"></i>
<div class="ui corner label">
<i class="icon asterisk"></i>
</div>
</div>
</div>
<div class="inline field user-photo" ng-class="{ error: data.errors.photo }">
<label for="file" class="ui icon button">
<i class="file icon"></i>
Select Photo
</label>
<input type="file" id="file" file-path-changed="fileSelected(files)">
<span class="ui red label" ng-bind="data.errors.photo"></span>
<span class="ui green label" ng-if="data.photoUrl" ng-bind="data.photoUrl"></span>
<div class="ui segment" ng-if="data.photoData">
<img class="rounded ui image" ng-src="{{ data.photoData }}" />
</div>
</div>
<div class="field">
<div class="ui blue submit button" ng-click="register()">Register</div>
</div>
<div class="field">
<span class="ui red label" ng-if="data.errors.submit" ng-bind="data.errors.submit"></span>
<span class="ui green label" ng-if="data.success">
Registration Seccess with {{data.coords.longitude ? 'longitude =' + data.coords.longitude : 'no longitude' }},
{{data.coords.latitude ? 'latitude =' + data.coords.latitude : 'no latitude' }},
username = {{data.username}}, photo url = {{data.photoUrl}}
</span>
</div>
</form>
<script type="text/javascript" src="scripts/module.js"></script>
<script type="text/javascript" src="scripts/directives.js"></script>
<script type="text/javascript" src="scripts/app-service.js"></script>
<script type="text/javascript" src="scripts/app-controller.js"></script>
</body>
</html>
为了创建一个更友好的界面,这里我使用了Semantic UI,Semantic UI是一个漂亮的CSS框架,所以index.html里面包含了一些Semantic UI
中的类和元素。
directives.js
window.module.directive('filePathChanged', function() {
return {
restrict: 'A',
scope: {
filePathChanged: '&'
},
link: function (scope, element, attrs) {
element.bind('change', function() {
scope.filePathChanged({ files: element.prop('files') });
});
}
};
});
总结
在阅读完成此文后,你应该了解了使用回调函数的缺点,然后引出了deferred和promise,并讲解了如何使用他们,而且本文也包含了一些重要的关于promise的方法及其示例,还介绍了链式promise。最后介绍了一个使用promise的教程。下面列出了本文中使用到的方法及简介:
var deferred = $q.defer;
表示构建一个deferred;deferred.resolve(value);
表示deferred被resolve,value为参数;deferred.reject(reason);
表示deferred被reject,reason为参数;var promise = deferred.promise;
表示获得deferred的promise;promise.then(success, failure);
表示为promise分配成功回调(resolve)和失败回调(reject);promise.catch(failure);
表示为promise分配失败回调(和promise.then(null, failure);
等价);promise.finally(always);
表示不论promise被resolve或reject都会执行的回调;var promise = $q.reject(reason);
表示返回一个rejected promise,reason为参数;var promise = $q.when(valueOrPromise);
表示处理valueOrPromise或将其他框架或语言的promise实现转变为AngularJS的promise实现;var promise = $q.all(promisesArr);
表示只有当promisesArr
中所有的promise都被resolve时,返回的才是resolved promise。
下面是源码的链接:
推荐一款个人使用了半年的理财产品:创建了6年的挖财,新人收益36%,7天18%,1年10%,注册送308元券