Проверка нокаута. Валидаторы 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, если проверка не удалась. Вот что происходит:
-
nestedModel2.prop2
пользовательский ввод изменяет наблюдаемую nestedModel2.prop2
. -
ko.validation.validateObservable
уведомляется посредством подписки об этом изменении. - Функция
validateAsync
вызывается. -
$.ajax
пользовательский асинхронный валидатор, который отправляет асинхронный вызов $.ajax
на сервер и завершает работу. -
ko.validation.validateObservable
устанавливает для __valid__
наблюдаемого значение true
и завершается. - Сервер возвращает неверный ответ, и
callBack(false)
выполняется. - Функция
callBack
устанавливает для __valid__
значение false
. -
ko.validation.validateObservable
уведомляется об изменении наблюдаемой __valid__
(callBack
изменил его с true
на false
). По сути, это повторяет шаг 2 выше. - Шаги 3, 4 и 5 выше повторяются.
- Поскольку наблюдаемое значение не изменилось, сервер возвращает другой недопустимый ответ, инициируя шаги 6, 7, 8 и 9 выше.
- У нас есть матч для пинг-понга.
Таким образом, похоже, что проблема в том, что ko.validation.validateObservable
подписки ko.validation.validateObservable
прослушивает изменения не только входного значения пользователя, но и его вложенного __valid__
наблюдаемого. Это ошибка или я что-то не так делаю?
Вторичный вопрос
Из ko.validation
источников ko.validation
что пользовательское ko.validation
значение с асинхронным валидатором считается действительным, пока его ko.validation
сервер. Из-за этого на вызов nestedModel2.isValid()
нельзя полагаться как на "правду". Вместо этого, похоже, что мы должны использовать хуки isValidating
для создания подписок на асинхронные валидаторы и принимать эти решения только после того, как они сообщат значение false
. Это по замыслу? По сравнению с остальной частью библиотеки это кажется наиболее противоречивым, поскольку не асинхронные валидаторы не имеют isValidating
для подписки и могут полагаться на .isValid()
чтобы сказать правду. Это тоже задумано, или я здесь тоже что-то не так делаю?
Ответы
Ответ 1
Поэтому вопрос, который я задал, действительно касался использования асинхронных валидаторов в ko.validation. Есть два больших навыка, которые я узнал из своего опыта:
-
Не создавайте async
анонимные или одноразовые валидаторы пользовательских правил. Вместо этого создайте их как пользовательские правила. В противном случае вы получите бесконечное совпадение циклы/пинга, описанное в моем вопросе.
-
Если вы используете 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