防止表单数据重复提交,是 APP 常见而又必须具有的功能。客户端最多见的作法是,当用户点击按钮的时候,首先把按钮给禁用,待数据彻底提交到服务端后,再让按钮处于启用的状态。以下图中的“结算”按钮。html
道理很简单,实现起来也不难。可是若是所有代码都这样子去写,未免太烦琐。咱们看一下 ChiTu Store 是如何封装的。(注:客户防止重复提交,不意味着服务端不须要防止重复提交。)jquery
打开 App/Module/Shopping/ShoppingCart.html 页面,咱们找到结算按钮,结算按钮绑定到 buy 方法的。git
<button class="btn btn-primary" type="button" data-bind="click:buy, disable:productsCount()<=0">结算(<span data-bind="text:productsCount"></span>)</button>
咱们再来看一下 buy 方法(注:代码有删减),值得注意的时候,buy 方法是返回一个 Deferred 对象,也就是 jquery 中的类型为 JQueryPromise 的对象。关于 Promise 对象,这里不展开讲,不了解自行 Google。在后面,我会告诉你们,为何要返回一个 JQueryPromise 对象。github
buy = () => { var deferred = $.Deferred(); shopping.createOrder(productIds, quantities) .done((order) => { app.redirect('Shopping_OrderProducts_' + order.Id()); deferred.resolve(order); }) .fail((data) => { deferred.reject(data); }); return deferred; }
打开 App/Core/ko.ext.ts 文件,找到关于 click 绑定的代码。咱们能够看获得,上面的实现,又是经过重写 knockout js 的 click 绑定来实现的。它主要的实现过程,封装在了 translateClickAccessor 函数中。app
var _click = ko.bindingHandlers.click; ko.bindingHandlers.click = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { valueAccessor = translateClickAccessor(element, valueAccessor, allBindings, viewModel, bindingContext); return _click.init(element, valueAccessor, allBindings, viewModel, bindingContext); } };
关于 translateClickAccessor 函数(代码有删减),是这样子:函数
function translateClickAccessor(element, valueAccessor, allBindings, viewModel, bindingContext) { var value = ko.unwrap(valueAccessor()); if (value == null) { return valueAccessor; } return $.proxy(function () { var element = this._element; var valueAccessor = this._valueAccessor; var allBindings = this._allBindings; var viewModel = this._viewModel; var bindingContext = this._bindingContext; var value = this._value; return function (viewModel) { var deferred: JQueryPromise<any> = $.Deferred<any>().resolve(); deferred = deferred.pipe(function () { var result = $.isFunction(value) ? value(viewModel, event) : value; if (result && $.isFunction(result.always)) { $(element).attr('disabled', 'disabled'); $(element).addClass('disabled'); result.element = element; result.always(function () { $(element).removeAttr('disabled'); $(element).removeClass('disabled'); }); //=============================================== // 超时去掉按钮禁用,防止 always 不起做用。 setTimeout($.proxy(function () { $(this._element).removeAttr('disabled'); $(this._element).removeClass('disabled'); }, { _element: element }), 1000 * 20); //=============================================== }); } return result; }); return deferred; }; }, { _element: element, _valueAccessor: valueAccessor, _allBindings: allBindings, _viewModel: viewModel, _bindingContext: bindingContext, _value: value }); }
咱们这来看第一句:var value = ko.unwrap(valueAccessor()) 这句话是获取绑定到 click 方法的函数的返回值,在咱们这个例子里,是 JQuery.Deferred 对象。this
为何咱们要求是 JQuery.Deferred(JQueryPromise) 对象?由于 JQueryPromise 有 done,fail,always 等方法,经过这些方法,咱们能够知道任务是否已经完成。spa
下面这几行代码,判断 click 所绑定方法返回的结果是否为 JQueryPromise 对象,固然,这种判断不是百分百准确,可是对于绝大多数状况来讲应该是没有问题的。只有返回的对象是 JQueryPromise对象,咱们才进行处理(if 逻辑块代码)。code
var result = $.isFunction(value) ? value(viewModel, event) : value; if (result && $.isFunction(result.always)) { //.......... }
咱们来看一下相关的代码,下面这段代码,仍是挺好理解,首先要作的就是在 element 元素(在咱们的例子中,是结算按钮),加上 disabled 的属性,而后加上一个 disabled 的 class,当执行完成后,如论是成功仍是失败,都取消禁用。固然,咱们还要做一个超时的处理,这时的超时设置为 2 秒。htm
$(element).attr('disabled', 'disabled'); $(element).addClass('disabled'); result.element = element; result.always(function () { $(element).removeAttr('disabled'); $(element).removeClass('disabled'); }); //=============================================== // 超时去掉按钮禁用,防止 always 不起做用。 setTimeout($.proxy(function () { $(this._element).removeAttr('disabled'); $(this._element).removeClass('disabled'); }, { _element: element }), 1000 * 20); //===============================================
相关的代码,在 github 的 ChiTuStore 项目中能够找到。