最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前端防止表單重復(fù)提交的多種解決方案

 更新時(shí)間:2025年07月10日 09:17:23   作者:二川bro  
表單重復(fù)提交是Web應(yīng)用中常見的問題,會導(dǎo)致數(shù)據(jù)重復(fù)、業(yè)務(wù)邏輯錯(cuò)誤和糟糕的用戶體驗(yàn),本文將詳細(xì)介紹多種防止表單重復(fù)提交的技術(shù)方案,從基礎(chǔ)實(shí)現(xiàn)到高級防御策略,需要的朋友可以參考下

1. 基礎(chǔ)防御方案

1.1 按鈕禁用方案

實(shí)現(xiàn)原理:表單提交時(shí)立即禁用提交按鈕,防止多次點(diǎn)擊

// Vanilla JavaScript實(shí)現(xiàn)
const form = document.getElementById('myForm');
const submitBtn = document.getElementById('submitBtn');

form.addEventListener('submit', function(e) {
  // 禁用按鈕
  submitBtn.disabled = true;
  submitBtn.textContent = '提交中...';
  
  // 可選的防止表單默認(rèn)提交行為
  // e.preventDefault();
  // 在這里執(zhí)行異步提交邏輯
});

// 如果提交失敗需要恢復(fù)按鈕狀態(tài)
function enableSubmitButton() {
  submitBtn.disabled = false;
  submitBtn.textContent = '提交';
}

React組件示例

function MyForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      await submitFormData();
      // 提交成功后的處理
    } catch (error) {
      // 提交失敗恢復(fù)按鈕狀態(tài)
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* 表單字段 */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

1.2 加載狀態(tài)指示

/* 加載狀態(tài)樣式 */
.submit-btn.loading {
  position: relative;
  pointer-events: none;
}

.submit-btn.loading::after {
  content: '';
  position: absolute;
  right: 10px;
  top: 50%;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  border: 2px solid rgba(255,255,255,0.3);
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

2. 高級防御策略

2.1 請求標(biāo)記(Token)方案

實(shí)現(xiàn)原理:服務(wù)端生成唯一令牌,表單提交時(shí)驗(yàn)證并消耗令牌

// 服務(wù)端生成令牌并注入頁面
const csrfToken = generateToken(); // 存儲在session中
res.render('form-page', { csrfToken });

// 前端表單中包含令牌
<form>
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <!-- 其他字段 -->
</form>

// 服務(wù)端驗(yàn)證
app.post('/submit', (req, res) => {
  const { _csrf } = req.body;
  if (!validateToken(_csrf)) {
    return res.status(400).json({ error: '無效的請求令牌' });
  }
  // 處理有效請求
  invalidateToken(_csrf); // 使令牌失效
});

2.2 請求去重方案

實(shí)現(xiàn)原理:記錄正在處理的請求,阻止重復(fù)請求

const pendingRequests = new Set();

async function submitForm(data) {
  // 生成請求唯一標(biāo)識
  const requestKey = JSON.stringify({
    url: '/api/submit',
    data: sanitizeData(data) // 去除可變字段如時(shí)間戳
  });
  
  if (pendingRequests.has(requestKey)) {
    throw new Error('請勿重復(fù)提交');
  }
  
  pendingRequests.add(requestKey);
  try {
    const result = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data)
    });
    return await result.json();
  } finally {
    pendingRequests.delete(requestKey);
  }
}

2.3 時(shí)間戳限制

let lastSubmitTime = 0;
const MIN_INTERVAL = 5000; // 5秒內(nèi)不允許重復(fù)提交

function handleSubmit() {
  const now = Date.now();
  if (now - lastSubmitTime < MIN_INTERVAL) {
    alert('操作過于頻繁,請稍后再試');
    return;
  }
  
  lastSubmitTime = now;
  // 繼續(xù)提交邏輯
}

3. 框架特定實(shí)現(xiàn)

3.1 Vue實(shí)現(xiàn)

<template>
  <form @submit.prevent="handleSubmit">
    <!-- 表單內(nèi)容 -->
    <button 
      type="submit" 
      :disabled="isSubmitting"
      :class="{ 'loading': isSubmitting }"
    >
      <span v-if="!isSubmitting">提交</span>
      <span v-else>處理中...</span>
    </button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      isSubmitting: false,
      lastSubmitTime: 0
    }
  },
  methods: {
    async handleSubmit() {
      // 5秒內(nèi)不允許重復(fù)提交
      if (Date.now() - this.lastSubmitTime < 5000) {
        this.$toast.warning('請勿頻繁提交');
        return;
      }
      
      this.isSubmitting = true;
      this.lastSubmitTime = Date.now();
      
      try {
        await this.$http.post('/api/submit', this.formData);
        this.$toast.success('提交成功');
      } catch (error) {
        this.isSubmitting = false;
        this.$toast.error('提交失敗');
      } finally {
        this.isSubmitting = false;
      }
    }
  }
}
</script>

3.2 Angular實(shí)現(xiàn)

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-my-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()" #formRef="ngForm">
      <!-- 表單字段 -->
      <button 
        type="submit" 
        [disabled]="form.invalid || isSubmitting"
      >
        <span *ngIf="!isSubmitting">提交</span>
        <span *ngIf="isSubmitting">處理中...</span>
      </button>
    </form>
  `
})
export class MyFormComponent {
  form = this.fb.group({
    // 表單控件定義
  });
  
  isSubmitting = false;
  private submitLock = false;

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    if (this.submitLock) return;
    
    this.submitLock = true;
    this.isSubmitting = true;
    
    this.service.submit(this.form.value).subscribe({
      next: () => {
        // 成功處理
      },
      error: () => {
        this.submitLock = false;
        this.isSubmitting = false;
      },
      complete: () => {
        this.isSubmitting = false;
      }
    });
  }
}

4. 網(wǎng)絡(luò)請求層防御

4.1 Axios攔截器實(shí)現(xiàn)

// 請求攔截器
axios.interceptors.request.use(config => {
  if (config.method?.toUpperCase() === 'POST') {
    const requestKey = `${config.url}-${JSON.stringify(config.data)}`;
    if (window.activeRequests?.has(requestKey)) {
      throw new axios.Cancel('重復(fù)請求已取消');
    }
    
    window.activeRequests = window.activeRequests || new Set();
    window.activeRequests.add(requestKey);
    config.cancelToken = new axios.CancelToken(cancel => {
      config.cancel = cancel;
    });
  }
  return config;
});

// 響應(yīng)攔截器
axios.interceptors.response.use(
  response => {
    if (response.config.method?.toUpperCase() === 'POST') {
      const requestKey = `${response.config.url}-${JSON.stringify(response.config.data)}`;
      window.activeRequests?.delete(requestKey);
    }
    return response;
  },
  error => {
    if (error.config?.method?.toUpperCase() === 'POST') {
      const requestKey = `${error.config.url}-${JSON.stringify(error.config.data)}`;
      window.activeRequests?.delete(requestKey);
    }
    return Promise.reject(error);
  }
);

4.2 Fetch API封裝

const activeRequests = new Map();

async function safeFetch(url, options = {}) {
  // 生成請求指紋
  const requestKey = `${url}-${JSON.stringify(options)}`;
  
  if (activeRequests.has(requestKey)) {
    throw new Error('重復(fù)請求已被阻止');
  }
  
  activeRequests.set(requestKey, true);
  try {
    const response = await fetch(url, options);
    if (!response.ok) throw new Error('請求失敗');
    return await response.json();
  } finally {
    activeRequests.delete(requestKey);
  }
}

5. 用戶體驗(yàn)優(yōu)化

5.1 視覺反饋增強(qiáng)

/* 按鈕狀態(tài)變化動畫 */
.submit-btn {
  transition: 
    background-color 0.3s ease,
    opacity 0.3s ease;
}

.submit-btn:disabled {
  opacity: 0.7;
  background-color: #ccc;
  cursor: not-allowed;
}

/* 加載指示器 */
.loading-indicator {
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255,255,255,0.3);
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s ease-in-out infinite;
  margin-left: 8px;
  vertical-align: middle;
}

5.2 提交后頁面跳轉(zhuǎn)

// 提交成功后重定向到結(jié)果頁
async function handleSubmit() {
  try {
    await submitForm();
    // 替換當(dāng)前歷史記錄,防止后退重復(fù)提交
    window.location.replace('/success-page');
  } catch (error) {
    // 錯(cuò)誤處理
  }
}

// 或使用框架路由
// React Router示例
const navigate = useNavigate();
const handleSubmit = async () => {
  await submitForm();
  navigate('/success', { replace: true }); // 替換當(dāng)前歷史記錄
};

6. 服務(wù)端協(xié)作方案

6.1 冪等性設(shè)計(jì)

實(shí)現(xiàn)原理:服務(wù)端通過唯一ID確保相同請求只處理一次

// 客戶端生成唯一請求ID
const requestId = generateUUID(); // 如: '7b1f29b3-7a7d-4d9e-bd5e-5d5d5d5d5d5d'

// 包含在請求中
await fetch('/api/order', {
  method: 'POST',
  headers: {
    'X-Request-ID': requestId
  },
  body: JSON.stringify(orderData)
});

// 服務(wù)端處理
app.post('/api/order', async (req, res) => {
  const requestId = req.headers['x-request-id'];
  if (await isRequestProcessed(requestId)) {
    return res.status(200).json(await getCachedResponse(requestId));
  }
  
  // 處理新請求
  const result = await processOrder(req.body);
  await cacheRequest(requestId, result);
  res.json(result);
});

6.2 樂觀鎖定策略

適用于:數(shù)據(jù)更新場景,防止并發(fā)修改

// 客戶端獲取數(shù)據(jù)時(shí)同時(shí)獲取版本號
const { data, version } = await fetch('/api/resource/123');

// 提交更新時(shí)帶上版本號
await fetch('/api/resource/123', {
  method: 'PUT',
  body: JSON.stringify({
    ...updatedData,
    version // 當(dāng)前版本號
  })
});

// 服務(wù)端驗(yàn)證
app.put('/api/resource/:id', (req, res) => {
  const currentVersion = getCurrentVersion(req.params.id);
  if (currentVersion !== req.body.version) {
    return res.status(409).json({ 
      error: '數(shù)據(jù)已被修改,請刷新后重試' 
    });
  }
  // 處理更新...
});

7. 綜合解決方案

7.1 防御層級架構(gòu)

7.2 完整實(shí)現(xiàn)示例

class FormSubmitHandler {
  constructor(formId, options = {}) {
    this.form = document.getElementById(formId);
    this.submitBtn = this.form.querySelector('[type="submit"]');
    this.minInterval = options.minInterval || 5000;
    this.lastSubmitTime = 0;
    this.pendingRequest = null;
    
    this.init();
  }
  
  init() {
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
  }
  
  async handleSubmit(e) {
    e.preventDefault();
    
    // 1. 檢查時(shí)間間隔
    const now = Date.now();
    if (now - this.lastSubmitTime < this.minInterval) {
      this.showError('操作過于頻繁,請稍后再試');
      return;
    }
    
    // 2. 禁用按鈕
    this.disableForm();
    
    try {
      // 3. 生成請求唯一標(biāo)識
      const requestKey = this.generateRequestKey();
      if (window.activeFormRequests?.has(requestKey)) {
        throw new Error('重復(fù)請求已取消');
      }
      
      window.activeFormRequests = window.activeFormRequests || new Set();
      window.activeFormRequests.add(requestKey);
      
      // 4. 執(zhí)行提交
      this.lastSubmitTime = now;
      this.pendingRequest = this.submitFormData();
      await this.pendingRequest;
      
      // 5. 成功處理
      this.showSuccess();
      this.redirectAfterSuccess();
      
    } catch (error) {
      if (error.message !== '重復(fù)請求已取消') {
        this.showError(error.message || '提交失敗');
      }
    } finally {
      // 6. 恢復(fù)表單
      this.enableForm();
      this.pendingRequest = null;
    }
  }
  
  disableForm() {
    this.submitBtn.disabled = true;
    this.submitBtn.classList.add('loading');
    this.form.querySelectorAll('input, select, textarea').forEach(el => {
      el.disabled = true;
    });
  }
  
  enableForm() {
    this.submitBtn.disabled = false;
    this.submitBtn.classList.remove('loading');
    this.form.querySelectorAll('input, select, textarea').forEach(el => {
      el.disabled = false;
    });
  }
  
  async submitFormData() {
    const formData = new FormData(this.form);
    const response = await fetch(this.form.action, {
      method: this.form.method,
      body: formData
    });
    
    if (!response.ok) {
      throw new Error(await response.text());
    }
    
    return response.json();
  }
  
  generateRequestKey() {
    return `${this.form.action}-${this.form.method}-${Array.from(new FormData(this.form)).toString()}`;
  }
  
  showError(message) {
    // 實(shí)現(xiàn)錯(cuò)誤提示顯示邏輯
  }
  
  showSuccess() {
    // 實(shí)現(xiàn)成功提示顯示邏輯
  }
  
  redirectAfterSuccess() {
    // 提交成功后跳轉(zhuǎn)或重置表單
  }
}

// 使用示例
new FormSubmitHandler('myForm', { minInterval: 3000 });

8. 測試策略

8.1 單元測試要點(diǎn)

describe('表單重復(fù)提交防御', () => {
  let form, handler;
  
  beforeEach(() => {
    document.body.innerHTML = `
      <form id="testForm">
        <input name="username">
        <button type="submit">提交</button>
      </form>
    `;
    form = document.getElementById('testForm');
    form.addEventListener('submit', e => e.preventDefault());
  });
  
  it('提交時(shí)應(yīng)禁用按鈕', async () => {
    const handler = new FormSubmitHandler('testForm');
    const submitBtn = form.querySelector('button');
    
    form.dispatchEvent(new Event('submit'));
    expect(submitBtn.disabled).toBe(true);
  });
  
  it('5秒內(nèi)應(yīng)阻止重復(fù)提交', async () => {
    const handler = new FormSubmitHandler('testForm', { minInterval: 5000 });
    const mockSubmit = jest.spyOn(handler, 'submitFormData');
    
    // 第一次提交
    form.dispatchEvent(new Event('submit'));
    // 立即嘗試第二次提交
    form.dispatchEvent(new Event('submit'));
    
    expect(mockSubmit).toHaveBeenCalledTimes(1);
  });
  
  it('請求完成應(yīng)恢復(fù)按鈕狀態(tài)', async () => {
    const handler = new FormSubmitHandler('testForm');
    const submitBtn = form.querySelector('button');
    
    // 模擬快速提交
    form.dispatchEvent(new Event('submit'));
    await Promise.resolve(); // 模擬異步完成
    
    expect(submitBtn.disabled).toBe(false);
  });
});

8.2 E2E測試示例

describe('表單提交', () => {
  it('應(yīng)防止重復(fù)提交', () => {
    cy.intercept('POST', '/api/submit', {
      delay: 1000,
      body: { success: true }
    }).as('submitRequest');
    
    cy.visit('/form-page');
    cy.get('form').submit();
    cy.get('button[type="submit"]').should('be.disabled');
    
    // 嘗試重復(fù)提交
    cy.get('form').submit();
    cy.get('@submitRequest.all').should('have.length', 1);
    
    // 等待請求完成
    cy.wait('@submitRequest');
    cy.get('button[type="submit"]').should('not.be.disabled');
  });
});

9. 最佳實(shí)踐總結(jié)

多層防御

  • 前端按鈕禁用
  • 請求攔截去重
  • 服務(wù)端冪等處理

用戶體驗(yàn)

  • 清晰的加載狀態(tài)
  • 友好的錯(cuò)誤提示
  • 防止導(dǎo)航后退重復(fù)提交

技術(shù)實(shí)現(xiàn)

  • 合理設(shè)置防抖時(shí)間
  • 唯一請求標(biāo)識
  • 適當(dāng)?shù)逆i定策略

異常處理

  • 網(wǎng)絡(luò)錯(cuò)誤恢復(fù)
  • 服務(wù)端錯(cuò)誤重試
  • 超時(shí)處理機(jī)制

測試覆蓋

  • 重復(fù)點(diǎn)擊場景
  • 網(wǎng)絡(luò)延遲場景
  • 提交失敗恢復(fù)

通過實(shí)施這些策略,您可以有效防止表單重復(fù)提交問題,同時(shí)提供流暢的用戶體驗(yàn)。根據(jù)應(yīng)用的具體需求,可以選擇適合的防御層級組合。

以上就是前端防止表單重復(fù)提交的多種解決方案的詳細(xì)內(nèi)容,更多關(guān)于前端防止表單重復(fù)提交的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • javascript使用call調(diào)用微信API

    javascript使用call調(diào)用微信API

    這篇文章主要介紹了javascript使用call調(diào)用微信API,需要的朋友可以參考下
    2014-12-12
  • lightBox 簡易的全屏透明遮罩解決方法

    lightBox 簡易的全屏透明遮罩解決方法

    現(xiàn)在全屏的半透明遮罩層在web2.0網(wǎng)站應(yīng)用非常廣泛了,絕大多數(shù)遮罩是通過計(jì)算頁面大小,然后覆蓋一個(gè)與頁面同等大小的層實(shí)現(xiàn),如騰訊qzone, wordpress后臺。
    2010-06-06
  • JS+CSS實(shí)現(xiàn)精美的二級導(dǎo)航效果代碼

    JS+CSS實(shí)現(xiàn)精美的二級導(dǎo)航效果代碼

    這篇文章主要介紹了JS+CSS實(shí)現(xiàn)精美的二級導(dǎo)航效果代碼,涉及JavaScript基于鼠標(biāo)事件動態(tài)變換元素樣式的技巧,非常簡單實(shí)用,需要的朋友可以參考下
    2015-09-09
  • 小程序從手動埋點(diǎn)到自動埋點(diǎn)的實(shí)現(xiàn)方法

    小程序從手動埋點(diǎn)到自動埋點(diǎn)的實(shí)現(xiàn)方法

    這篇文章主要介紹了小程序從手動埋點(diǎn)到自動埋點(diǎn)的實(shí)現(xiàn)方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-01-01
  • Ionic2調(diào)用本地SQlite實(shí)例

    Ionic2調(diào)用本地SQlite實(shí)例

    這篇文章主要介紹了Ionic2調(diào)用本地SQlite實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-04-04
  • 小程序wx.getUserProfile接口的具體使用

    小程序wx.getUserProfile接口的具體使用

    最近微信小程序?qū)τ趯徍诵〕绦蛱岢隽藥в衱x.login、wx.getUserInfo接口的調(diào)整,并提出了一個(gè)新的接口供開發(fā)者調(diào)用,本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下
    2021-06-06
  • JS判斷鼠標(biāo)進(jìn)入容器的方向與window.open新窗口被攔截的問題

    JS判斷鼠標(biāo)進(jìn)入容器的方向與window.open新窗口被攔截的問題

    這篇文章主要給大家介紹了利用Javascript判斷鼠標(biāo)進(jìn)入容器方向的方法,以及window.open新窗口被攔截的問題分析,文中給出了詳細(xì)圖文介紹和示例代碼,相信對大家的理解和學(xué)習(xí)具有一定的參考借鑒價(jià)值,下面來一起看看吧。
    2016-12-12
  • 在TypeScript中將unknown類型轉(zhuǎn)換為更具體的類型

    在TypeScript中將unknown類型轉(zhuǎn)換為更具體的類型

    本文介紹了TypeScript中unknown類型的使用,以及如何將unknown類型安全地轉(zhuǎn)換為更具體的類型,本文詳細(xì)介紹了類型斷言、內(nèi)置類型守衛(wèi)和自定義類型守衛(wèi)的使用方法,并提供了最佳實(shí)踐建議,以確保代碼的類型安全性和可維護(hù)性,需要的朋友可以參考下
    2026-04-04
  • 微信小程序?qū)崿F(xiàn)動態(tài)驗(yàn)證碼

    微信小程序?qū)崿F(xiàn)動態(tài)驗(yàn)證碼

    這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)動態(tài)驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-05-05
  • 使用Taro實(shí)現(xiàn)小程序商城的購物車功能模塊的實(shí)例代碼

    使用Taro實(shí)現(xiàn)小程序商城的購物車功能模塊的實(shí)例代碼

    這篇文章主要介紹了使用Taro實(shí)現(xiàn)的小程序商城的購物車功能模塊,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-06-06

最新評論

留坝县| 海口市| 江永县| 康乐县| 台江县| 图木舒克市| 土默特右旗| 凯里市| 昂仁县| 花垣县| 个旧市| 中宁县| 嘉义县| 亳州市| 汶上县| 政和县| 黄冈市| 冕宁县| 平江县| 明光市| 长子县| 海伦市| 大厂| 瓮安县| 开江县| 东莞市| 德化县| 大城县| 华池县| 剑川县| 仙居县| 杂多县| 叙永县| 汪清县| 林周县| 梅河口市| 海兴县| 钦州市| 金山区| 德江县| 钟祥市|