在揭示模块模式中返回一个 jQuery Promise

Returning a jQuery Promise in Revealing Module Pattern

我正在 Revealing Module 模式中编写一个自定义库来处理特定的 cookie,并尝试使用 jQuery Promise 作为 cookie 的 "Getter" 函数的 return 按顺序防止那些调用函数的人在初始设置之前更新函数,从而保持同步。

见下文:

/**
 * Handles the state cookie for a browser.
 *
 * JS DEPENDENCIES:
 * - jQuery Cookie plugin
 *
 * DOM DEPENDENCIES:
 * - None
 *
 */
var myUtilities = myUtilities || {};

myUtilities.stateManager = (function() {
    var cookieName = 'us_state';

    /**
     * Find geolocation state / set cookie
     * The passed deferred object only gets set as resolved if the AJAX response has the resulting data we need. Otherwise it is rejected.
     *
     * @param  {Object} position Passed from 'navigator.geolocation.getCurrentPosition'. Contains browser's approximation of its current latitude+longitude.
     * @return {Object}          The promise resolution (resolve or reject). Resolved has a String of state abbreviation in lowecase. Rejected is empty.
     */
    function _getLocation(position) {
        var latitude  = position.coords.latitude,
            longitude = position.coords.longitude;

        /* TEST VALUES */
        /* CA coords */
        // latitude  = '37.7833';
        // longitude = '-122.4167';
        /* AZ coords */
        // latitude  = '33.45';
        // longitude = '-112.0667';

        // If this errors out due to CORS issue (or similar issue) of if the return value doesn't match then we set the promise to reject
        return $.ajax({
            url: 'https://maps.googleapis.com/maps/api/geocode/json?latlng=' + latitude + ',' + longitude,
            dataType: "json"
        });
    }

    /**
     * Defer for getCurrentPosition callback
     * Create an anonymous function to handle success; accepts a Position object as argument, and calls _getLocation() passing in the position object.
     * When AJAX promise is complete evalute the data to find the state abbreviation.
     * Reject a failed call for getCurrentPosition (user did not allow/timeout on browser's request to use geolocation)
     *
     * @var {Object} $df jQuery Deferred object
     * @return {Object} jQuery Promise
     */
    function _deferGetLocation() {
        var $df = $.Deferred();

        if ("geolocation" in navigator) {
            navigator.geolocation.getCurrentPosition(
                function(position) {
                    _getLocation(position)
                        .then(function(data) {
                            if (data.length !== 0) {
                                var result  = data.results[0],
                                    address = '',
                                    state   = '';

                                // A for-loop is used because the response changes based on the address that Google API returns (a single search into a specific part of the data Object is not always successful evne though the data may be in there)
                                for (var i = 0, len = result.address_components.length; i < len; i++) {
                                    address = result.address_components[i];

                                    if (address.types.indexOf('administrative_area_level_1') >= 0) {
                                        // By returning here we exit the loop as soon as we get a match, like a 'break'
                                        $df.resolve(address.short_name.toLowerCase());
                                        break;
                                    }
                                }
                            }
                        });
                    });
        } else {
            $df.reject();
        }

        return $df.promise();
    }

    /**
     * Either get the get cookie or set it now.
     * If the cookie exists we resolve the promise immediately, else wait for the geolocation to be resolved, set state cookie and resolve.
     *
     * @var {Object} $df         jQuery Deferred object
     * @var {String} stateString state, 2 character abbreviation format
     * @return {Object} Promise with a String for the callback (two-character value indicating which state the user is in)
     */
    function _getStateCookie(){
        var $df = $.Deferred();

        if ($.cookie(cookieName)) {
            $df.resolve($.cookie(cookieName));
        } else {
            _deferGetLocation()
                .then(function(state) {
                    $df.resolve(_setStateCookie(state));
                });
        }

        return $df.promise();
    }

    /**
     * Set the 'cookieName' cookie to a desired state, or default to 'co'
     *
     * @param {String} state The value of the cookie as a 2 character length state abbreviation
     * @param {Datetime} expirationDate Days until the cookie expires
     */
    function _setStateCookie (state, expirationDate){
        state          = ( typeof state == 'undefined' || !_isValidState(state) ) ? 'co' : state;
        expirationDate = ( typeof expirationDate == 'undefined' ) ? 365 : expirationDate;

        $.cookie(cookieName, state, { path: '/', expires: expirationDate });

        // Offer an event listener for this cookie
        $(document).trigger('state-utility.cookieChange');

        return state;
    }

    /**
     * Validates a given string against our predetermined "valid states" (AZ, CA, CA).
     * Returns  true if valid, false otherwise.
     * Case-sensitive, AZ == az -> false
     *
     * @param  {String}  state A value to be compared for valid state
     * @return {Boolean}       True if valid, false otherwise
     */
    function _isValidState(state) {
        return (state == 'az' || state == 'ca' || state == 'ca');
    }

    function _isCookieSet() {
        return ($.cookie(cookieName) && _isValidState($.cookie(cookieName)));
    }

    return {
        // Using a Promise so that multiple calls to _getStateCookie() are handled synchronously
        getStateCookie : function() {
            return _getStateCookie().then( function(state) { return state; });
        },
        setStateCookie : function(state, expirationDate) {
            return _setStateCookie(state, expirationDate);
        },
        updateStateElement : function(target) {
            return _updateStateElement(target);
        },
        isValidState : function(state) {
            return _isValidState(state);
        },
        isCookieSet : function() {
            return _isCookieSet();
        }
    };
})();
<script src="https://raw.githubusercontent.com/carhartl/jquery-cookie/master/src/jquery.cookie.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

当尝试使用 myUtilities.stateManager.getStateCookie() 检索 cookie 的值时出现问题。我希望这个对 return 的调用带有两个字符的字符串,表示最近的适用状态。相反,我得到了 Promise 对象 returned.

为什么 return 编辑的是 Promise 而不是字符串,需要更改哪些内容才能 return 所需的字符串?

感谢您的宝贵时间。

您只能通过附加 .then() 处理程序并异步执行所有 .then() 处理程序来获得超出承诺的值。

所以,这段代码根本行不通:

getStateCookie : function() {
        return _getStateCookie().then( function(state) { return state; });
},

那只是 return 一个价值为 state 的承诺。请注意,.then() 处理程序不会在此处向您的代码添加任何内容。

你的结果是异步的。你无法改变这一点。调用者必须将其作为异步处理。这意味着调用者将通过回调函数获得结果。该回调可以是普通回调或承诺回调。由于您已经在使用 promises,因此最简单的方法是 return 将具有值的 promise 并让调用者将其自己的 .then() 处理程序放在上面,以便它可以获得传递给它的值自己的回调函数。

我建议这样做:

getStateCookie : function() {
    return _getStateCookie();
},

然后调用者这样使用它:

myUtilities.stateManager.getStateCookie().then(function(state) {
    // callers code goes here to use state
});

恐怕您永远无法期望从 javascript 中的异步过程中获得同步结果。您所做的任何事情都不会将异步转换为同步。您(在可预见的未来的某一天)所能期望的最好的结果是使异步代码看起来更像同步的语法。

这里有一些建议...

_getLocation(),我会:

  • 添加失败处理程序以将 jQuery.ajax 的错误报告规范化为单一原因。
function _getLocation(position) {
    var latitude  = position.coords.latitude,
        longitude = position.coords.longitude;

    return $.ajax({
        url: 'https://maps.googleapis.com/maps/api/geocode/json?latlng=' + latitude + ',' + longitude,
        dataType: "json"
    }).then(null, function(jqXHR, textStatus, errorThrown) {
        return errorThrown;
    });
}

_deferGetLocation(),我会:

  • 清除 _deferGetLocation() 中的显式承诺构造反模式。
  • 提供在两个条件下拒绝承诺的原因。
  • 稍微整理一下。
function _deferGetLocation() {
    var promise;
    if ("geolocation" in navigator) {
        navigator.geolocation.getCurrentPosition(function(position) {
            promise = _getLocation(position).then(function(data) {
                var result = data.results[0],
                    state;
                if (data.length !== 0) {
                    // A for-loop is used because the response changes based on the address that Google API returns (a single search into a specific part of the data Object is not always successful even though the data may be in there)
                    for (var i = 0, len = result.address_components.length; i < len; i++) {
                        if (result.address_components[i].types.indexOf('administrative_area_level_1') >= 0) {
                            state = result.address_components[i].short_name.toLowerCase();
                            break;
                        }
                    }
                }
                return state || $.Deferred().reject('geolocation failed').promise();
            });
        });
    return promise || $.Deferred().reject('browser does not support geolocation').promise();
}

在重命名后_getStateCookie(),我会:

  • 重命名为 _getStateCookieAsync() 作为对消费者的警告,该方法 return 是一个承诺。
  • _getStateCookie() 中清除显式承诺构造反模式并简化(重要的时间)。
function _getStateCookieAsync() {
    var state = $.cookie(cookieName);
    return (state) ? $.when(state) : _deferGetLocation().then(_setStateCookie);
}

在公开方法的 return 语句中,我会:

  • 仅公开必要的内容 - 没有义务公开所有方法。
  • 通过函数名称公开 - 不需要额外的函数包装器。
return {
    getStateCookieAsync : _getStateCookieAsync,
    setStateCookie : _setStateCookie, // will it ever be set from outside?
    // updateStateElement : _updateStateElement, // doesn't exist
    isValidState : _isValidState, // probably only of use internally
    isCookieSet : _isCookieSet
};