Асинхронные валидаторы Knockout Validation: это ошибка или я делаю что-то не так?

Мне очень нравится, как библиотека проверки нокаута Эрика Барнарда интегрируется с наблюдаемыми, позволяет группировать и предлагать возможность подключения настраиваемых валидаторов (включая валидаторы «на лету»). Есть несколько мест, где он мог бы быть более гибким/дружественным к UX, но в целом он достаточно хорошо документирован... за исключением, imo, когда речь идет об асинхронных валидаторах.

Сегодня я боролся с этим несколько часов, прежде чем выполнить поиск и приземлиться на это< /а>. Я думаю, что у меня те же проблемы/вопросы, что и у первоначального автора, но согласен, что было неясно, о чем именно просила duxa. Я хочу привлечь к этому вопросу больше внимания, поэтому я также спрашиваю здесь.

function MyViewModel() {
    var self = this;
    self.nestedModel1.prop1 = ko.observable().extend({
        required: { message: 'Model1 Prop1 is required.' },
        maxLength: {
            params: 140,
            message: '{0} characters max please.'
        }
    });
    self.nestedModel2.prop2 = ko.observable().extend({
        required: { message: 'Model2 Prop2 is required' },
        validation: {
            async: true,
            validator: function(val, opts, callback) {
                $.ajax({                                  // BREAKPOINT #1
                    url: '/validate-remote',
                    type: 'POST',
                    data: { ...some data... }
                })
                .success(function(response) {
                    if (response == true) callback(true); // BREAKPOINT #2
                    else callback(false);
                });
            },
            message: 'Sorry, server says no :('
        }
    });
}

ko.validation.group(self.nestedModel1);
ko.validation.group(self.nestedModel2);

Несколько замечаний по коду выше: есть 2 отдельные группы проверки, по одной для каждой вложенной модели. Вложенная модель № 1 не имеет асинхронных валидаторов, а вложенная модель № 2 имеет как синхронизацию (обязательно), так и асинхронность. Асинхронный вызов вызывает сервер для проверки входных данных. Когда сервер отвечает, аргумент callback используется, чтобы сообщить ko.validation, является ли ввод пользователя хорошим или плохим. Если вы поставите точки останова на указанные строки и активируете проверку, используя заведомо недопустимое значение, вы получите бесконечный цикл, в котором функция success ajax вызывает повторный вызов функции validator. Я взломал ko.validation Источник, чтобы увидеть, что происходит.

ko.validation.validateObservable = function(observable) {
    // set up variables & check for conditions (omitted for brevity)

    // loop over validators attached to the observable
    for (; i < len; i++) {
        if (rule['async'] || ctx['async']) {
            //run async validation
            validateAsync();
        } else {
            //run normal sync validation
            if (!validateSync(observable, rule, ctx)) {
                return false; //break out of the loop
            }
        }
    }

    //finally if we got this far, make the observable valid again!
    observable.error = null;
    observable.__valid__(true);
    return true;
}

Эта функция находится в цепочке подписки, прикрепленной к наблюдаемому пользователем вводу, поэтому при изменении ее значения новое значение будет проверено. Алгоритм перебирает каждый валидатор, подключенный к входу, и выполняет отдельные функции в зависимости от того, является ли валидатор асинхронным или нет. Если проверка синхронизации не удалась, цикл прерывается, и вся функция validateObservable завершает работу. Если все валидаторы синхронизации проходят успешно, выполняются последние 3 строки, по сути сообщая ko.validation, что этот ввод действителен. Функция __valid__ в библиотеке выглядит так:

//the true holder of whether the observable is valid or not
observable.__valid__ = ko.observable(true);

Из этого следует вынести две вещи: __valid__ — это наблюдаемая величина, и она устанавливается в true после выхода из функции validateAsync. Теперь давайте посмотрим на validateAsync:

function validateAsync(observable, rule, ctx) {
    observable.isValidating(true);

    var callBack = function (valObj) {
        var isValid = false,
            msg = '';

        if (!observable.__valid__()) {
            // omitted for brevity, __valid__ is true in this scneario
        }

        //we were handed back a complex object
        if (valObj['message']) {
            isValid = valObj.isValid;
            msg = valObj.message;
        } else {
            isValid = valObj;
        }

        if (!isValid) {
            //not valid, so format the error message...
            observable.error = ko.validation.formatMessage(...);
            observable.__valid__(isValid);
        }

        // tell it that we're done
        observable.isValidating(false);
    };

    //fire the validator and hand it the callback
    rule.validator(observable(), ctx.params || true, callBack);
}

Важно отметить, что только первая и последняя строки этой функции выполняются до того, как ko.validation.validateObservable установит для наблюдаемого __valid__ значение true и выйдет. Функция callBack — это то, что передается в качестве третьего параметра асинхронной функции validator, объявленной в MyViewModel. Однако до того, как это произойдет, вызываются подписчики наблюдаемого isValidating, чтобы уведомить о начале асинхронной проверки. Когда вызов сервера завершен, вызывается обратный вызов (в данном случае просто передается значение true или false).

Теперь вот почему точки останова в MyViewModel вызывают бесконечный цикл пинг-понга при сбое проверки на стороне сервера: Обратите внимание, что в вышеприведенной функции callBack наблюдаемое значение __valid__ устанавливается в false, когда проверка не удалась. Вот что происходит:

  1. Недопустимый пользовательский ввод изменяет наблюдаемую nestedModel2.prop2.
  2. ko.validation.validateObservable уведомляется через подписку об этом изменении.
  3. Вызывается функция validateAsync.
  4. Вызывается настраиваемый асинхронный валидатор, который отправляет асинхронный вызов $.ajax на сервер и завершает работу.
  5. ko.validation.validateObservable устанавливает наблюдаемый объект __valid__ равным true и завершает работу.
  6. Сервер возвращает недопустимый ответ, и выполняется callBack(false).
  7. Функция callBack устанавливает __valid__ в false.
  8. ko.validation.validateObservable уведомляется об изменении наблюдаемого __valid__ (callBack изменил его с true на false). Это по существу повторяет шаг 2 выше.
  9. Шаги 3, 4 и 5, описанные выше, повторяются.
  10. Поскольку значение наблюдаемого не изменилось, сервер возвращает еще один недопустимый ответ, запуская шаги 6, 7, 8 и 9 выше.
  11. У нас есть матч по пинг-понгу.

Таким образом, похоже, проблема в том, что обработчик подписки ko.validation.validateObservable прослушивает изменения не только значения пользовательского ввода, но также изменений своего вложенного наблюдаемого объекта __valid__. Это баг, или я что-то не так делаю?

Второстепенный вопрос

Вы можете видеть из ko.validation источников выше, что введенное пользователем значение с асинхронным валидатором рассматривается как действительное, пока сервер проверяет его. Из-за этого на звонок nestedModel2.isValid() нельзя полагаться на «правду». Вместо этого похоже, что мы должны использовать хуки isValidating для создания подписок на асинхронные валидаторы и принимать эти решения только после того, как они уведомят о значении false. Это по дизайну? По сравнению с остальной частью библиотеки это кажется наиболее нелогичным, потому что не асинхронные валидаторы не имеют isValidating для подписки и могут полагаться на .isValid(), чтобы говорить правду. . Это тоже так задумано, или я тоже что-то не так делаю?


person danludwig    schedule 04.10.2012    source источник
comment
Вы приложили много усилий к этому вопросу, жаль, что у вас нет ответов. Я только новичок в проверке KO, поэтому я не могу вам помочь.   -  person GONeale    schedule 10.01.2013
comment
С этим вопросом есть некоторые проблемы. Фактически заданный вопрос не ясен и скрыт во внешней ссылке. Мне пришлось прочитать несколько абзацев текста, прежде чем я понял это. Этот вопрос имеет слишком много контекста; Я не вижу леса за деревьями. И вы, кажется, задаете несколько вопросов. Предложение: создайте простейшее воспроизведение проблемы в JSFiddle, дайте ссылку на него в своем вопросе и задайте краткий вопрос в первом абзаце. Сделайте это, и у вас будет больше шансов получить некоторые ответы.   -  person Judah Gabriel Himango    schedule 08.03.2013
comment
Можете ли вы обновить ИЛИ, возможно, ответить на ваш вопрос? Я вижу, что вы нашли решение по этому URL-адресу... github.com /ericmbarnard/Knockout-Validation/issues/145. Ваш ответ поможет многим людям, так как информации об асинхронной проверке не так много.   -  person bflemi3    schedule 09.04.2013


Ответы (2)


Таким образом, вопрос, который я задал, действительно касался того, как использовать асинхронные валидаторы в ko.validation. Есть 2 важных вывода, которые я вынес из своего опыта:

  1. Не создавайте async Анонимные или одноразовые валидаторы пользовательских правил. Вместо этого создайте их как Пользовательские правила. В противном случае вы получите бесконечный цикл/пинг-пинг, описанный в моем вопросе.

  2. Если вы используете async валидаторов, не доверяйте isValid() до тех пор, пока все async валидаторы isValidating subscriptions не изменятся на false.

Если у вас есть несколько асинхронных валидаторов, вы можете использовать следующий шаблон:

var viewModel = {
    var self = this;
    self.prop1 = ko.observable().extend({validateProp1Async: self});
    self.prop2 = ko.observable().extend({validateProp2Async: self});
    self.propN = ko.observable();
    self.isValidating = ko.computed(function() {
        return self.prop1.isValidating() || self.prop2.isValidating();
    });
    self.saveData = function(arg1, arg2, argN) {

        if (self.isValidating()) {
            setTimeout(function() {
                self.saveData(arg1, arg2, argN);
            }, 50);
            return false;
        }

        if (!self.isValid()) {
            self.errors.showAllMessages();
            return false;
        }

        // data is now trusted to be valid
        $.post('/something', 'data', function() { doWhatever() });
    }
};

Вы также можете посмотреть еще один справочник с аналогичными альтернативными решениями.

Вот пример асинхронного «настраиваемого правила»:

var validateProp1Async = {
    async: true,
    message: 'you suck because your input was wrong fix it or else',
    validator: function(val, otherVal, callback) {
        // val will be the value of the viewmodel's prop1() observable
        // otherVal will be the viewmodel itself, since that was passed in
        //     via the .extend call
        // callback is what you need to tell ko.validation about the result
        $.ajax({
            url: '/path/to/validation/endpoint/on/server',
            type: 'POST', // or whatever http method the server endpoint needs
            data: { prop1: val, otherProp: otherVal.propN() } // args to send server
        })
        .done(function(response, statusText, xhr) {
            callback(true); // tell ko.validation that this value is valid
        })
        .fail(function(xhr, statusText, errorThrown) {
            callback(false); // tell ko.validation that his value is NOT valid
            // the above will use the default message. You can pass in a custom
            // validation message like so:
            // callback({ isValid: false, message: xhr.responseText });
        });
    }
};

По сути, вы используете аргумент callback для функции validator, чтобы сообщить ko.validation, прошла ли проверка успешно. Именно этот вызов вызовет изменение наблюдаемых значений isValidating в наблюдаемых объектах проверенного свойства обратно на false (это означает, что асинхронная проверка завершена, и теперь известно, был ли ввод действительным или нет).

Вышеприведенное будет работать, если ваши конечные точки проверки на стороне сервера возвращают статус HTTP 200 (ОК) после успешной проверки. Это приведет к выполнению функции .done, поскольку она эквивалентна функции $.ajax success. Если ваш сервер возвращает статус HTTP 400 (неверный запрос) при сбое проверки, он инициирует выполнение функции .fail. Если ваш сервер возвращает пользовательское сообщение проверки обратно с 400, вы можете получить его от xhr.responseText, чтобы эффективно переопределить сообщение you suck because your input was wrong fix it or else по умолчанию.

person danludwig    schedule 09.04.2013
comment
Спасибо, это мне очень помогло :) - person bflemi3; 10.04.2013
comment
Не могли бы вы также привести пример вашего правила проверки validateProp1Async? - person bflemi3; 10.04.2013
comment
На странице асинхронных правил в вики нокаут-валидации (github.com/ericmbarnard/ Knockout-Validation/wiki/Async-Rules) показанный пример ajax меня смущает. validator не возвращает true или false. Как правило будет проверять значение, если оно не возвращает true или false? Вот почему я попросил показать ваше правило проверки. Извините за все вопросы - не так много документации, кроме Эрика Барнарда, вы единственный, кого я видел, кто, кажется, понимает, как именно работает асинхронность. - person bflemi3; 10.04.2013
comment
Я обновил ответ. Функция валидатора не возвращает true или false, что делает ее неасинхронной. Асинхронность означает, что вы обещаете вернуть значение позже, а для этого вам нужны функции обратного вызова. Один предоставляется в качестве третьего аргумента функции проверки подлинности, когда ее вызывает ko.validation. - person danludwig; 10.04.2013
comment
и еще +1 за воодушевляющее сообщение о проверке - person bflemi3; 10.04.2013
comment
Спасибо Дэн за огромное количество усилий, которые вы вложили в этот пост. Это мне очень помогло, так как документации довольно мало. К вашему сведению... Ссылка на проблему с github не работает. - person Kit Menke; 04.06.2013
comment
Кроме того, не могли бы вы дать некоторое представление о том, действительно ли требуется ko.validation.registerExtenders();? Документация говорит, что это так, но мои тесты показывают обратное... - person Kit Menke; 04.06.2013
comment
ko.validation.registerExtenders(); должен быть необходим только в том случае, если вы создаете свои собственные валидаторы. Библиотека вызывает его внутри для готовых валидаторов. - person danludwig; 04.06.2013

У меня была такая же проблема, вложенные наблюдаемые с проверкой. Итак, одна магия: в self.errors = ko.validation.group(self.submissionAnswers, { deep: true, live: true }); обратите внимание на специальный дополнительный параметр: объект, который содержит поле live: true

person Maksym Bykov    schedule 05.06.2019