歡迎大家到我的博客關注我學習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元券