Проверка нокаута. Валидаторы async: Является ли это ошибкой или я делаю что-то неправильно?

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

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

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);

Несколько замечаний по поводу кода выше: есть две отдельные группы проверки, по одной для каждой вложенной модели. Вложенная модель # 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. Если все валидаторы синхронизации ko.validation, выполняются последние 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 функция получает то, что передается в качестве 3 - го параметра в асинхронном validator функции, объявленной в MyViewModel. Однако, прежде чем это произойдет, isValidating наблюдаемые подписчики isValidating, чтобы уведомить о начале асинхронной проверки. Когда серверный вызов завершен, вызывается обратный вызов (в этом случае просто передается значение true или false).

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

  1. nestedModel2.prop2 пользовательский ввод изменяет наблюдаемую nestedModel2.prop2.
  2. ko.validation.validateObservable уведомляется посредством подписки об этом изменении.
  3. Функция validateAsync вызывается.
  4. $.ajax пользовательский асинхронный валидатор, который отправляет асинхронный вызов $.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 подписки ko.validation.validateObservable прослушивает изменения не только входного значения пользователя, но и его вложенного __valid__ наблюдаемого. Это ошибка или я что-то не так делаю?

Вторичный вопрос

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

Ответы

Ответ 1

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

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

  2. Если вы используете async валидаторы, не доверяйте isValid() пока subscriptions isValidating всех async валидаторов isValidating изменятся на 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 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 observables на наблюдаемых свойствах свойства изменить обратно на false (имеется в виду, что асинхронная проверка завершена, и теперь известно, был ли вход действительным или нет).

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

Ответ 2

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