跳到主要内容
JavaScript 生成 UUID 的常见方案与避坑指南 | 极客日志
JavaScript Node.js 大前端
JavaScript 生成 UUID 的常见方案与避坑指南 JavaScript 生成 UUID 涉及多种方案,从 Math.random 到原生 crypto API。分析各方法优劣,指出 Math.random 在低端机及安全性上的风险,推荐生产环境使用 uuid 包或 crypto.randomUUID。涵盖离线同步、草稿箱等实战场景,并提供调试验证技巧与兼容性降级策略,帮助开发者避免 ID 重复与碰撞问题。
ArchDesign 发布于 2026/4/7 更新于 2026/5/22 10 浏览JavaScript 生成 UUID 的常见方案与避坑指南
说实话,UUID 这玩意儿听起来挺'后端味儿',感觉应该是 Java 老哥在 Spring Boot 里搞定。但架不住现在前后端分离之后,后端越来越懒了——'这个 ID 你前端生成一下呗'。既然逃不掉,那咱就好好唠唠。今天这篇不整那些虚头八脑的,就是手把手教你从简易实现到正规军怎么搞 UUID,顺便给你看看那些年踩过的坑。
为啥前端突然要搞这破玩意儿?
我先还原个真实场景:需求评审会上,产品经理说要做个离线编辑功能,用户没网的时候也能写东西,有网了自动同步。后端小哥听完表示:'这个简单,前端你本地先存着,等联网了发给我。'
你小心翼翼地问:'那主键 ID 呢?'
后端翘着二郎腿:'你先生成一个呗,uuid 就行。'
那一刻你的内心是崩溃的。啥?我?生成主键?但说真的,这场景现在太常见。除了离线数据同步,还有埋点上报、本地缓存 Key、表单自动保存、WebSocket 消息去重等。所以别觉得 UUID 是后端专属,现在前端玩得可花了。但问题也来了:JavaScript 不像 Java 有个 java.util.UUID 直接 UUID.randomUUID() 就完事了,JS 生态百花齐放。
先整明白 UUID 到底是个啥
UUID 全称 Universally Unique Identifier,通用唯一识别码。标准格式是 8-4-4-4-12 的 32 个十六进制数字,比如 550e8400-e29b-41d4-a716-446655440000。
版本有好几个,咱们前端常用的是:
v4 :纯随机生成,最常用,看起来像 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx。
v1 :基于时间戳+MAC 地址,但前端拿不到 MAC 地址,所以基本是时间戳 + 随机数凑合版。
其实还有 v3、v5(基于命名空间和哈希),但前端基本用不上。你就记住:前端说 UUID,九成九是指 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 ());
这代码看着挺高级是吧?正则替换,位运算,十六进制转换。但我要给你泼盆冷水了:这玩意儿在生产环境用,分分钟教你做人 。
Math.random() 的随机性其实挺垃的,它是伪随机,基于种子生成的。在 V8 引擎里,不同浏览器实现还不一样。最离谱的是某些低端安卓机,Math.random() 的随机范围居然有偏置,生成的数扎堆,结果就是撞 ID 。
而且 Math.random() 还有个致命问题:它不是加密的 。如果你拿这个 UUID 当会话 ID 或者安全令牌,黑客能给你预测出来。
但你说完全不能用吗?倒也不是。如果只是做个前端临时缓存 key,丢了就丢了那种,用用也无妨。但记住,别用它做数据持久化的主键 ,真的会出事的。
简易实现第二式: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;
}
console .log (makeId ());
这代码是我早些年写的,当时觉得贼聪明——时间戳保证大致顺序,随机数保证唯一性,36 进制还能缩短字符串长度,完美!
结果上线第一天就出事了。用户狂点提交按钮,网络卡的时候点五次,后台瞬间收到五条数据,ID 居然一模一样。为啥?因为 Date.now() 是毫秒级的,用户手速再快也比不上机器循环快啊。
let counter = 0 ;
function betterId ( ) {
const timestamp = Date .now ().toString (36 );
const random = Math .random ().toString (36 ).substring (2 , 5 );
const count = (counter++).toString (36 ).padStart (4 , '0' );
return `${timestamp} -${random} -${count} ` ;
}
这下倒是不会重复了,但这代码看着就…挺丑的,而且还是不安全,还是那个问题,可以被预测。只适合临时用用,正式环境别这么搞。
简易实现第三式:浏览器指纹大杂烩 后来我又想了个骚操作,既然随机数不靠谱,那我把能拿到的设备信息都混进去总行了吧?指纹唯一性应该还可以。
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 ;
const seed = `${screenInfo} -${userAgent} -${language} -${platform} -${timezone} -${Date .now()} -${Math .random()} ` ;
let hash = 0 ;
for (let i = 0 ; i < seed.length ; i++) {
const char = seed.charCodeAt (i);
hash = ((hash << 5 ) - hash) + char;
hash = hash & hash;
}
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 不是唯一的 ,同型号手机,一样的屏幕分辨率,一样的浏览器,生成的指纹就一样。其次,现在浏览器都在搞隐私保护,navigator.userAgent 快要变成固定值了,platform 也快要藏起来了。
正规军来了:uuid npm 包到底香不香? 土法炼钢搞了半天,发现都有坑,那咋办?上正规军呗。
npm 上有个 uuid 包,周下载量上亿,属于业界标准了。用法简单得令人发指:
import { v4 as uuidv4 } from 'uuid' ;
const id = uuidv4 ();
console .log (id);
Node.js :用 crypto 模块生成真随机数
浏览器 :优先用 crypto.getRandomValues,降级才用 Math.random
React Native :也有对应实现
而且它还支持 v1、v3、v4、v5,虽然咱们基本只用 v4,但它有啊,显得专业。
我在生产环境用了三四年,几百万数据量,没出过重复问题。当然,理论上还是有碰撞概率的,但那个概率比你中彩票还低,可以忽略不计。
但有个坑要注意:版本问题 。uuid 包第 9 版是大版本,如果你项目里还在用第 8 版,升级的时候注意看 changelog,有些 API 变了。别问我怎么知道的,问就是曾经升级后线上挂了半小时。
还有个性能问题,如果你要一次性生成几万个 UUID ,uuid 包可能会有点慢,因为它内部有些校验逻辑。这时候你可以考虑用 crypto.randomUUID(),这是浏览器原生的,更快。
浏览器原生 API:crypto.randomUUID() 真香预警 这是现代浏览器(Chrome 92+、Firefox 95+、Safari 15.4+)带来的福音,原生支持,不用装包:
const id = crypto.randomUUID ();
console .log (id);
性能测试下来,比 uuid npm 包快大概 30%-50%,毕竟是原生 C++ 实现的。而且代码量少,不用引入依赖,bundle 体积都小了。
但!是!兼容性是个大问题 。你要是还要支持 IE11(虽然微软都放弃它了,但有些国企项目就是绕不开),或者某些老旧的安卓 WebView,这 API 直接报 undefined 给你看。
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 );
});
}
console .log (generateUUID ());
看到没?我还加了个 crypto.getRandomValues 的降级,这是浏览器提供的加密级随机数 API,比 Math.random() 安全多了,至少能保证随机性。
生产环境常见问题:那些我以为的唯一其实并不唯一 好了,前面都是技术方案,现在进入吐槽大会 环节,说说我在生产环境踩过的坑。
翻车事件一:安卓低端机大屠杀 2021 年,我们做个活动页,要在前端生成订单 ID,然后传给后端。我当时图省事,直接用了 Math.random() 版 UUID。结果活动上线当天,客服炸了,说有很多用户投诉'我明明只买了一次,怎么扣了三次钱?'
查日志发现,三个不同的用户,生成了同一个 UUID,后端以为是重复提交,直接拒绝了。再细查,全是某品牌千元安卓机,CPU 还是联发科三年前的入门款。那机器的 Math.random() 实现有问题,种子更新频率慢,短时间内生成的随机数高度相似。
解决方案:连夜改成 crypto.getRandomValues,并且加了个服务端兜底,如果前端没传 ID 或者 ID 格式不对,后端自己生成一个。
翻车事件二:并发地狱之 for 循环惨案 有个需求是批量导入,前端要一次生成 100 条数据的临时 ID。开发小哥写了个 for 循环:
const list = [];
for (let i = 0 ; i < 100 ; i++) {
list.push ({ id : Math .random ().toString (36 ).substring (2 ), data : xxx });
}
本地测试没问题,测试环境没问题,上线后用户导入 1000 条数据的时候,发现有 30% 的 ID 重复了。因为 Math.random() 在短时间内的种子可能没变,而且 toString(36).substring(2) 截取得太短,碰撞概率剧增。
翻车事件三:mock 数据摧毁生产数据库 这是最惨的一次。测试环境的 mock 脚本里,为了数据好看,用了固定种子的 UUID 生成器,比如:
let seed = 12345 ;
function mockUUID ( ) {
seed++;
return `fake-uuid-${seed} ` ;
}
结果某天测试同学不小心把测试配置指到了生产环境(别问为什么测试能连生产,问就是历史遗留问题),然后 mock 脚本跑了十万条数据进生产库,全是以 fake-uuid- 开头的 ID。更惨的是,这些数据后来同步到大数据平台,导致一整天的报表数据全脏了得回滚。
从那以后,我们定了个规矩:任何环境都不能用非标准 UUID 格式 ,mock 数据必须用正规的 uuid 库生成,哪怕只是测试。
实战代码大放送:这些场景你肯定用得上 光说不练假把式,给你几个我项目中真实在用的代码片段。
场景一:用户草稿箱本地存储 用户写长文的时候,每 30 秒自动保存一次草稿,关闭页面再回来还能恢复。
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 ( ) {
try {
return JSON .parse (localStorage .getItem (this .STORAGE_KEY )) || {};
} catch {
return {};
}
}
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 );
});
看到没?这里我用了双 ID 策略,外层的 currentDraftId 标识一篇草稿,内层的 versionId 标识每次保存的版本。这样即使用户疯狂 Ctrl+S,我们也能知道哪次保存是最新的。
场景二:PWA 离线数据同步 这是真正的业务场景,用户可能在地铁里没网的时候提交表单,有网了再同步。
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.randomUUID ();
const task = {
localId,
apiEndpoint,
payload,
createdAt : Date .now (),
retryCount : 0 ,
status : 'pending'
};
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;
}
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 {
const response = await fetch (task.apiEndpoint , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({ ...task.payload , clientId : task.localId })
});
if (response.ok ) {
await this .markAsSuccess (task.localId );
console .log (`任务 ${task.localId} 同步成功` );
}
} catch (error) {
await this .markAsFailed (task.localId );
console .error (`任务 ${task.localId} 同步失败` , error);
}
}
};
}
startListening ( ) {
window .addEventListener ('online' , () => {
console .log ('网络恢复,开始同步...' );
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 ) {
await fetch ('/api/submit' , {
method : 'POST' ,
body : JSON .stringify (data)
});
} else {
const localId = await syncManager.addTask ('/api/submit' , data);
alert ('当前无网络,数据已保存,联网后自动同步。本地 ID:' + localId);
}
});
这个方案的关键在于 localId,它在离线阶段就是数据的唯一身份证。等联网后,这个 ID 会传给后端,后端入库时可以把这个作为业务 ID,也可以自己再生成一个数据库自增 ID,但会把 localId 存到单独的字段做映射。这样前后端就能对得上号,不会出现'这数据我明明发了,后端说没收到'的扯皮情况。
场景三:WebSocket 消息防重发 网络抖动的时候,WebSocket 可能以为消息没发出去,实际已经发出去了,然后重发一次,导致后端处理了两次。
class ReliableWebSocket {
constructor (url ) {
this .url = url;
this .ws = null ;
this .messageQueue = [];
this .pendingMessages = new Map ();
this .reconnectAttempts = 0 ;
}
connect ( ) {
this .ws = new WebSocket (this .url );
this .ws .onopen = () => {
console .log ('WebSocket 连接成功' );
this .reconnectAttempts = 0 ;
this .flushQueue ();
};
this .ws .onmessage = (event ) => {
const data = JSON .parse (event.data );
if (data.type === 'ACK' ) {
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 ));
};
}
send (payload ) {
const messageId = crypto.randomUUID ();
const message = {
id : messageId,
timestamp : Date .now (),
payload,
retryCount : 0
};
if (this .ws .readyState === WebSocket .OPEN ) {
this .doSend (message);
} else {
this .messageQueue .push (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 (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} 发送失败,放弃重试` );
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 ();
this .pendingMessages .forEach ((msg, id ) => {
msg.retryCount = 0 ;
this .messageQueue .push (msg);
});
this .pendingMessages .clear ();
}
}
const ws = new ReliableWebSocket ('wss://example.com/ws' );
ws.connect ();
const msgId = ws.send ({ text : '你好啊' });
console .log ('发送消息 ID:' , msgId);
这个方案的核心就是消息 ID。服务端收到消息后,先查这个 ID 有没有处理过,处理过就直接返回 ACK,没处理过就处理然后存起来。这样就算客户端重发了,服务端也不会重复处理业务逻辑。
调试技巧:怎么验证你的 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 (`发现重复!第${i} 次生成了已存在的 ID: ${id} ` );
} else {
set.add (id);
}
if (i % 100000 === 0 && i > 0 ) {
console .log (`已生成${i} 个 UUID,目前无重复...` );
}
}
const endTime = performance.now ();
console .log (`测试完成!生成${count} 个 UUID,发现${duplicates} 个重复` );
console .log (`耗时:${(endTime - startTime).toFixed(2 )} ms` );
console .log (`平均每个 UUID 生成时间:${((endTime - startTime)/ count).toFixed(4 )} ms` );
return duplicates === 0 ;
}
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 );
if (typeof crypto !== 'undefined' && crypto.randomUUID ) {
testUniqueness (() => crypto.randomUUID (), 100000 );
}
跑这个脚本的时候,建议把浏览器标签页放后台,因为 100 万次循环会卡界面。或者用 Web Worker。
实时监控法:用 WeakMap 做内存去重 class UUIDMonitor {
constructor ( ) {
this .uuidSet = new Set ();
this .duplicates = [];
this .maxSize = 10000 ;
}
check (uuid ) {
if (this .uuidSet .has (uuid)) {
this .duplicates .push ({
uuid,
time : Date .now (),
stack : new Error ().stack
});
this .report (uuid);
return false ;
}
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 ) {
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 );
}
function saltyUUID ( ) {
const base = crypto.randomUUID ? crypto.randomUUID () : 'xxx' ;
return base.replace (/^.{4}/ , getDeviceSalt ());
}
Symbol 临时兜底 如果你只是需要在当前运行时 唯一的标识,不需要持久化,用 Symbol 最简单:
const uniqueKey = Symbol ('draft_key' );
const cache = {};
cache[uniqueKey] = { data : 'xxx' };
还有个小众场景:UUID 压缩 。标准的 UUID 是 36 个字符(带横杠),如果存数据库觉得太长,可以转成 Base64 或者去掉横杠:
function compressUUID (uuid ) {
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 )} ` ;
}
最后唠叨两句,也是掏心窝子的话 看完了前面的,你应该发现了,UUID 这玩意儿看着简单,水挺深的 。
我最想说的是:别再拿 new Date().getTime() 当万能钥匙了 。我见过太多代码,包括一些大厂的老项目,还在用时间戳当 ID。是,毫秒级时间戳在单机单用户场景下好像不会重复,但你要考虑:
用户疯狂连点,JS 事件循环跑得快,1 毫秒内能执行好多次
分布式系统里,多个用户同时操作
用户电脑时间被手动调了(别笑,真有用户喜欢把手表调快 5 分钟)
时间戳这东西,做排序字段可以,做主键就是找死。真出事了你背锅,产品经理不会说是需求没写清楚,只会说'前端怎么搞的'。
另外,UUID 也不是银弹 。它解决不了业务上的唯一性问题,比如同一个用户在同一秒提交了两次一样的表单,UUID 不同,但业务上是重复数据。这时候你需要业务层面的去重,比如根据用户 ID+ 内容 hash 判断。
还有,不要自己发明 UUID 算法 。我看到过有人用 Math.random().toString(36).substring(2) 就当 UUID 用,结果长度不够,随机性差,字符集也不对。标准 UUID 是固定的 8-4-4-4-12 格式,32 个十六进制字符,别整那些花里胡短的'创新',到时候和别的系统对接对不上就尴尬了。
最后的最后,考虑兼容性 。如果你的项目还要支持 IE,或者那些政企项目里的国产浏览器(内核可能还是 Chromium 60),记得做降级。crypto.randomUUID 虽好,但别直接 const id = crypto.randomUUID() 就完事了,先判断下 crypto 和 randomUUID 存不存在。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online