防止表單資料重複提交,是 APP 常見而又必須具備的功能。客戶端最常見的做法是,當使用者點選按鈕的時候,首先把按鈕給禁用,待資料完全提交到服務端後,再讓按鈕處於啟用的狀態。如下圖中的“結算”按鈕。
道理很簡單,實現起來也不難。但是如果全部程式碼都這樣子去寫,未免太煩瑣。我們看一下 ChiTu Store 是如何封裝的。(注:客戶防止重複提交,不意味著服務端不需要防止重複提交。)
一、結算程式碼
開啟 App/Module/Shopping/ShoppingCart.html 頁面,我們找到結算按鈕,結算按鈕繫結到 buy 方法的。
<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 物件。
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; }
二、Click事件的封裝程式碼
開啟 App/Core/ko.ext.ts 檔案,找到關於 click 繫結的程式碼。我們可以看得到,上面的實現,又是通過重寫 knockout js 的 click 繫結來實現的。它主要的實現過程,封裝在了 translateClickAccessor 函式中。
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 物件。
為什麼我們要求是 JQuery.Deferred(JQueryPromise) 物件?因為 JQueryPromise 有 done,fail,always 等方法,通過這些方法,我們可以知道任務是否已經完成。
下面這幾行程式碼,判斷 click 所繫結方法返回的結果是否為 JQueryPromise 物件,當然,這種判斷不是百分百準確,但是對於絕大多數情況來說應該是沒有問題的。只有返回的物件是 JQueryPromise物件,我們才進行處理(if 邏輯塊程式碼)。
var result = $.isFunction(value) ? value(viewModel, event) : value; if (result && $.isFunction(result.always)) { //.......... }
我們來看一下相關的程式碼,下面這段程式碼,還是挺好理解,首先要做的就是在 element 元素(在我們的例子中,是結算按鈕),加上 disabled 的屬性,然後加上一個 disabled 的 class,當執行完成後,如論是成功還是失敗,都取消禁用。當然,我們還要作一個超時的處理,這時的超時設定為 2 秒。
$(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 專案中可以找到。