“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元券

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