跳到主要内容前端开发中 JavaScript 生成 UUID 的最佳实践与陷阱 | 极客日志JavaScriptNode.js大前端算法
前端开发中 JavaScript 生成 UUID 的最佳实践与陷阱
综述由AI生成JavaScript 生成 UUID 是前端离线存储、数据同步及临时标识的关键技术。文章对比了 Math.random、Date.now 等原生方案与 crypto.randomUUID、npm uuid 包的安全性差异,重点剖析生产环境中因随机数质量导致的 ID 碰撞问题。通过草稿箱、PWA 同步及 WebSocket 去重等实战场景,展示了如何构建可靠的唯一标识系统,并提供了兼容性降级策略与调试验证方法。
修罗14 浏览 前端开发中 JavaScript 生成 UUID 的最佳实践与陷阱
说实话,UUID 这玩意儿听起来挺'后端味儿'的,感觉应该是 Java 老哥在 Spring Boot 里搞定的事儿。但架不住现在前后端分离之后,后端越来越懒了——'哎这个 ID 你前端生成一下呗,反正你也是要本地预览的嘛'。
既然逃不掉,那咱就好好唠唠。今天这篇不整那些虚头八脑的,就是手把手教你从原生方案到正规军怎么搞 UUID,顺便给你看看那些年在生产环境踩过的坑。
为啥前端突然要搞这破玩意儿?
先还原个真实场景:需求评审会上,产品经理说要做个离线编辑功能,用户没网的时候也能写东西,有网了自动同步。后端小哥听完表示:'这个简单,前端你本地先存着,等联网了发给我。'
你小心翼翼地问:'那主键 ID 呢?'
后端翘着二郎腿:'你先生成一个呗,uuid 就行,我直接用。'
那一刻你的内心是崩溃的。啥?我?生成主键?但这场景现在太常见了。除了离线数据同步,还有埋点上报、本地缓存 Key、表单自动保存以及 WebSocket 消息去重。别觉得 UUID 是后端专属,现在前端玩得可花了。但问题也来了:JavaScript 不像 Java 有个 java.util.UUID 直接调用就完事了,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}`;
}
这下倒是不会重复了,但这代码看着就…挺丑的,而且还是不安全,还是那个问题,可以被预测。只适合临时用用,正式环境别这么搞。
正规军来了: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:也有对应实现
我在生产环境用了三四年,几百万数据量,没出过重复问题。当然,理论上还是有碰撞概率的,但那个概率比你中彩票还低,可以忽略不计。
但有个坑要注意:版本问题。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 存不存在。
好了,说这么多,希望能帮你在下次被后端甩锅'ID 你自己生成'的时候,心里有点底。记住,能甩给后端的锅尽量甩,甩不掉的,也要甩得专业点,至少别整出重复 ID 让全组加班就行。
代码写完了,记得多测测,特别是那些安卓低端机,那才是前端真正的炼狱场。祝你好运,愿世间再无重复 ID。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- 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