“JavaScript Promises和AngularJS $q Service”Part 2 (教程篇)

歡迎大家到我的博客關注我學習Ionic 1和Ionic 2的歷程,共同學習,共同進步。

注:本文是譯文,難免有錯誤或理解不足之處,請大家多多指正,大家也可挪步原文。由於本文講解十分精彩,非常推薦大家查看原文,由於原文內容十分豐富,所以將其分爲2部分,這是Part 2(教程篇),戳這裏查看Part 1(基礎篇)

$q Service promise使用教程

現在假設我們的app上需要增加一個註冊的功能,用戶註冊需要提供:用戶當前的座標信息、用戶的照片和用戶名。爲了完成這個註冊的操作,我們的後臺需要獲得如下信息:

  1. 提供當前的座標信息,經度和維度;
  2. 用戶上傳的照片信息保存到服務器中,並返回一個表示此照片的url;
  3. 保存用戶名並返回一個用戶名保存的回執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()還定義了onloadonerror回調函數。注意這裏我使用了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, $qappService三個依賴,當然其中也實現了一些處理數據的方法(教程最後會有完整版的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 UISemantic 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的教程。下面列出了本文中使用到的方法及簡介:

  1. var deferred = $q.defer;表示構建一個deferred;
  2. deferred.resolve(value);表示deferred被resolve,value爲參數;
  3. deferred.reject(reason);表示deferred被reject,reason爲參數;
  4. var promise = deferred.promise;表示獲得deferred的promise;
  5. promise.then(success, failure);表示爲promise分配成功回調(resolve)和失敗回調(reject);
  6. promise.catch(failure);表示爲promise分配失敗回調(和promise.then(null, failure);等價);
  7. promise.finally(always);表示不論promise被resolve或reject都會執行的回調;
  8. var promise = $q.reject(reason);表示返回一個rejected promise,reason爲參數;
  9. var promise = $q.when(valueOrPromise);表示處理valueOrPromise或將其他框架或語言的promise實現轉變爲AngularJS的promise實現;
  10. var promise = $q.all(promisesArr);表示只有當promisesArr中所有的promise都被resolve時,返回的纔是resolved promise。

下面是源碼的鏈接:

推薦一款個人使用了半年的理財產品:創建了6年的挖財,新人收益36%,7天18%,1年10%,註冊送308元券

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章