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

JS生成UUID的10種姿勢及避坑指南(前端小白也能秒上手)

 更新時間:2026年06月23日 10:45:59   作者:master_chenchengg  
在開發(fā)過程中,有時候需要js生成全局唯一標識符,在java中可以使用uuid,但是JS中沒有現(xiàn)成的函數(shù),這篇文章主要介紹了JS生成UUID的10種姿勢及避坑指南的相關資料,需要的朋友可以參考下

前言

說實話啊,這篇文章我原本是不想寫的。真的,因為UUID這玩意兒聽起來就挺"后端味兒"的,感覺應該是那幫穿格子衫的Java老哥在Spring Boot里@GeneratedValue一下搞定的事兒。但架不住現(xiàn)在前后端分離之后,后端那幫兄弟越來越懶了——“哎這個ID你前端生成一下唄,反正你也是要本地預覽的嘛”,“哎呀你離線存儲自己搞個臨時ID嘛,后端只管存”。

我:???

行吧,既然逃不掉,那咱就好好嘮嘮。今天這篇不整那些虛頭八腦的,就是手把手教你從"土法煉鋼"到"正規(guī)軍"怎么搞UUID,順便給你看看那些年在生產(chǎn)環(huán)境踩過的坑,血淋淋的教訓,看完保證你少加三天班。

為啥前端突然要搞這破玩意兒?還不是被后端逼的

我先還原個真實場景,你看熟不熟悉:

需求評審會上,產(chǎn)品經(jīng)理說要做個"離線編輯功能",用戶沒網(wǎng)的時候也能寫東西,有網(wǎng)了自動同步。后端小哥聽完當場表示:“這個簡單,前端你本地先存著,等聯(lián)網(wǎng)了發(fā)給我,我給入庫。”

你小心翼翼地問:“那主鍵ID呢?”

后端翹著二郎腿:“你先生成一個唄,uuid就行,我直接用。”

那一刻你的內(nèi)心是崩潰的。啥?我?生成主鍵?這玩意兒不是數(shù)據(jù)庫該干的活嗎?

但說真的,這場景現(xiàn)在太常見了。除了剛才說的離線數(shù)據(jù)同步,還有:

  • 埋點上報:你要追蹤用戶點了哪個按鈕,得給每個事件一個唯一身份證號吧?
  • 本地緩存Key:localStorage里存了一堆草稿,總得有個標識區(qū)分"草稿1"和"草稿2"吧?
  • 表單自動保存:用戶寫到一半的表單,刷新頁面不能丟,得給個臨時ID對應到這份草稿。
  • WebSocket消息去重:網(wǎng)絡抖動導致消息發(fā)了兩次,怎么知道這倆是重復的?

所以啊,別覺得UUID是后端專屬,現(xiàn)在前端玩得可花了。但問題也來了:JavaScript它不像Java有個java.util.UUID直接UUID.randomUUID()就完事了,JS這生態(tài)…怎么說呢,百花齊放(群魔亂舞)吧。

先整明白UUID到底是個啥,別瞎用

UUID全稱Universally Unique Identifier,通用唯一識別碼。標準格式是8-4-4-4-12的32個十六進制數(shù)字,比如550e8400-e29b-41d4-a716-446655440000。

版本有好幾個,咱們前端常用的是:

  • v4:純隨機生成,最常用,看起來像xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx。
  • v1:基于時間戳+MAC地址,但前端拿不到MAC地址,所以基本是時間戳+隨機數(shù)湊合版。

其實還有v3、v5(基于命名空間和哈希),但前端基本用不上,咱就不展開了。你就記?。呵岸苏fUUID,九成九是指v4那種隨機的。

土法煉鋼第一式:Math.random()真的靠譜嗎?

先說最傻白甜的寫法,估計剛學JS的人都寫過:

function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0;
    var v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

console.log(generateUUID()); // 比如:f47ac10b-58cc-4372-a567-0e02b2c3d479

這代碼看著挺高級是吧?正則替換,位運算,十六進制轉換,乍一看以為是大佬寫的。但我要給你潑盆冷水了:這玩意兒在生產(chǎn)環(huán)境用,分分鐘教你做人。

Math.random()的隨機性其實挺垃的,它是偽隨機,基于種子生成的。在V8引擎里,不同瀏覽器實現(xiàn)還不一樣。最離譜的是某些低端安卓機(說的就是你們,那些運營商定制的千元機),Math.random()的隨機范圍居然有偏置,生成的數(shù)扎堆,結果就是撞ID。

我曾經(jīng)在日志系統(tǒng)里看到,一天之內(nèi)居然有幾百個重復的UUID,查了半天發(fā)現(xiàn)都是某個國產(chǎn)安卓機型干的好事。那場面,堪稱車禍現(xiàn)場。

而且Math.random()還有個致命問題:它不是加密的。如果你拿這個UUID當會話ID或者安全令牌,黑客能給你預測出來,直接社會工程學攻擊走起。

但你說完全不能用嗎?倒也不是。如果只是做個前端臨時緩存key,丟了就丟了那種,用用也無妨。但記住,別用它做數(shù)據(jù)持久化的主鍵,真的會出事的。

土法煉鋼第二式:Date.now()加料版

既然純隨機不靠譜,那咱加點時間戳總行了吧?時間總是唯一的嘛(理論上)。

function makeId() {
  let id = '';
  // 時間戳部分,13位
  const timestamp = Date.now().toString(36); // 轉成36進制,縮短長度
  // 隨機數(shù)部分,補夠長度
  const randomPart = Math.random().toString(36).substring(2, 8);
  
  id = `${timestamp}-${randomPart}-${Math.random().toString(36).substring(2, 8)}`;
  return id;
}

console.log(makeId()); // 類似:lxx1h2z3-abc123-def456

這代碼是我早些年寫的,當時覺得賊聰明——時間戳保證大致順序,隨機數(shù)保證唯一性,36進制還能縮短字符串長度,完美!

結果上線第一天就出事了。用戶狂點提交按鈕,網(wǎng)絡卡的時候點五次,后臺瞬間收到五條數(shù)據(jù),ID居然一模一樣。為啥?因為Date.now()是毫秒級的,用戶手速再快也比不上機器循環(huán)快啊。

后來我學乖了,加了個自增計數(shù)器:

let counter = 0;

function betterId() {
  const timestamp = Date.now().toString(36);
  const random = Math.random().toString(36).substring(2, 5);
  // 加了個原子計數(shù)器,每次調(diào)用都+1
  const count = (counter++).toString(36).padStart(4, '0');
  
  return `${timestamp}-${random}-${count}`;
}

這下倒是不會重復了,但這代碼看著就…挺丑的,而且還是不安全,還是那個問題,可以被預測。只適合臨時用用,正式環(huán)境別這么搞。

土法煉鋼第三式:瀏覽器指紋大雜燴

后來我又想了個騷操作,既然隨機數(shù)不靠譜,那我把能拿到的設備信息都混進去總行了吧?指紋唯一性應該還可以。

function fingerprintUUID() {
  // 收集各種瀏覽器信息
  const screenInfo = `${screen.width}x${screen.height}x${screen.colorDepth}`;
  const userAgent = navigator.userAgent;
  const language = navigator.language;
  const platform = navigator.platform;
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  
  // 混在一起做個簡單的hash(這里簡化處理,實際可以用更復雜的hash算法)
  const seed = `${screenInfo}-${userAgent}-${language}-${platform}-${timezone}-${Date.now()}-${Math.random()}`;
  
  // 簡單hash函數(shù)
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    const char = seed.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // 轉32位整數(shù)
  }
  
  // 轉成十六進制,再格式化成UUID格式
  const hex = Math.abs(hash).toString(16).padStart(8, '0');
  const randomPart = Math.random().toString(16).substring(2, 10);
  
  return `${hex.substring(0, 8)}-${hex.substring(0, 4)}-4${hex.substring(1, 4)}-${randomPart.substring(0, 4)}-${randomPart.substring(4, 12)}${Date.now().toString(16).substring(0, 4)}`;
}

console.log(fingerprintUUID());

這代碼看著唬人,但其實問題更大。首先,fingerprint不是唯一的,同型號手機,一樣的屏幕分辨率,一樣的瀏覽器,生成的指紋就一樣。其次,現(xiàn)在瀏覽器都在搞隱私保護,navigator.userAgent快要變成固定值了(Chrome的User-Agent Reduction計劃),platform也快要藏起來了。

所以這條路基本也行不通,屬于自娛自樂型。

正規(guī)軍來了:uuid npm包到底香不香?

土法煉鋼搞了半天,發(fā)現(xiàn)都有坑,那咋辦?上正規(guī)軍唄。

npm上有個uuid包,周下載量上億,屬于業(yè)界標準了。用法簡單得令人發(fā)指:

// 先裝上:npm install uuid

import { v4 as uuidv4 } from 'uuid';

// 生成一個v4 UUID
const id = uuidv4();
console.log(id); // 比如:9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

這包的好處是啥呢?它考慮了各種環(huán)境:

  • Node.js:用crypto模塊生成真隨機數(shù)
  • 瀏覽器:優(yōu)先用crypto.getRandomValues,降級才用Math.random
  • React Native:也有對應實現(xiàn)

而且它還支持v1、v3、v4、v5,雖然咱們基本只用v4,但它有啊,顯得專業(yè)。

我在生產(chǎn)環(huán)境用了三四年,幾百萬數(shù)據(jù)量,沒出過重復問題。當然,理論上還是有碰撞概率的,但那個概率比你中彩票還低,可以忽略不計。

但有個坑要注意:版本問題。uuid包第9版是大版本,如果你項目里還在用第8版,升級的時候注意看changelog,有些API變了。別問我怎么知道的,問就是曾經(jīng)升級后線上掛了半小時。

還有個性能問題,如果你要一次性生成幾萬個UUID,uuid包可能會有點慢,因為它內(nèi)部有些校驗邏輯。這時候你可以考慮用crypto.randomUUID(),這是瀏覽器原生的,更快。

瀏覽器原生API:crypto.randomUUID()真香預警

這是現(xiàn)代瀏覽器(Chrome 92+、Firefox 95+、Safari 15.4+)帶來的福音,原生支持,不用裝包:

// 直接調(diào)用,簡單粗暴
const id = crypto.randomUUID();
console.log(id); // f47ac10b-58cc-4372-a567-0e02b2c3d479

性能測試下來,比uuid npm包快大概30%-50%,畢竟是原生C++實現(xiàn)的。而且代碼量少,不用引入依賴,bundle體積都小了。

但!是!兼容性是個大問題。你要是還要支持IE11(雖然微軟都放棄它了,但有些國企項目就是繞不開),或者某些老舊的安卓WebView,這API直接報undefined給你看。

所以穩(wěn)妥的寫法是加個polyfill:

function generateUUID() {
  // 優(yōu)先用原生的
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
    return crypto.randomUUID();
  }
  
  // 降級方案,用uuid庫,或者自己實現(xiàn)一個v4
  // 這里為了演示,手寫一個簡陋版,生產(chǎn)環(huán)境建議還是引入uuid包
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    const r = (typeof crypto !== 'undefined' && crypto.getRandomValues) 
      ? crypto.getRandomValues(new Uint8Array(1))[0] % 16 
      : Math.random() * 16 | 0;
    const v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

// 測試一下
console.log(generateUUID());

看到?jīng)]?我還加了個crypto.getRandomValues的降級,這是瀏覽器提供的加密級隨機數(shù)API,比Math.random()安全多了,至少能保證隨機性。

生產(chǎn)環(huán)境翻車實錄:那些我以為的唯一其實并不唯一

好了,前面都是技術方案,現(xiàn)在進入吐槽大會環(huán)節(jié),說說我在生產(chǎn)環(huán)境踩過的坑。

翻車事件一:安卓低端機

2021年,我們做個活動頁,要在前端生成訂單ID,然后傳給后端。我當時圖省事,直接用了Math.random()版UUID。結果活動上線當天,客服炸了,說有很多用戶投訴"我明明只買了一次,怎么扣了三次錢?"

查日志發(fā)現(xiàn),三個不同的用戶,生成了同一個UUID,后端以為是重復提交,直接拒絕了。再細查,全是某品牌千元安卓機,CPU還是聯(lián)發(fā)科三年前的入門款。那機器的Math.random()實現(xiàn)有問題,種子更新頻率慢,短時間內(nèi)生成的隨機數(shù)高度相似。

解決方案:連夜改成crypto.getRandomValues,并且加了個服務端兜底,如果前端沒傳ID或者ID格式不對,后端自己生成一個。

翻車事件二:并發(fā)地獄之for循環(huán)

有個需求是批量導入,前端要一次生成100條數(shù)據(jù)的臨時ID。開發(fā)小哥寫了個for循環(huán):

const list = [];
for (let i = 0; i < 100; i++) {
  list.push({
    id: Math.random().toString(36).substring(2),
    data: xxx
  });
}

本地測試沒問題,測試環(huán)境沒問題,上線后用戶導入1000條數(shù)據(jù)的時候,發(fā)現(xiàn)有30%的ID重復了。因為Math.random()在短時間內(nèi)的種子可能沒變,而且toString(36).substring(2)截取得太短,碰撞概率劇增。

翻車事件三:mock數(shù)據(jù)摧毀生產(chǎn)數(shù)據(jù)庫

這是最慘的一次。測試環(huán)境的mock腳本里,為了數(shù)據(jù)好看,用了固定種子的UUID生成器,比如:

// mock腳本
let seed = 12345;
function mockUUID() {
  seed++;
  return `fake-uuid-${seed}`;
}

結果某天測試同學不小心把測試配置指到了生產(chǎn)環(huán)境(別問為什么測試能連生產(chǎn),問就是歷史遺留問題),然后mock腳本跑了十萬條數(shù)據(jù)進生產(chǎn)庫,全是以fake-uuid-開頭的ID。更慘的是,這些數(shù)據(jù)后來同步到大數(shù)據(jù)平臺,導致一整天的報表數(shù)據(jù)全臟了得回滾。

從那以后,我們定了個規(guī)矩:任何環(huán)境都不能用非標準UUID格式,mock數(shù)據(jù)必須用正規(guī)的uuid庫生成,哪怕只是測試。

實戰(zhàn)代碼大放送:這些場景你肯定用得上

光說不練假把式,給你幾個我項目中真實在用的代碼片段。

場景一:用戶草稿箱本地存儲

用戶寫長文的時候,每30秒自動保存一次草稿,關閉頁面再回來還能恢復。

class DraftManager {
  constructor() {
    this.STORAGE_KEY = 'user_drafts';
    // 每個草稿給一個唯一ID,頁面加載時如果沒ID就新建一個
    this.currentDraftId = sessionStorage.getItem('current_draft_id') || this.createNewDraft();
  }

  createNewDraft() {
    const id = crypto.randomUUID ? crypto.randomUUID() : 
               `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
    sessionStorage.setItem('current_draft_id', id);
    return id;
  }

  saveDraft(content) {
    const drafts = this.getAllDrafts();
    drafts[this.currentDraftId] = {
      content,
      updateTime: Date.now(),
      // 加個子ID,用于版本控制,萬一用戶開了兩個標簽頁呢
      versionId: crypto.randomUUID ? crypto.randomUUID() : Date.now()
    };
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(drafts));
  }

  getAllDrafts() {
    try {
      return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || {};
    } catch {
      return {};
    }
  }

  // 清理7天前的草稿
  cleanOldDrafts() {
    const drafts = this.getAllDrafts();
    const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
    
    Object.keys(drafts).forEach(id => {
      if (drafts[id].updateTime < sevenDaysAgo) {
        delete drafts[id];
      }
    });
    
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(drafts));
  }
}

// 使用
const draftManager = new DraftManager();
document.getElementById('editor').addEventListener('input', (e) => {
  draftManager.saveDraft(e.target.value);
});

看到?jīng)]?這里我用了雙ID策略,外層的currentDraftId標識一篇草稿,內(nèi)層的versionId標識每次保存的版本。這樣即使用戶瘋狂Ctrl+S,我們也能知道哪次保存是最新的。

場景二:PWA離線數(shù)據(jù)同步

這是真正的業(yè)務場景,用戶可能在地鐵里沒網(wǎng)的時候提交表單,有網(wǎng)了再同步。

class OfflineSyncManager {
  constructor() {
    this.DB_NAME = 'offline_data';
    this.STORE_NAME = 'pending_requests';
    this.db = null;
    this.initDB();
  }

  async initDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.DB_NAME, 1);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        // 用UUID作為主鍵
        db.createObjectStore(this.STORE_NAME, { keyPath: 'localId' });
      };
    });
  }

  // 添加離線任務
  async addTask(apiEndpoint, payload) {
    const localId = crypto.randomUUID(); // 生成臨時主鍵
    
    const task = {
      localId,           // 本地唯一標識
      apiEndpoint,
      payload,
      createdAt: Date.now(),
      retryCount: 0,
      status: 'pending'  // pending, failed, success
    };

    const transaction = this.db.transaction([this.STORE_NAME], 'readwrite');
    const store = transaction.objectStore(this.STORE_NAME);
    await store.add(task);
    
    console.log(`任務已離線保存,本地ID:${localId}`);
    return localId;
  }

  // 同步數(shù)據(jù)
  async sync() {
    if (!navigator.onLine) return;

    const transaction = this.db.transaction([this.STORE_NAME], 'readonly');
    const store = transaction.objectStore(this.STORE_NAME);
    const request = store.getAll();

    request.onsuccess = async () => {
      const tasks = request.result.filter(t => t.status === 'pending');
      
      for (const task of tasks) {
        try {
          // 發(fā)送請求時帶上localId,后端返回時會帶上,這樣前端可以對應上
          const response = await fetch(task.apiEndpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              ...task.payload,
              clientId: task.localId  // 告訴后端這是哪條數(shù)據(jù)
            })
          });

          if (response.ok) {
            // 成功后從本地刪除,或者標記為成功
            await this.markAsSuccess(task.localId);
            console.log(`任務 ${task.localId} 同步成功`);
          }
        } catch (error) {
          await this.markAsFailed(task.localId);
          console.error(`任務 ${task.localId} 同步失敗`, error);
        }
      }
    };
  }

  // 監(jiān)聽網(wǎng)絡恢復
  startListening() {
    window.addEventListener('online', () => {
      console.log('網(wǎng)絡恢復,開始同步...');
      this.sync();
    });
  }
}

// 使用示例
const syncManager = new OfflineSyncManager();

// 用戶點擊提交
document.getElementById('submit').addEventListener('click', async () => {
  const data = {
    name: document.getElementById('name').value,
    content: document.getElementById('content').value
  };

  if (navigator.onLine) {
    // 有網(wǎng)直接發(fā)
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  } else {
    // 沒網(wǎng)存本地
    const localId = await syncManager.addTask('/api/submit', data);
    alert('當前無網(wǎng)絡,數(shù)據(jù)已保存,聯(lián)網(wǎng)后自動同步。本地ID:' + localId);
  }
});

這個方案的關鍵在于localId,它在離線階段就是數(shù)據(jù)的唯一身份證。等聯(lián)網(wǎng)后,這個ID會傳給后端,后端入庫時可以把這個作為業(yè)務ID,也可以自己再生成一個數(shù)據(jù)庫自增ID,但會把localId存到單獨的字段做映射。這樣前后端就能對得上號,不會出現(xiàn)"這數(shù)據(jù)我明明發(fā)了,后端說沒收到"的扯皮情況。

場景三:WebSocket消息防重發(fā)

網(wǎng)絡抖動的時候,WebSocket可能以為消息沒發(fā)出去,實際已經(jīng)發(fā)出去了,然后重發(fā)一次,導致后端處理了兩次。

class ReliableWebSocket {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.messageQueue = [];      // 待發(fā)送隊列
    this.pendingMessages = new Map();  // 已發(fā)送但未確認的消息
    this.reconnectAttempts = 0;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket連接成功');
      this.reconnectAttempts = 0;
      this.flushQueue(); // 把之前沒發(fā)出去的發(fā)了
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      // 處理ACK確認
      if (data.type === 'ACK') {
        // 服務端收到消息了,從pending里刪掉
        this.pendingMessages.delete(data.messageId);
        console.log(`消息 ${data.messageId} 已確認送達`);
      } else {
        // 處理普通消息
        this.handleMessage(data);
      }
    };

    this.ws.onclose = () => {
      console.log('連接斷開,準備重連...');
      setTimeout(() => this.reconnect(), 1000 * Math.pow(2, this.reconnectAttempts));
    };
  }

  // 發(fā)送消息,帶唯一ID和重試機制
  send(payload) {
    const messageId = crypto.randomUUID(); // 每條消息一個唯一ID
    const message = {
      id: messageId,
      timestamp: Date.now(),
      payload,
      // 重試次數(shù),防重發(fā)用
      retryCount: 0
    };

    if (this.ws.readyState === WebSocket.OPEN) {
      this.doSend(message);
    } else {
      // 沒連接先存隊列
      this.messageQueue.push(message);
    }

    return messageId; // 返回ID,方便業(yè)務層監(jiān)聽
  }

  doSend(message) {
    this.ws.send(JSON.stringify(message));
    // 記錄到pending,等ACK
    this.pendingMessages.set(message.id, {
      ...message,
      sendTime: Date.now()
    });

    // 3秒沒收到ACK就重試
    setTimeout(() => this.checkAck(message.id), 3000);
  }

  checkAck(messageId) {
    if (this.pendingMessages.has(messageId)) {
      const msg = this.pendingMessages.get(messageId);
      if (msg.retryCount < 3) {
        console.log(`消息 ${messageId} 未確認,第${msg.retryCount + 1}次重試`);
        msg.retryCount++;
        this.doSend(msg);
      } else {
        console.error(`消息 ${messageId} 發(fā)送失敗,放棄重試`);
        this.pendingMessages.delete(messageId);
      }
    }
  }

  flushQueue() {
    while (this.messageQueue.length > 0) {
      const msg = this.messageQueue.shift();
      this.doSend(msg);
    }
  }

  reconnect() {
    this.reconnectAttempts++;
    console.log(`第${this.reconnectAttempts}次重連...`);
    this.connect();
    
    // 把pending的消息標記為需要重發(fā)
    this.pendingMessages.forEach((msg, id) => {
      msg.retryCount = 0; // 重置重試次數(shù)
      this.messageQueue.push(msg);
    });
    this.pendingMessages.clear();
  }
}

// 使用
const ws = new ReliableWebSocket('wss://example.com/ws');
ws.connect();

// 發(fā)送消息
const msgId = ws.send({ text: '你好啊' });
console.log('發(fā)送消息ID:', msgId);

這個方案的核心就是消息ID。服務端收到消息后,先查這個ID有沒有處理過,處理過就直接返回ACK,沒處理過就處理然后存起來。這樣就算客戶端重發(fā)了,服務端也不會重復處理業(yè)務邏輯。

調(diào)試技巧:怎么驗證你的UUID真的唯一?

寫完代碼總得測試吧?但UUID理論上會重復,只是概率極低,怎么驗證你的生成器靠譜呢?

暴力測試法:跑100萬次

function testUniqueness(generator, count = 1000000) {
  const set = new Set();
  const startTime = performance.now();
  let duplicates = 0;
  
  for (let i = 0; i < count; i++) {
    const id = generator();
    if (set.has(id)) {
      duplicates++;
      console.log(`發(fā)現(xiàn)重復!第${i}次生成了已存在的ID: ${id}`);
    } else {
      set.add(id);
    }
    
    // 每10萬次報告一次進度
    if (i % 100000 === 0 && i > 0) {
      console.log(`已生成${i}個UUID,目前無重復...`);
    }
  }
  
  const endTime = performance.now();
  console.log(`測試完成!生成${count}個UUID,發(fā)現(xiàn)${duplicates}個重復`);
  console.log(`耗時:${(endTime - startTime).toFixed(2)}ms`);
  console.log(`平均每個UUID生成時間:${((endTime - startTime) / count).toFixed(4)}ms`);
  
  return duplicates === 0;
}

// 測試Math.random版
testUniqueness(() => {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0;
    var v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}, 100000);

// 測試crypto.randomUUID
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
  testUniqueness(() => crypto.randomUUID(), 100000);
}

跑這個腳本的時候,建議把瀏覽器標簽頁放后臺,因為100萬次循環(huán)會卡界面?;蛘哂肳eb Worker。

實時監(jiān)控法:用WeakMap做內(nèi)存去重

如果你不確定線上環(huán)境有沒有重復,可以埋個點:

class UUIDMonitor {
  constructor() {
    this.uuidSet = new Set();
    this.duplicates = [];
    this.maxSize = 10000; // 只保留最近1萬個,防止內(nèi)存爆炸
  }

  check(uuid) {
    if (this.uuidSet.has(uuid)) {
      this.duplicates.push({
        uuid,
        time: Date.now(),
        stack: new Error().stack // 記錄調(diào)用棧
      });
      
      // 上報到監(jiān)控平臺
      this.report(uuid);
      return false;
    }
    
    // 滿了就清一半,LRU策略簡單版
    if (this.uuidSet.size >= this.maxSize) {
      const iter = this.uuidSet.values();
      for (let i = 0; i < this.maxSize / 2; i++) {
        this.uuidSet.delete(iter.next().value);
      }
    }
    
    this.uuidSet.add(uuid);
    return true;
  }

  report(uuid) {
    // 發(fā)到 sentry 或者自研監(jiān)控平臺
    console.error(`UUID重復警告:${uuid}`, this.duplicates[this.duplicates.length - 1]);
  }
}

// 使用
const monitor = new UUIDMonitor();

function safeGenerateUUID() {
  const id = crypto.randomUUID ? crypto.randomUUID() : 
             'xxx-xxx'.replace(/x/g, () => Math.random().toString(16)[2]);
  
  if (!monitor.check(id)) {
    // 重復了,重新生成(雖然理論上不應該)
    return safeGenerateUUID();
  }
  return id;
}

console.log大法:時間戳定位

如果懷疑某個UUID有問題,可以在生成的時候打詳細的log:

function debugUUID() {
  const id = crypto.randomUUID();
  console.log(
    `%c生成UUID: ${id}`, 
    'color: #1890ff; font-weight: bold;',
    `\n時間: ${new Date().toISOString()}`,
    `\n頁面: ${location.href}`,
    `\n用戶: ${localStorage.getItem('userId') || '未登錄'}`,
    `\n堆棧:`, new Error().stack
  );
  return id;
}

這樣出問題的時候,你可以在控制臺Filter里搜這個UUID,看它是什么時候在哪生成的。

冷門但好用的小技巧

用performance.now()提升時間精度

Date.now()只能到毫秒,但performance.now()可以精確到微秒(雖然也是假的,是高分表時間),適合用來做時間戳部分:

function highResTimestamp() {
  // 基礎時間戳 + 高分表時間,精度更高
  const base = Date.now();
  const highRes = performance.now();
  return `${base}-${Math.floor(highRes * 1000)}`;
}

navigator.userAgent做輕量設備指紋

雖然不建議依賴userAgent,但用來做鹽值(salt)增加隨機性還是可以的:

function getDeviceSalt() {
  const ua = navigator.userAgent;
  let hash = 0;
  for (let i = 0; i < ua.length; i++) {
    const char = ua.charCodeAt(i);
    hash = ((hash << 5) - hash) + char + (i % 7); // 加點料
  }
  return Math.abs(hash).toString(16).substring(0, 4);
}

// 混入UUID生成
function saltyUUID() {
  const base = crypto.randomUUID ? crypto.randomUUID() : 'xxx';
  return base.replace(/^.{4}/, getDeviceSalt()); // 把前4位換成設備指紋
}

Symbol臨時兜底

如果你只是需要在當前運行時唯一的標識,不需要持久化,用Symbol最簡單:

const uniqueKey = Symbol('draft_key');
const cache = {};

// 保證當前進程唯一
cache[uniqueKey] = { data: 'xxx' };

// 但是注意,Symbol不能JSON序列化,不能存localStorage,不能跨iframe傳遞
// 只適合內(nèi)存中的臨時標識

還有個小眾場景:UUID壓縮。標準的UUID是36個字符(帶橫杠),如果存數(shù)據(jù)庫覺得太長,可以轉成Base64或者去掉橫杠:

function compressUUID(uuid) {
  // 去掉橫杠,32位
  return uuid.replace(/-/g, '');
}

function decompressUUID(short) {
  // 還原橫杠
  return `${short.substring(0, 8)}-${short.substring(8, 12)}-${short.substring(12, 16)}-${short.substring(16, 20)}-${short.substring(20)}`;
}

// 更狠的,轉成Base62( alphanumeric ),可以縮短到22位左右
// 但這需要額外庫,而且可能丟精度,慎用

最后嘮叨兩句,也是掏心窩子的話

看完了前面的,你應該發(fā)現(xiàn)了,UUID這玩意兒看著簡單,水挺深的。

我最想說的是:別再拿new Date().getTime()當萬能了。我見過太多代碼,包括一些大廠的老項目,還在用時間戳當ID。是,毫秒級時間戳在單機單用戶場景下好像不會重復,但你要考慮:

  • 用戶瘋狂連點,JS事件循環(huán)跑得快,1毫秒內(nèi)能執(zhí)行好多次
  • 分布式系統(tǒng)里,多個用戶同時操作
  • 用戶電腦時間被手動調(diào)了(別笑,真有用戶喜歡把手表調(diào)快5分鐘)

時間戳這東西,做排序字段可以,做主鍵就是找死。真出事了你背鍋,產(chǎn)品經(jīng)理不會說是需求沒寫清楚,只會說"前端怎么搞的"。

另外,UUID也不是銀彈。它解決不了業(yè)務上的唯一性問題,比如同一個用戶在同一秒提交了兩次一樣的表單,UUID不同,但業(yè)務上是重復數(shù)據(jù)。這時候你需要業(yè)務層面的去重,比如根據(jù)用戶ID+內(nèi)容hash判斷。

還有,不要自己發(fā)明UUID算法。我看到過有人用Math.random().toString(36).substring(2)就當UUID用,結果長度不夠,隨機性差,字符集也不對。標準UUID是固定的8-4-4-4-12格式,32個十六進制字符,別整那些花里胡短的"創(chuàng)新",到時候和別的系統(tǒng)對接對不上就尷尬了。

最后的最后,考慮兼容性。如果你的項目還要支持IE,或者那些政企項目里的國產(chǎn)瀏覽器(內(nèi)核可能還是Chromium 60),記得做降級。crypto.randomUUID雖好,但別直接const id = crypto.randomUUID()就完事了,先判斷下cryptorandomUUID存不存在。

好了,說這么多,希望能幫你在下次被后端甩鍋"ID你自己生成"的時候,心里有點底。記住,能甩給后端的鍋盡量甩,甩不掉的,也要甩得專業(yè)點,至少別整出重復ID讓全組加班就行。

代碼寫完了,記得多測測,特別是那些安卓低端機,那才是前端真正的煉獄場。祝你好運,愿世間再無重復ID。

到此這篇關于JS生成UUID的10種姿勢及避坑指南的文章就介紹到這了,更多相關JS生成UUID方式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論

金门县| 定陶县| 曲周县| 海伦市| 来安县| 长治市| 施秉县| 潮安县| 泽州县| 台安县| 邳州市| 察哈| 土默特左旗| 宁南县| 澄城县| 阿拉善左旗| 民乐县| 台山市| 商丘市| 青冈县| 台湾省| 天柱县| 安丘市| 东乌| 塘沽区| 叶城县| 尉犁县| 靖州| 海阳市| 井陉县| 新建县| 三江| 邻水| 册亨县| 甘泉县| 年辖:市辖区| 德兴市| 化隆| 上林县| 历史| 荆门市|