前端小白也能秒上手:JS生成UUID的10种姿势(附避坑指南)
前端小白也能秒上手:JS生成UUID的10种姿势(附避坑指南)
前端小白也能秒上手:JS生成UUID的10种姿势(附避坑指南)
说实话啊,这篇文章我原本是不想写的。真的,因为UUID这玩意儿听起来就挺"后端味儿"的,感觉应该是那帮穿格子衫的Java老哥在Spring Boot里@GeneratedValue一下搞定的事儿。但架不住现在前后端分离之后,后端那帮兄弟越来越懒了——“哎这个ID你前端生成一下呗,反正你也是要本地预览的嘛”,“哎呀你离线存储自己搞个临时ID嘛,后端只管存”。
我:???
行吧,既然逃不掉,那咱就好好唠唠。今天这篇不整那些虚头八脑的,就是手把手教你从"土法炼钢"到"正规军"怎么搞UUID,顺便给你看看那些年在生产环境踩过的坑,血淋淋的教训,看完保证你少加三天班。
为啥前端突然要搞这破玩意儿?还不是被后端逼的
我先还原个真实场景,你看熟不熟悉:
需求评审会上,产品经理说要做个"离线编辑功能",用户没网的时候也能写东西,有网了自动同步。后端小哥听完当场表示:“这个简单,前端你本地先存着,等联网了发给我,我给入库。”
你小心翼翼地问:“那主键ID呢?”
后端翘着二郎腿:“你先生成一个呗,uuid就行,我直接用。”
那一刻你的内心是崩溃的。啥?我?生成主键?这玩意儿不是数据库该干的活吗?
但说真的,这场景现在太常见了。除了刚才说的离线数据同步,还有:
- 埋点上报:你要追踪用户点了哪个按钮,得给每个事件一个唯一身份证号吧?
- 本地缓存Key:localStorage里存了一堆草稿,总得有个标识区分"草稿1"和"草稿2"吧?
- 表单自动保存:用户写到一半的表单,刷新页面不能丢,得给个临时ID对应到这份草稿。
- 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的人都写过:
functiongenerateUUID(){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这代码看着挺高级是吧?正则替换,位运算,十六进制转换,乍一看以为是大佬写的。但我要给你泼盆冷水了:这玩意儿在生产环境用,分分钟教你做人。
Math.random()的随机性其实挺垃的,它是伪随机,基于种子生成的。在V8引擎里,不同浏览器实现还不一样。最离谱的是某些低端安卓机(说的就是你们,那些运营商定制的千元机),Math.random()的随机范围居然有偏置,生成的数扎堆,结果就是撞ID。
我曾经在日志系统里看到,一天之内居然有几百个重复的UUID,查了半天发现都是某个国产安卓机型干的好事。那场面,堪称车祸现场。
而且Math.random()还有个致命问题:它不是加密的。如果你拿这个UUID当会话ID或者安全令牌,黑客能给你预测出来,直接社会工程学攻击走起。
但你说完全不能用吗?倒也不是。如果只是做个前端临时缓存key,丢了就丢了那种,用用也无妨。但记住,别用它做数据持久化的主键,真的会出事的。
土法炼钢第二式:Date.now()加料版
既然纯随机不靠谱,那咱加点时间戳总行了吧?时间总是唯一的嘛(理论上)。
functionmakeId(){let id ='';// 时间戳部分,13位const timestamp = Date.now().toString(36);// 转成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());// 类似:lxx1h2z3-abc123-def456这代码是我早些年写的,当时觉得贼聪明——时间戳保证大致顺序,随机数保证唯一性,36进制还能缩短字符串长度,完美!
结果上线第一天就出事了。用户狂点提交按钮,网络卡的时候点五次,后台瞬间收到五条数据,ID居然一模一样。为啥?因为Date.now()是毫秒级的,用户手速再快也比不上机器循环快啊。
后来我学乖了,加了个自增计数器:
let counter =0;functionbetterId(){const timestamp = Date.now().toString(36);const random = Math.random().toString(36).substring(2,5);// 加了个原子计数器,每次调用都+1const count =(counter++).toString(36).padStart(4,'0');return`${timestamp}-${random}-${count}`;}这下倒是不会重复了,但这代码看着就…挺丑的,而且还是不安全,还是那个问题,可以被预测。只适合临时用用,正式环境别这么搞。
土法炼钢第三式:浏览器指纹大杂烩
后来我又想了个骚操作,既然随机数不靠谱,那我把能拿到的设备信息都混进去总行了吧?指纹唯一性应该还可以。
functionfingerprintUUID(){// 收集各种浏览器信息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函数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位整数}// 转成十六进制,再格式化成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不是唯一的,同型号手机,一样的屏幕分辨率,一样的浏览器,生成的指纹就一样。其次,现在浏览器都在搞隐私保护,navigator.userAgent快要变成固定值了(Chrome的User-Agent Reduction计划),platform也快要藏起来了。
所以这条路基本也行不通,属于自娱自乐型。
正规军来了:uuid npm包到底香不香?
土法炼钢搞了半天,发现都有坑,那咋办?上正规军呗。
npm上有个uuid包,周下载量上亿,属于业界标准了。用法简单得令人发指:
// 先装上:npm install uuidimport{ v4 as uuidv4 }from'uuid';// 生成一个v4 UUIDconst id =uuidv4(); console.log(id);// 比如:9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d这包的好处是啥呢?它考虑了各种环境:
- 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);// f47ac10b-58cc-4372-a567-0e02b2c3d479性能测试下来,比uuid npm包快大概30%-50%,毕竟是原生C++实现的。而且代码量少,不用引入依赖,bundle体积都小了。
但!是!兼容性是个大问题。你要是还要支持IE11(虽然微软都放弃它了,但有些国企项目就是绕不开),或者某些老旧的安卓WebView,这API直接报undefined给你看。
所以稳妥的写法是加个polyfill:
functiongenerateUUID(){// 优先用原生的if(typeof crypto !=='undefined'&& crypto.randomUUID){return crypto.randomUUID();}// 降级方案,用uuid库,或者自己实现一个v4// 这里为了演示,手写一个简陋版,生产环境建议还是引入uuid包return'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){const r =(typeof crypto !=='undefined'&& crypto.getRandomValues)? crypto.getRandomValues(newUint8Array(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生成器,比如:
// mock脚本let seed =12345;functionmockUUID(){ seed++;return`fake-uuid-${seed}`;}结果某天测试同学不小心把测试配置指到了生产环境(别问为什么测试能连生产,问就是历史遗留问题),然后mock脚本跑了十万条数据进生产库,全是以fake-uuid-开头的ID。更惨的是,这些数据后来同步到大数据平台,导致一整天的报表数据全脏了得回滚。
从那以后,我们定了个规矩:任何环境都不能用非标准UUID格式,mock数据必须用正规的uuid库生成,哪怕只是测试。
实战代码大放送:这些场景你肯定用得上
光说不练假把式,给你几个我项目中真实在用的代码片段。
场景一:用户草稿箱本地存储
用户写长文的时候,每30秒自动保存一次草稿,关闭页面再回来还能恢复。
classDraftManager{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{returnJSON.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 =newDraftManager(); document.getElementById('editor').addEventListener('input',(e)=>{ draftManager.saveDraft(e.target.value);});看到没?这里我用了双ID策略,外层的currentDraftId标识一篇草稿,内层的versionId标识每次保存的版本。这样即使用户疯狂Ctrl+S,我们也能知道哪次保存是最新的。
场景二:PWA离线数据同步
这是真正的业务场景,用户可能在地铁里没网的时候提交表单,有网了再同步。
classOfflineSyncManager{constructor(){this.DB_NAME='offline_data';this.STORE_NAME='pending_requests';this.db =null;this.initDB();}asyncinitDB(){returnnewPromise((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'});};});}// 添加离线任务asyncaddTask(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;}// 同步数据asyncsync(){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{// 发送请求时带上localId,后端返回时会带上,这样前端可以对应上const response =awaitfetch(task.apiEndpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...task.payload,clientId: task.localId // 告诉后端这是哪条数据})});if(response.ok){// 成功后从本地删除,或者标记为成功awaitthis.markAsSuccess(task.localId); console.log(`任务 ${task.localId} 同步成功`);}}catch(error){awaitthis.markAsFailed(task.localId); console.error(`任务 ${task.localId} 同步失败`, error);}}};}// 监听网络恢复startListening(){ window.addEventListener('online',()=>{ console.log('网络恢复,开始同步...');this.sync();});}}// 使用示例const syncManager =newOfflineSyncManager();// 用户点击提交 document.getElementById('submit').addEventListener('click',async()=>{const data ={name: document.getElementById('name').value,content: document.getElementById('content').value };if(navigator.onLine){// 有网直接发awaitfetch('/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可能以为消息没发出去,实际已经发出去了,然后重发一次,导致后端处理了两次。
classReliableWebSocket{constructor(url){this.url = url;this.ws =null;this.messageQueue =[];// 待发送队列this.pendingMessages =newMap();// 已发送但未确认的消息this.reconnectAttempts =0;}connect(){this.ws =newWebSocket(this.url);this.ws.onopen=()=>{ console.log('WebSocket连接成功');this.reconnectAttempts =0;this.flushQueue();// 把之前没发出去的发了};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));};}// 发送消息,带唯一ID和重试机制send(payload){const messageId = crypto.randomUUID();// 每条消息一个唯一IDconst 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;// 返回ID,方便业务层监听}doSend(message){this.ws.send(JSON.stringify(message));// 记录到pending,等ACKthis.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} 发送失败,放弃重试`);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的消息标记为需要重发this.pendingMessages.forEach((msg, id)=>{ msg.retryCount =0;// 重置重试次数this.messageQueue.push(msg);});this.pendingMessages.clear();}}// 使用const ws =newReliableWebSocket('wss://example.com/ws'); ws.connect();// 发送消息const msgId = ws.send({text:'你好啊'}); console.log('发送消息ID:', msgId);这个方案的核心就是消息ID。服务端收到消息后,先查这个ID有没有处理过,处理过就直接返回ACK,没处理过就处理然后存起来。这样就算客户端重发了,服务端也不会重复处理业务逻辑。
调试技巧:怎么验证你的UUID真的唯一?
写完代码总得测试吧?但UUID理论上会重复,只是概率极低,怎么验证你的生成器靠谱呢?
暴力测试法:跑100万次
functiontestUniqueness(generator, count =1000000){const set =newSet();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);}// 每10万次报告一次进度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;}// 测试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.randomUUIDif(typeof crypto !=='undefined'&& crypto.randomUUID){testUniqueness(()=> crypto.randomUUID(),100000);}跑这个脚本的时候,建议把浏览器标签页放后台,因为100万次循环会卡界面。或者用Web Worker。
实时监控法:用WeakMap做内存去重
如果你不确定线上环境有没有重复,可以埋个点:
classUUIDMonitor{constructor(){this.uuidSet =newSet();this.duplicates =[];this.maxSize =10000;// 只保留最近1万个,防止内存爆炸}check(uuid){if(this.uuidSet.has(uuid)){this.duplicates.push({ uuid,time: Date.now(),stack:newError().stack // 记录调用栈});// 上报到监控平台this.report(uuid);returnfalse;}// 满了就清一半,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);returntrue;}report(uuid){// 发到 sentry 或者自研监控平台 console.error(`UUID重复警告:${uuid}`,this.duplicates[this.duplicates.length -1]);}}// 使用const monitor =newUUIDMonitor();functionsafeGenerateUUID(){const id = crypto.randomUUID ? crypto.randomUUID():'xxx-xxx'.replace(/x/g,()=> Math.random().toString(16)[2]);if(!monitor.check(id)){// 重复了,重新生成(虽然理论上不应该)returnsafeGenerateUUID();}return id;}console.log大法:时间戳定位
如果怀疑某个UUID有问题,可以在生成的时候打详细的log:
functiondebugUUID(){const id = crypto.randomUUID(); console.log(`%c生成UUID: ${id}`,'color: #1890ff; font-weight: bold;',`\n时间: ${newDate().toISOString()}`,`\n页面: ${location.href}`,`\n用户: ${localStorage.getItem('userId')||'未登录'}`,`\n堆栈:`,newError().stack );return id;}这样出问题的时候,你可以在控制台Filter里搜这个UUID,看它是什么时候在哪生成的。
冷门但好用的小技巧
用performance.now()提升时间精度
Date.now()只能到毫秒,但performance.now()可以精确到微秒(虽然也是假的,是高分表时间),适合用来做时间戳部分:
functionhighResTimestamp(){// 基础时间戳 + 高分表时间,精度更高const base = Date.now();const highRes = performance.now();return`${base}-${Math.floor(highRes *1000)}`;}navigator.userAgent做轻量设备指纹
虽然不建议依赖userAgent,但用来做盐值(salt)增加随机性还是可以的:
functiongetDeviceSalt(){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生成functionsaltyUUID(){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传递// 只适合内存中的临时标识还有个小众场景:UUID压缩。标准的UUID是36个字符(带横杠),如果存数据库觉得太长,可以转成Base64或者去掉横杠:
functioncompressUUID(uuid){// 去掉横杠,32位return uuid.replace(/-/g,'');}functiondecompressUUID(short){// 还原横杠return`${short.substring(0,8)}-${short.substring(8,12)}-${short.substring(12,16)}-${short.substring(16,20)}-${short.substring(20)}`;}// 更狠的,转成Base62( alphanumeric ),可以缩短到22位左右// 但这需要额外库,而且可能丢精度,慎用最后唠叨两句,也是掏心窝子的话
看完了前面的,你应该发现了,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存不存在。
好了,说这么多,希望能帮你在下次被后端甩锅"ID你自己生成"的时候,心里有点底。记住,能甩给后端的锅尽量甩,甩不掉的,也要甩得专业点,至少别整出重复ID让全组加班就行。
代码写完了,记得多测测,特别是那些安卓低端机,那才是前端真正的炼狱场。祝你好运,愿世间再无重复ID,阿弥陀佛。
