怎樣防止重複發送 Ajax 請求?
一個簡易的需求,點一個按鈕,則向伺服器請求資源,不作處理時,多次點擊後會有很多個請求在等待。粗暴的解決方式是點一次就將按鈕disable掉。請問一下有沒有更好的辦法,比如多點一次後自動down掉前一次請求?補充:不是一次請求,更類似於gmail的全站AJAX,剛用firebug看了一下gmail,發現重複請求時,之前的請求狀態變為「Aborted」,並且不反回任何數據。請問是如何做到的呢?畢竟一般理解客戶端AJAX發送後是不能終止的。
不推薦用外部變數鎖定或修改按鈕狀態的方式,因為那樣比較難:
- 要考慮並理解 success, complete, error, timeout 這些事件的區別,並註冊正確的事件,一旦失誤,功能將不再可用;
- 不可避免地比普通流程要要多註冊一個 complete 事件;
- 恢復狀態的代碼很容易和不相干的代碼混合在一起;
我推薦用主動查詢狀態的方式(A、B,jQuery 為例)或工具函數的方式(C、D)來去除重複操作,並提供一些例子作為參考:
A. 獨佔型提交只允許同時存在一次提交操作,並且直到本次提交完成才能進行下一次提交。
module.submit = function() {
if (this.promise_.state() === "pending") {
return
}
return this.promise_ = $.post("/api/save")
}
B. 貪婪型提交
無限制的提交,但是以最後一次操作為準;亦即需要儘快給出最後一次操作的反饋,而前面的操作結果並不重要。
module.submit = function() {
if (this.promise_.state() === "pending") {
this.promise_.abort()
}
// todo
}
比如某些應用的條目中,有一些進行類似「喜歡」或「不喜歡」操作的二態按鈕。如果按下後不立即給出反饋,用戶的目光焦點就可能在那個按鈕上停頓許久;如果按下時即時切換按鈕的狀態,再在程序上用 abort 來實現積極的提交,這樣既能提高用戶體驗,還能降低伺服器壓力,皆大歡喜。
C. 節制型提交無論提交如何頻繁,任意兩次有效提交的間隔時間必定會大於或等於某一時間間隔;即以一定頻率提交。
module.submit = throttle(150, function() {
// todo
})
如果客戶發送每隔100毫秒發送過來10次請求,此模塊將只接收其中6個(每個在時間線上距離為150毫秒)進行處理。
這也是解決查詢衝突的一種可選手段,比如以知乎草稿舉例,仔細觀察可以發現:
編輯器的 blur 事件會立即觸發保存;
保存按鈕的 click 事件也會立即觸發保存;
但是存在一種情況會使這兩個事件在數毫秒內連續發生——當焦點在編輯器內部,並且直接去點擊保存按鈕——這時用 throttle 來處理是可行的。
另外還有一些事件處理會很頻繁地使用 throttle,如: resize、scroll、mousemove。
任意兩次提交的間隔時間,必須大於一個指定時間,才會促成有效提交;即不給休息不幹活。
module.submit = debounce(150, function() {
// todo
})
還是以知乎草稿舉例,當在編輯器內按下 ctrl + s 時,可以手動保存草稿;如果你連按,程序會表示不理解為什麼你要連按,只有等你放棄連按,它才會繼續。
============
更多記憶中的例子
方式 C 和 方式 D 有時更加通用,比如這些情況:
- 遊戲中你撿到一把威力強大的高速武器,為了防止你的子彈在屏幕上打成一條直線,可以 throttle 來控制頻率;
- 在彈幕型遊戲里,為了防止你把射擊鍵夾住來進行無腦遊戲,可以用 debounce 來控制頻率;
- 在編譯任務里,守護進程監視了某一文件夾里所有的文件(如任一文件的改變都可以觸發重新編譯,一次執行就需要2秒),但某種操作能夠瞬間造成大量文件改變(如 git checkout),這時一個簡單的 debounce 可以使編譯任務只執行一次。
而方式 C 甚至可以和方式 B 組合使用,比如自動完成組件(Google 首頁的搜索就是):
- 當用戶快速輸入文本時(特別是打字能手),可以 throttle keypress 事件處理函數,以指定時間間隔來提取文本域的值,然後立即進行新的查詢;
- 當新的查詢需要發送,但上一個查詢還沒返回結果時,可以 abort 未完成的查詢,並立即發送新查詢;
----- update 2013-01-08 -----
E. 記憶型
var scrape = memoize(function(url) {
return $.post("/scraper", { "url": url })
})
對於同樣的參數,其返回始終結果是恆等的——每次都將返回同一對象。
應用例子有編輯器,如粘貼內容時抓取其中的鏈接信息,memoize 用以保證同樣的鏈接不會抓取兩次。
F. 累積型
前幾天處理自動完成事件時得到這個函數,發現也可以用在處理連續事件上,它能夠把連續的多次提交合併為一個提交,比如:
var request = makePile(5, function() {
$.post("/", { list: JSON.stringify([].slice.call(arguments)) })
})
// 連續發送五次
request({a:1}), request({a:2}), request({a:3}), request({a:4}), request({a:5})
/* post =&>
list:[{"a":1},{"a":2},{"a":3},{"a":4},{"a":5}]
*/
樣例實現:
var makePile = function(count, onfilter, onvalue) {
var values = [], id = function(value) { return value }
return function(value) {
values.push((onvalue || id).apply(this, arguments))
if (values.length === count) {
onfilter.apply(this, values)
values = []
}
}
}
----- update 2013-04-16 -----
另一種累積是按時間而不是次數,比如應用在行為統計上,可能在瞬間收集到數十上百類似的行為,這時可以用上面 pile 的結構加上 debounce 來防止大批重複請求(但又不丟失任何統計):
var trackFactory = function(delay, action) {
var params = [], slice = [].slice
var touch = debounce(delay, function() {
if (params.length) {
action(params)
params = []
}
})
return function() {
params.push(slice.call(arguments))
touch()
}
}
var track = trackFactory(550, function(params) {
// send tracking request
})
G. 採樣型
這是最近重構時聯想到的,一種和上面都不同的去重操作,可以應用在自動載入(timeline)行為控制上:
autoload.listen(feeds, "next", sample(3, function() {
this.enable()
}))
如果 sample 是固化的選擇函數(n 選 1),它這實際上會這樣工作:
O-O-X-O-O-X
但「自動載入」的應用可能想要的是(兩次自動,一次手動):
X-X-O-X-X-O
對於這種情況,可以定義作為配置的選擇函數來實現控制:
options { sample: (n) =&> n % 3 !== 0 }
即每個下一次載入完成之後, 每三次有兩次對下一次載入實行自動載入。
如果是post請求,點一次和點兩次是有區別的,比如點「回複評論」,點兩次就相當於回復了兩個內容一樣的評論,所以一定要在UI上限制用戶。
最簡單的方法是點擊後按鈕置為disable狀態,同時顯示waiting動畫,收到響應後再恢復正常按鈕。一定要設置合理的timeout,不然用戶會等得很焦慮。
如果post請求成功了但是因為timeout用戶又提交了一次,要防止重複提交就只能在伺服器端處理。最簡單的做法是客戶端對同一個form提交時產生一個相同的nonce參數,伺服器收到相同的nonce參數時,僅處理第一個請求,這樣就保持了冪等性。
如果是get請求,雖然和post比要求沒那麼高,用戶點兩次相當於發送了兩個請求,但是先發送的請求不一定先收到響應,所以處理收到的響應可以以伺服器響應內容里的時間戳為準,時間戳比較舊的響應直接忽略。
const request$ = Observable.ajax("http://xxx.xxx.xxx/api/:id")
const $btn = document.querySelector(".btn")
Observable.fromEvent($btn, "click")
.do(() =&> $btn.classList.add("disable"))
.exhaustMap(() =&> request$)
.do(() =&> $btn.classList.remove("disable"))
.subscribe((response) =&> {
// response here
})
隨便點擊,上一個請求回來前後面的點擊全部都被忽略
只需要用樣式提示一下 button 是禁用狀態就醒了,js 層面不用加任何鎖啊 flag 之類的東西,也不需要 abort 什麼。
我的經驗的做法是,封裝自己的Ajax請求對象.不使用直接使用系統的或者其他第三方的包裝.
有了自己的封裝之後,並不是簡單abort就是最好的.
我這裡會根據實際的情況做處理:
1. 所有的Ajax請求都是非同步的,我的封裝中做一個100毫秒的setTimeout延時.這樣就可以有效的解決用戶快速重複點擊的問題. 足夠快的時候,前一請求並沒有真正的發出來.就被clearTimeout清除掉了. abort掉Ajax的請求也會有服務端響應,需要消耗資源.
2.如果用戶的操作不並足夠快.比如點了保存按鈕,在伺服器沒有返回成功的時候,再次點了保存按鈕.(事實上兩次請求是一樣的.而我們更希望第一次的請求是有效的,第二次的可以不做處理).
在封裝中,我們檢查傳入ajax的請url及參數是否一致,如果一致.則第二次的AJAX並不發出.
3.最後一種情況,兩次相間的請求是不同的請求,比如樓上說快速切換Tab的例子.無法,只能abort掉前一個Ajax請求了.
再補充一點,統一的Ajax封裝有許多特別的好處:
1.可以計數pending中的請求數,顯示當前還有幾個請求進行中(我們原來的系統中有一個這樣的需求,一般系統也會有比如顯示loading的需求...)
2.可以有統一的異常處理,發送的請求失敗了(伺服器可以返回統一的格式,客戶端做集中處理,彈出錯框等,或者如firebug那樣,把所有的異常放到一個統一的地方去.或者調試階段增加處理.)
3.如果有大量的小請求,比如取某個表單的下拉列表.一堆下拉框的話.可以在客戶端做請求合併.batching Ajax, 這個是從DWR中獲得的靈感. 通常需要防止重複Ajax請求的情形:
表單提交
這種情形在表單submit按鈕點擊後,一般採取disable按鈕,防止重複點擊,也就防止了重複請求。不過你需要處理各種出錯情形,然後enable按鈕,以便用戶修改數據再次提交。
資源請求
比如有四個Tab標籤,點擊每個標籤都將從服務端獲取數據。如果用戶快速點擊標籤,那麼將不可避免的產生重複。可以像@馬驍 所說的,設定flag。如果上一個請求尚未處理完畢,就不發送新的請求。但這麼處理一是不夠通用,二是用戶體驗也不好,因為用戶希望點擊Tab就立刻得到響應。因此,通常做法是abort掉上一個請求。
disabled掉按鈕是常態吧?難道交互不該如此么?隨便比較麻煩。最好的交互是點一下,按鈕disabled,然後顯示正在提交。提交失敗後再恢復,成功後直接提交掉。
如果用jquery的話,jquery的ajax提供ajaxPrefilter方法對ajax請求進行攔截,可以在公用的js里配置上1個公用的ajaxPrefilter。全局維護1個request的列表,在ajaxPrefilter中判斷要發送的請求是否已經存在,如不存在則發送並將發送中的請求存儲到本地,同時覆蓋掉傳進來的options下的complete屬性,在裡面包裹上請求完成後踢出隊列的處理。
其它框架的話用類似的方式處理,基本思想就是對原始提供的全局ajax方法進行簡單包裝下即可。
// 所有ajax請求的通用前置filter
$.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
// 不重複發送相同請求
var key = generatePendingRequestKey(options);
if (!pendingRequests[key]) {
storePendingRequest(key, jqXHR);
} else {
// or do other
jqXHR.abort();
}
var complete = options.complete;
options.complete = function(jqXHR, textStatus) {
// clear from pending requests
pendingRequests[jqXHR.pendingRequestKey] = null;
if ($.isFunction(complete)) {
complete.apply(this, arguments);
}
};
});
以上是sample code。
幾個地方都可以定製:
- 如何算同1個請求
- 遇到發送相同請求的具體處理,可以選擇abort之前的,也可以直接abort掉後發的,完全取決於業務需求
自己加一個 flag 變數判斷一下就好了,還有一種方法,可以利用一些類庫提供的 "listenOnce" 方法綁定事件,這樣回調函數只會被執行一次,比如 : goog.events.listenOnce();
我常用的方式
1. setTimeout + clearTimeout
連續的點擊會把上一次點擊清除掉,也就是ajax請求會在最後一次點擊後發出去
2. disable 按鈕
3. 每次點擊都發請求,往回調里加閉包帶唯一遞增的id,每一次請求回調以後看這個id是不是主函數體里最後一次點擊的id,是則調用回調,不是則返回,也就是ajax回數後看這個回調是不是最後一次點擊的回調,不是就不操作頁面了
第1,2種請求是做用戶瘋狂快速點擊時防止發大量請求的
第3種是用戶不那麼頻繁的點擊,但是每一次點擊ajax請求回數時間比較長,也就是第二次請求可能比第一次先回數這種特殊情況的
$q執行時候可以傳一個cancel方法進去,一旦這個方法返回值變為true的時候就會取消這個請求
不過這個取消僅限客戶端,伺服器端還是會被茫茫多的請求轟炸
所以就必須限制流量
- 客戶端有一個全局檢查(或者是某種方式的檢查),在一個請求出現後延遲0.2秒(時間可以改動)再發往伺服器端。如果在此期間發生新的請求,直接取消前一個請求並重置0.2秒的等待。
這招對於監測滑鼠滾輪事件有奇效 - 伺服器端如果你有心的話可以計算一個request token,然後執行前先去log里檢查這個request是不是已經被執行了。不過這招遠沒有前邊那條好implement就是了。
這是我在zepto下的實現。
供你參考。
參考自 @陳擘翔 謝思路。
$.ajaxSettings.beforeSend = function(xhr,options){
//debugger;
//當ajax快速提交時,url /platform/investor/info?CUSTOMER_ID=9A28E64C004655D9investorId=32_=1448593991155
// 最後一位 是 ajax自動加的時間戳。
// 需要 正則表達式 判斷時間戳 然後 去掉_=時間戳 這樣的形式。
var key = options.url,
complete = options.complete,//暫存options的complete方法指針
flag = options.abortOnRetry !== undefined ? !!options.abortOnRetry : true;
//abortOnretry 屬性 判定是否需要攔截重複ajax。
if(/.+_=[d]{13}$/.test(key)){
key = key.split("_=")[0];
}
if( !flag ){
console.log("放行");
return true;
}
else {
if( pendingRequests[key] !== undefined) {
console.log("阻斷");
return false;
//xhr.abort();
}
else{
pendingRequests[key] = xhr;
// 放棄後觸發的重複提交
//pendingRequests[key].abort(); // 放棄先觸發的提交
}
var timer = setTimeout(function(){
timer = null;
console.dir(pendingRequests);
pendingRequests = {};
},5000);
//這個計時器,用來清空序列,5s後可以發送請求。
options.complete = function(xhr, Status) {//complete方法的默認形參
//重寫complete方法
console.log("complete");
pendingRequests[key] = null;
if ($.isFunction(complete)) {
complete.apply(this, arguments);//在options下執行。
}
};
console.log("放行");
return true;
}
首先 防止重複提交不僅僅是前端要做處理 更需要後端做相應處理 這樣才能絕對保證數據不重複錄入資料庫。
前端如何防止表單重複提交,看了很多解決方法
要從根本上解決這個問題 好的方式肯定不是每個Button 都寫一個disable 根據狀態來判斷,這樣維護起來太麻煩,改動起來太啰嗦。解決方式肯定是從http攔截出入手。
使用session 是一個很好的解決方案,可以設定時間自動過期, 每次請求時查詢session 看是否有當前url,如果有即是沒有失效,則中斷請求。
但是瀏覽器的session 不能像服務端一樣 設定過期時間。於是我們做這個可以參考這種方案設計一個類似的方法, 首先可以保存請求的url 然後可以設定過期時間。
一般來說防止重複提交僅限於POST 請求。 也可以加入DELETE 或者 PUT
具體實現如:
// 設置緩存時間 和緩存請求數組
var requestUrl = [], saveTime = 1000;
http 攔截器中處理:
const Interceptor = function (obj,callback) {
if (obj.method === "POST") {
// 篩選在緩存時間內未過期請求 重新賦值緩存請求數組 新數組與當前請求url 匹配
// 如果有相等項 則判斷為重複提交的請求 直接return
let nowTime = new Date().getTime();
requestUrl = requestUrl.filter((item) =&> {
return (item.setTime + saveTime) &> nowTime;
});
let sessionUrl = requestUrl.filter((item) =&> {
return item.url === obj.url;
});
if (sessionUrl.length &> 0) {
// console.log(obj.url + "請求重複 中斷請求!");
return;
}
let item = { url: obj.url, setTime: new Date().getTime() };
requestUrl.push(item);
}
callback(obj);
};
原理很簡單 :requestUrl這個數組 相當於一個緩存器, 裡面存儲了 每次請求的url 的觸發時間。 每一個請求過濾掉過期的url ,將未過期的請求與當前請求判斷, 這個方法很簡單 幾乎可以覆蓋大部分場景了
以上
如果是jquery的話,可以試試 這個參數 async:false,
補充下,這是對於比較簡單ajax請求,設置後是同步的意思。
對於複雜的ajax請求可以通過如下:
var isSent = false;
var timeout;
jQuery("#buttonID").click(function() {
if(!isSent) {
isSent = true;
jQuery.ajax({
url: "http://test.com",
....,
complete: function() {
isSent = false; //ajax失敗和成功是都重新設置isSend
},
});
if(isSent) isSent.abort(); // abort request
}, 30000 );//after 30 seconds
}
});
根據xhr的readyState的值是否為4來判斷後台是否處理了發出的請求,如果還未處理,則可以使用abort()方法來cancel掉請求。
var request;
if(request request.readyState != 4 ) {
self.request.abort();
}
request = $.ajax({
***
})
1. 禁用按鈕
2.使用one,只執行一次
3.設定flag
&
用一個變數鎖定啊,不明白排名第一所說的幾個問題。用戶一點擊,flag變成false,再點也不發送請求,ajax成功以後flag設為true
我認為正確的做法就是發起一起請求後 disabled 按鈕,可以在按鈕上提示用戶正在提交,或者加一個遮罩層提示用戶,這樣就可以避免重複請求。
最早我看到這個問題的時候,我也在苦惱這個問題,我也沒有系統的解決辦法。
所以解決辦法是,每次都在提交的btn 上加一個狀態,然後檢查這個狀態。
====================
後來隨著我的經驗增加,也看了一些優秀的源碼。
現在我用的方式是,用一個全局的管理「操作」的管理器來管理這些操作。
簡單實現:
var ActionManager = {};
var actionName = url //提交的地址
if(ActionManager[actionName])
return ;
ActionManager[actionName] = function(){};//某個操作的具體執行函數
ActionManager[actionName].call(this);
以上是超簡略實現。
豐富起來,可以給 ActionManager 增加方法如
addAction
excuteAction
並對這些方法增加參數,表示是否需要阻塞;增加回調如執行前回調、執行後回調、如果遇到阻塞時的回調。
這樣可以實現對界面很多的操作或者功能阻塞檢查,防止重複操作。
不僅僅是ajax的重複提交。
以上回答大致分為2種,
第一種是判斷一個值,這個值與之前的重複,就不請求。
第二種是判斷一個值,這個值與之前的重複,就丟棄請求(之前或之後的)。
關鍵點:
第一種方式理解簡單使用的人多,但是比較麻煩,多寫一點代碼。採用全局變數也好(現在你還在污染全局啊),閉包也好,請求出去了你要修改狀態,所以會多寫一個ajax的complete改變狀態為可用,(不寫這個,你的狀態就有可能回不去了,只這success里寫是錯誤的,萬一沒有success,用戶這個按鈕就不能用了)。
第二種用的人很少,(因為jquery api不熟嘛)ajaxPrefilter,請求處理之前的預處理器,實際原理和第一種一樣,放個請求數組,判斷是否有這個請求了,有就不處理了,所謂的丟棄,就是不處理。原理一樣,只是這回你不用這每一個ajax里寫補一個狀態變化。(少點代碼)
區別:一個是分開給每個請求寫一個小開關,一個是寫一個總開關處理到底要不要發這個請求。
哪種好你自己可以分辨了
至於最高票答案 長天之雲,回答的是怎麼寫這個開關,比如判斷請求間隔時間,判斷請求次數,判斷。。。然後決定發送還是不發送這個請求。
(謝謝他的答案讓我腦洞大開,最後一想,其實就是對一些可用的要素進行判斷,時間、次數、對象、動作等等,進行判斷,還可以交叉組合)不僅僅是ABCDEF這幾個,僅判斷時間這一項都有很多種玩法,最後只是根據你的需求來做就好。)
至於這個開關是寫總開關還是 給每一個請求寫小開關,根據你需求嘛,比方搜索框自動完成你寫個時間間隔開關,搜索提交 和其它按鈕提交你寫個總開關控制重複提交。用一個數組做容器,每點擊一次就push一個到數組中。在回調函數中去檢查數組,如果這個回調是success的,那麼就清空數組,否則pop一個xhr出來進行Ajax重發。
推薦閱讀:
TAG:前端開發 | JavaScript | Ajax |