JavaScript 生成 UUID 的常见方法与避坑指南
在前后端分离架构中,前端生成唯一标识符(UUID)的需求日益增多。本文从基础实现到标准库方案,详细解析 JavaScript 生成 UUID 的方法、优缺点及生产环境注意事项。
为什么前端需要生成 UUID
常见场景包括:
- 离线数据同步:用户无网时本地存储,联网后上传。
- 埋点上报:为每个事件分配唯一 ID。
- 本地缓存 Key:区分 localStorage 中的多条草稿或临时数据。
- WebSocket 消息去重:防止网络抖动导致的重复处理。
UUID 基础概念
UUID(Universally Unique Identifier)通用唯一识别码,标准格式为 8-4-4-4-12 的 32 个十六进制数字。前端常用版本:
- v4:纯随机生成,最常用。
- v1:基于时间戳 + MAC 地址,前端通常无法获取 MAC,故较少使用。
自定义实现方案
1. Math.random() 实现
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());
风险:Math.random() 是伪随机,部分低端安卓机存在随机性偏差,可能导致 ID 碰撞。且非加密安全,不适合用作会话令牌。
2. Date.now() 组合
function makeId() {
let id = '';
const timestamp = Date.now().toString(36);
const randomPart = Math.random().toString(36).substring(2, 8);
id = `${timestamp}-${randomPart}-${Math.random().toString(36).substring(2, 8)}`;
return id;
}
风险:高并发下毫秒级时间戳可能重复,需配合计数器使用,但仍存在可预测性。
3. 浏览器指纹混合
收集屏幕信息、UserAgent 等生成哈希。但隐私保护政策导致指纹信息受限,且同型号设备指纹可能相同,不推荐作为主键。
成熟方案
1. npm 包 uuid
业界标准库,支持 Node.js 和浏览器环境。
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
优点:跨平台兼容性好,安全性高,支持多种版本。 注意:注意大版本升级带来的 API 变更;批量生成时性能略低于原生 API。
2. 浏览器原生 API crypto.randomUUID()
现代浏览器(Chrome 92+, Firefox 95+)原生支持。
const id = crypto.randomUUID();
优点:无需依赖,性能优于 npm 包,加密级随机数。 注意:兼容性要求较高,老旧浏览器需降级处理。
降级方案示例:
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// 降级实现
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);
});
}
生产环境常见问题
1. 随机数质量不足
部分低端机型 Math.random() 种子更新慢,导致短时间内 ID 重复。建议强制使用 crypto 相关 API。
2. 并发冲突
循环生成 ID 时,若未加锁或计数器,可能产生重复。确保原子性或引入全局唯一标识。
3. Mock 数据污染
测试环境 Mock 脚本应使用正规 UUID 库,避免特殊格式污染生产数据库。
实战场景代码
场景一:草稿箱本地存储
class DraftManager {
constructor() {
this.STORAGE_KEY = 'user_drafts';
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(),
versionId: crypto.randomUUID ? crypto.randomUUID() : Date.now()
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(drafts));
}
getAllDrafts() {
{
.(.(.)) || {};
} {
{};
}
}
}
场景二:PWA 离线同步
利用 IndexedDB 存储任务,使用 UUID 作为本地主键,联网后同步至后端。
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;
db.createObjectStore(this.STORE_NAME, { keyPath: 'localId' });
};
});
}
async addTask(apiEndpoint, payload) {
const localId = crypto.();
task = {
localId,
apiEndpoint,
payload,
: .(),
: ,
:
};
transaction = ..([.], );
store = transaction.(.);
store.(task);
localId;
}
}
场景三:WebSocket 消息防重发
每条消息携带唯一 ID,服务端确认(ACK)后删除待发送队列记录。
class ReliableWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.pendingMessages = new Map();
}
send(payload) {
const messageId = crypto.randomUUID();
const message = { id: messageId, timestamp: Date.now(), payload };
if (this.ws.readyState === WebSocket.OPEN) {
this.doSend(message);
}
return messageId;
}
doSend(message) {
this.ws.send(JSON.stringify(message));
this.pendingMessages.set(message.id, { ...message, sendTime: Date.now() });
setTimeout(() => this.checkAck(message.id), 3000);
}
checkAck(messageId) {
if (..(messageId)) {
msg = ..(messageId);
(msg. < ) {
msg.++;
.(msg);
} {
..(messageId);
}
}
}
}
调试与验证
暴力测试法
生成大量 UUID 存入 Set,检查是否有重复。
function testUniqueness(generator, count = 100000) {
const set = new Set();
for (let i = 0; i < count; i++) {
const id = generator();
if (set.has(id)) {
console.log(`发现重复!第${i}次生成了已存在的 ID: ${id}`);
return false;
}
set.add(id);
}
return true;
}
监控埋点
在生产环境使用 WeakMap 或 Set 记录生成的 ID,发现重复立即告警。
总结与建议
- 拒绝时间戳主键:时间戳在高并发或分布式环境下易重复,不建议作为主键。
- 优先使用原生 API:
crypto.randomUUID()性能与安全性最佳,注意兼容性降级。 - 避免自定义算法:除非必要,不要发明新的 UUID 格式,遵循标准 8-4-4-4-12 结构以便对接。
- 考虑业务去重:UUID 仅保证技术唯一性,业务层面仍需根据用户 ID+ 内容 Hash 判断重复。
通过合理选择生成策略并规避常见陷阱,可有效保障前端数据的一致性与安全性。


