Knockout验证异步验证器:这是一个错误还是我做错了什么?

dan*_*wig 16 javascript asynchronous knockout-2.0 knockout.js knockout-validation

我非常喜欢Eric Barnard的淘汰验证lib与observable集成,允许分组,并提供自定义验证器可插拔性(包括即时验证器).有几个地方可以提供更多的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);
Run Code Online (Sandbox Code Playgroud)

关于上面代码的几点注意事项:有两个独立的验证组,每个嵌套模型一个.嵌套模型#1没有异步验证器,嵌套模型#2同时具有同步(必需)和异步.async调用服务器调用来验证输入.当服务器响应时,该callback参数用于ko.validation判断用户输入是好还是坏.如果在指定的行上放置断点并使用已知的无效值触发验证,则最终会出现无限循环,其中ajax success函数会导致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;
}
Run Code Online (Sandbox Code Playgroud)

此函数位于附加到用户输入observable的订阅链中,以便在其值更改时,将验证新值.该算法循环连接到输入的每个验证器,并根据验证器是否异步执行单独的函数.如果同步验证失败,则循环中断,整个validateObservable函数退出.如果所有同步验证器都通过,则执行最后3行,基本上告诉ko.validation该输入有效.该__valid__库中的函数如下所示:

//the true holder of whether the observable is valid or not
observable.__valid__ = ko.observable(true);
Run Code Online (Sandbox Code Playgroud)

从中可以看出两件事:__valid__是一个可观察的,并且truevalidateAsync函数退出后设置.现在让我们来看看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);
}
Run Code Online (Sandbox Code Playgroud)

重要的是要注意,在ko.validation.validateObservable__valid__observable 设置为true并退出之前,只执行此函数的第一行和最后一行.该callBack函数作为第3个参数传递给validator声明的异步函数MyViewModel.但是,在此之前,isValidating会调用一个observable的订阅者来通知异步验证已经开始.服务器调用完成后,将调用回调(在这种情况下只传递true或false).

现在这就是为什么断点在MyViewModel服务器端验证失败时导致无限的乒乓循环的原因:在callBack上面的函数中,注意__valid__验证失败时observable 如何设置为false.这是发生的事情:

  1. 无效的用户输入会更改nestedModel2.prop2observable.
  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从改变了它truefalse)这基本上重复上面的步骤2.
  9. 重复上述步骤3,4和5.
  10. 由于observable的值没有改变,服务器返回另一个无效响应,触发上面的步骤6,7,8和9.
  11. 我们有自己的乒乓球比赛.

所以似乎问题是ko.validation.validateObservable订阅处理程序不仅监听用户输入值的更改,还监视其嵌套的__valid__observable的更改.这是一个错误,还是我做错了什么?

第二个问题

您可以从ko.validation上面的来源中看到,在服务器验证时,具有异步验证器的用户输入值被视为有效.因此,nestedModel2.isValid()不能依赖于"真理".相反,看起来我们必须使用isValidating钩子来创建对异步验证器的订阅,并且只有在它们通知值之后才做出这些决定false.这是设计的吗?与库的其余部分相比,这似乎最直观,因为异步验证器没有isValidating订阅,并且可以依靠.isValid()说实话.这也是设计,还是我在这里做错了什么?

dan*_*wig 15

所以我问的问题实际上与如何在ko.validation中使用异步验证器有关.我从经验中学到了两个重要的要点:

  1. 不要创建async 匿名或单次使用自定义规则验证程序.而是将它们创建为自定义规则.否则你将最终得到我的问题中描述的无限循环/ ping ping匹配.

  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() });
    }
};
Run Code Online (Sandbox Code Playgroud)

您还可以看到本作与同类替代解决方案的另一个参考.

以下是异步"自定义规则"的示例:

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 });
        });
    }
};
Run Code Online (Sandbox Code Playgroud)

基本上,您使用callbackarg validator函数来告诉ko.validation验证是否成功.该调用将触发isValidating验证属性observables上的observable返回到false(意味着,异步验证已完成,现在知道输入是否有效).

如果验证成功时服务器端验证端点返回HTTP 200(OK)状态,则上述操作将起作用.这将导致.done函数执行,因为它相当于$.ajax success.如果您的服务器在验证失败时返回HTTP 400(错误请求)状态,它将触发.fail执行的功能.如果您的服务器使用400返回自定义验证消息,您可以从中xhr.responseText有效地覆盖默认you suck because your input was wrong fix it or else消息.