Возвращение обещания jQuery в раскрытии шаблона модуля

Я пишу пользовательскую библиотеку в шаблоне "Модуль раскрытия" для обработки определенного файла cookie и пытаюсь использовать обещание jQuery в качестве возврата для функции "Получатель" файла cookie, чтобы те, кто вызывает функцию, не обновляли функцию до ее первоначальной установки. сохраняя это синхронно.

Увидеть ниже:

/**
 * 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>

Когда возникает проблема при попытке получить значение cookie с помощью myUtilities.stateManager.getStateCookie(), Я ожидаю, что этот вызов вернется с двухсимвольной строкой для ближайшего применимого состояния. Вместо этого я возвращаю объект Promise.

Почему Promise возвращается вместо строки, и что нужно изменить, чтобы получить нужную строку?

Спасибо за ваше время.

2 ответа

Решение

Я боюсь, что вы никогда не сможете получить синхронный результат от асинхронного процесса в 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() в качестве предупреждения для потребителей, что метод возвращает обещание.
  • очистить явное обещание строительства антипаттерна от _getStateCookie() и упростить (большое время).
function _getStateCookieAsync() {
    var state = $.cookie(cookieName);
    return (state) ? $.when(state) : _deferGetLocation().then(_setStateCookie);
}

И в заявлении возврата метода я бы:

  • выставлять только то, что необходимо - нет никаких обязательств раскрывать каждый метод.
  • Экспонировать по имени функции - нет необходимости в дополнительных оболочках функций.
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
};

Вы только получаете ценность из обещания, прикрепляя .then() обработчик и все .then() обработчики выполняются асинхронно.

Итак, этот код просто не работает:

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

Это просто вернет обещание, чья ценность state, Обратите внимание .then() Обработчик ничего не добавляет к вашему коду здесь.

Ваш результат асинхронный. Вы не можете это изменить. Абоненты должны иметь дело с этим как асинхронный. Это означает, что вызывающая сторона получит результат через функцию обратного вызова. Этот обратный вызов может быть простым обратным вызовом или обещанием обратного вызова. Поскольку вы уже используете обещания, проще всего вернуть обещание, которое будет иметь значение в нем, и позволить вызывающей стороне указать свое .then() обработчик, чтобы он мог получить значение, переданное его собственной функции обратного вызова.

Я бы предложил это:

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

И тогда вызывающие абоненты используют это следующим образом:

myUtilities.stateManager.getStateCookie().then(function(state) {
    // callers code goes here to use state
});
Другие вопросы по тегам