一、先说痛点
想临时生成个密码,你大概会搜一个「在线密码生成器」,把需求交给它。但想一想:你刚生成的口令,明文是不是已经落在了对方的服务器日志里?
更别提那些页面:先弹三个广告,再偷偷引一段第三方脚本,最后还顺手埋个埋点。工具越来越多,但它们离你的机器、离你的数据,越来越远。
能不能反过来——自己写一个,数据不出本机,断网也能跑?这一篇就带你从零做一个:一个离线密码 / 口令生成器,支持长度、字符规则、强度提示,纯前端、零依赖,最后整个东西就是一个.html文件,双击即用。
二、目标:一个能塞进 U 盘的工具
我们要交付的东西很具体:
- 选长度(6–40 位)、选字符集(大写 / 小写 / 数字 / 符号);
- 点一下生成,实时显示「熵强度」;
- 一键复制,并记住你上次的长度偏好;
- 不引任何 CDN、框架、字体;不联网、不上传;存成一个
.html就能发给同事。
读完你手里就有一个能用的工具,而它的全部代码,大约 60 行。
三、单文件工具长什么样
「单文件」不是噱头,而是把三件事装进一个文件:结构(HTML)+ 样式(CSS)+ 逻辑(JS)。最小骨架长这样:
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <style> /* 样式写这里,不引外部 css */ </style> </head> <body> <!-- 结构写这里 --> <script> /* 逻辑写这里,不引外部 js */ </script> </body> </html>三个要点:① 用系统字体栈(system-ui, "Microsoft YaHei", sans-serif),不下载字体;② 所有计算在浏览器本地完成,没有fetch到任何后端;③ 状态用localStorage留在本地。这就是「单文件 · 零依赖 · 本地优先」三条铁律的来历。
四、实战:60 行写出密码生成器
下面给出完整可复制的单文件。把它整段保存成password.html,双击就能用。
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>离线密码生成器</title> <style> :root { --bg:#0A0A0B; --card:#141417; --line:rgba(255,255,255,.08); --accent:#5E6AD2; --muted:#A1A1AA; } * { box-sizing:border-box; } body { margin:0; font:15px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,"Microsoft YaHei",sans-serif; background:var(--bg); color:#fff; display:flex; justify-content:center; padding:32px 16px; } .app { width:100%; max-width:420px; background:var(--card); border:1px solid var(--line); border-radius:14px; padding:20px; } h1 { font-size:18px; margin:0 0 16px; } .out { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:16px; word-break:break-all; background:#0A0A0B; border:1px solid var(--line); border-radius:8px; padding:12px; min-height:48px; } button { background:var(--accent); color:#fff; border:0; border-radius:8px; padding:10px 14px; cursor:pointer; font-size:14px; } button.ghost { background:transparent; border:1px solid var(--line); color:var(--muted); } input[type=range]{ flex:1; } label.chk { display:flex; align-items:center; gap:6px; font-size:14px; color:var(--muted); } .meta { display:flex; justify-content:space-between; color:var(--muted); font-size:13px; margin-top:6px; } .bar { height:6px; border-radius:3px; background:#222; overflow:hidden; margin-top:8px; } .bar > i { display:block; height:100%; width:0; background:var(--accent); transition:width .2s; } </style> </head> <body> <div class="app"> <h1>🔐 离线密码生成器</h1> <div class="out" id="out" aria-live="polite">点击生成</div> <div class="bar"><i id="bar"></i></div> <div class="meta"><span id="bits">强度:—</span><span id="lenLabel">16 位</span></div> <div class="row" style="display:flex;align-items:center;gap:10px;margin:10px 0"> <span>长度</span> <input type="range" id="len" min="6" max="40" value="16"> </div> <div class="row" style="display:flex;flex-wrap:wrap;gap:10px;margin:10px 0"> <label class="chk"><input type="checkbox" id="up" checked> 大写</label> <label class="chk"><input type="checkbox" id="lo" checked> 小写</label> <label class="chk"><input type="checkbox" id="dg" checked> 数字</label> <label class="chk"><input type="checkbox" id="sy" checked> 符号</label> </div> <div class="row" style="display:flex;gap:10px;margin:10px 0"> <button id="gen">生成</button> <button id="copy" class="ghost">复制</button> </div> </div> <script> const $ = id => document.getElementById(id); const SETS = { up: "ABCDEFGHJKLMNPQRSTUVWXYZ", lo: "abcdefghijkmnopqrstuvwxyz", dg: "23456789", sy: "!@#$%^&()-_=+[]{}" }; // 用密码学安全随机数,而不是 Math.random() function randInt(n){ const a = new Uint32Array(1); crypto.getRandomValues(a); return a[0] % n; } function gen(){ const len = +$("len").value; let pool = ""; for (const k in SETS) if ($(k).checked) pool += SETS[k]; if (!pool) { $("out").textContent = "至少选一种字符"; return; } let s = ""; for (let i=0;i<len;i++) s += pool[randInt(pool.length)]; $("out").textContent = s; const bits = Math.round(len * Math.log2(pool.length)); // 熵估算 $("bits").textContent = "强度:" + bits + " bits"; $("bar").style.width = Math.min(100, bits/64100) + "%"; try { localStorage.setItem("pw.len", len); } catch(e){} } // 复制:优先用剪贴板 API,file:// 下回退到 execCommand function copy(){ const t = $("out").textContent; if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(t); else { const ta=document.createElement("textarea"); ta.value=t; document.body.appendChild(ta); ta.select(); try{document.execCommand("copy");}catch(e){} document.body.removeChild(ta); } } $("len").oninput = () => $("lenLabel").textContent = $("len").value + " 位"; $("gen").onclick = gen; $("copy").onclick = copy; const saved = +localStorage.getItem("pw.len"); if (saved) $("len").value = saved; gen(); </script> </body> </html>这段代码里,有三个值得记住的点
1)随机性要用crypto.getRandomValues,别用Math.random()。后者只是伪随机、可被预测,拿来做口令等于没加密。crypto走的是浏览器的密码学安全随机源,离线也能用。
2)强度按「熵」估算,而不是按「看起来乱不乱」。一行公式:bits = 长度 × log2(字符池大小)。16 位、含四类字符大约 90+ bits,远超够用线(64 bits 我设成进度条满格)。这比「感觉挺复杂」科学得多。
3)偏好用localStorage留本地。你下次打开还是上次的长度——但它只存在你这台机器上,不会回传任何服务器。
五、让它真正「工具化」:离线优先三铁律
把上面的思路抽出来,就是做单文件工具的三条纪律:
- 单文件:整个工具一个
.html,可下载、可邮件、可丢 U 盘; - 零依赖:不引任何外部 CDN / 框架 / 字体,断网照跑;
- 本地优先:数据不出本机,没有后端、没有埋点、没有账号。
再加两个小习惯会更「专业」:用prefers-reduced-motion尊重系统的减弱动效设置;把视觉统一成一套克制的中性暗色(比如上面用的#0A0A0B画布 +#5E6AD2靛蓝强调色)。
六、收尾
到这里,你已经从零写出一个能用的单文件工具了——它不依赖任何库,断网可用,口令明文只活在你自己的浏览器里。
如果你不想自己写、想直接白嫖:nano-tools 矩阵里这个密码生成器的完整版叫PassForge,连同另外 42 款同类单文件工具(白板、JSON、哈希、正则、色彩、无障碍……)都开源在 GitHub,挑一个双击html就能用。
留个思考题:如果把这个骨架换成「UUID 生成器」「Base64 编解码」或者「JSON 格式化」,改动其实不过十几行——你最想把它改造成什么?