无后台年会抽奖网页:localStorage持久化与Fisher-Yates洗牌实战

发布时间:2026/10/9 17:29:07
无后台年会抽奖网页:localStorage持久化与Fisher-Yates洗牌实战 简介这是一份面向企业年会、节日庆典等场景的年会抽奖网页源码纯前端实现、无需后台服务器适合行政人员或前端开发者快速部署使用。包体共24个文件约4.77MB以HTML入口页、JavaScript逻辑脚本、图片及字体资源构成其中抽奖名单集中存放于js/member.js中修改该文件即可替换参与者数据另附README.md说明文档便于迅速上手。抽奖逻辑由浏览器端随机算法实现数据处理均在本地完成部署简单17个png图片涵盖奖品展示、按钮、背景等界面元素视觉效果较好。目前已有986人学习浏览适合需要在短时间内搭建界面友好、操作直观的现场抽奖工具并根据实际需求定制名单与奖品配置的使用者。1. 年会抽奖网页无后台是什么意思能扛住多大的场子年会抽奖网页源码无后台等于把你原以为必须塞给服务器的活儿全部搬进浏览器里干。名单、随机、记录、导出四件事由一组静态文件完成现场只需要一台能开网页的电脑加一块投影幕。无后台不是没能力只是不去依赖哪台“中央服务器”恰恰因为少了这个中心反而砍掉了一整类“活动现场服务器挂了”的灾难。我去年帮某公司做年会时就用的这套方案两百人三轮奖中途换场地拔电源重新打开页面数据照常接着用。它适合单场 100 到 300 人活动的组织者、接年会活儿的开发者也适合不想碰服务器和数据库的非技术主办方。它不擅长的是多人同时写数据、跨天跑长线运营的线上活动那种场景我建议老老实实去上后端。2. 为什么无后台可行浏览器就是你的“中控台”2.1 纯静态方案的文件组成与工作边界一段年会抽奖流程里后台真正要做的事其实只有三件存一份参与者名单、产生不重复的随机结果、把中奖记录留档。这三件事不需要高并发不需要鉴权也不需要跨设备同步因为它们都发生在同一个物理空间里——年会现场。既然所有操作都集中在主持人手里的那台电脑上把这三件事压进浏览器就是顺理成章的优化。无后台方案的“后台”由两部分顶替文件系统负责名单的静态存储浏览器的 localStorage 负责运行期状态记录。文件组织通常就四件套一个 HTML 放页面骨架一个 CSS 放布局和动效一个 JS 放全部业务逻辑一个 txt 放每行一个名字的参与名单。这个结构对新手友好任何一个文件坏了都能单独替换不用重新部署也不存在“某个接口超时”这种查起来让人血压升高的玄学问题。边界条件必须说清楚这种方案的数据所有权属于打开页面的那台浏览器。手机和现场其他设备如果同时打开同一个页面只能当观众不能写入数据因为每个浏览器各自持有自己的 localStorage 副本。现场唯一合法的“写数据者”是负责抽奖的主投屏电脑。所以操作习惯上我一律只在这些主设备上点击抽奖其他连进来的设备只做投屏或展示。我一般按这个标准判断能不能用无后台参会人数在 500 人以内奖项轮次不超过五轮活动时长不超过一整天。超过这个规模名单导入方式要从粘贴文本升级成表格文件读取存储也要考虑多设备协同那就不如直接上后端。另外这种一次性活动页面完全不需要打包框架一个原生 JS 文件就能讲清楚全部逻辑维护成本远低于引一个脚手架。2.2 名单与结果存哪localStorage 的读写模型无后台方案里最容易被低估的是存储。很多人以为不开服务器就没法保存数据其实浏览器的 localStorage 就是一个小型持久化数据库它按同源策略隔离以键值对形式存储字符串。数组、对象这类结构必须先序列化成 JSON 再写入读取时再反序列化。年会名单几百行文本、中奖记录几十条撑死也占不了几百 KB而 localStorage 的常规容量在 5MB 级别空间完全够用。为什么不选 cookiecookie 设计出来是给 HTTP 请求携带状态的容量只有几 KB而且每次请求都会带上它在一个无后台的静态页面里根本没有请求可带用它属于绕远路。为什么不选 sessionStorage它的问题是关掉标签页就清空年会现场一旦有人误关页面所有记录归零这在现场是不可接受的。localStorage 的持久性正好匹配“临时工具但现场不能出错”的定位。还要提一个容易翻车的点localStorage 绑定在“协议 域名 端口”这个源上。如果你用 file:// 协议直接双击 HTML 打开页面有些浏览器对 file 源的存储行为并不稳定换个浏览器就找不到你存过的数据所以我在无后台方案里也坚持用本地静态服务来访问页面这既让手机能连进来也让 localStorage 的行为可预期。顺便补充一句如果现场用的是隐私模式某些浏览器会限制 localStorage 写入极端情况下会直接抛异常我一般会在存储操作外面包一层 try/catch失败时弹提示让用户换普通模式避免抽奖中途页面静默失忆。读写封装通常就是 setItem 加 getItem配合 JSON 序列化就能覆盖大多数需求。重要的是把“内存里的实时状态”和“localStorage 里的备份状态”这两套数据保持一致抽奖每走一步就立刻落盘。这部分代码在第 3 章直接给出这里先讲明白模型抽奖本质上是对“剩余池”和“中奖名单”这两个数组的持续变更变更一次、保存一次。2.3 抽奖公平性从 Math.random 到洗牌取样的差异“用 Math.random 抽个人出来而已能有多难”这是我常听到的一句话直到看见现场抽完一轮发现有人中两次。直接写 Math.floor(Math.random() * arr.length) 来抽取单次单人的概率看起来没问题但多轮多人时就暴露两类缺陷第一随机到的下标可能重复需要额外的去重循环第二如果不去重就让同一个人进入下一轮上一轮中奖者仍然留在池子里后续轮次的概率就乱了。公平的做法是洗牌后取样。先把参与名单整体做一次 Fisher-Yates 洗牌把顺序彻底打乱之后按顺序从前到后取人即可。Fisher-Yates 的流程是从数组末尾开始向前遍历每到一个位置 i就随机挑一个 0 到 i 的下标 j交换 i 和 j 的位置一轮下来每个元素出现在任意位置的概率相同。时间复杂度 O(n)空间 O(1)几百人的数组瞬间完成不存在性能压力。无后台方案里洗牌还有个额外的好处洗牌是在页面加载时一次性完成的洗完之后每轮抽取只是“顺序取数”不用反复生成随机数现场逻辑复杂度大幅降低。需要注意 Math.random() 不是加密安全随机源但年会场景没有敌手去反推种子现场的信任靠“投影实时展示候选人名单变动”来建立而不是靠随机源强度。如果你确实需要更硬的随机性可以从 window.crypto.getRandomValues 拿随机数来驱动洗牌下面第 3 章的代码里我会保留这个替换入口。3. 把源码跑起来文件结构、核心代码与最小可用配置3.1 文件清单与目录结构先把目录组织好。下面是我常用的最小文件集四个文件就够了不需要构建工具不需要任何依赖文件职责现场通常要改吗index.html页面骨架放标题、奖项区、抽奖按钮、名单展示区基本不改style.css布局、走马灯动效、大屏适配按年会主题色改app.js名单解析、洗牌、抽奖、持久化、导出核心逻辑所在names.txt每行一个参与者姓名或工号必改换成当届名单目录结构就是一个普通文件夹我习惯命名成 lottery-staticlottery-static/ ├── index.html ├── style.css ├── app.js └── names.txt文件夹直接放桌面就可以不要放进系统盘权限高的目录免得现场读文件弹权限提示。所有 js/css 用相对路径引用这样整个文件夹拷到任何一台电脑都能原样运行不用改一行配置。3.2 名单导入与奖项配置一个 HTML 页面怎么管理入场数据名单导入的无后台方案常见有两种第一种直接把名单写进 JS 数组变量适合几十人的小团队第二种维护一个 names.txt页面加载时读取。我本人倾向于第三种——页面文本框粘贴因为年会前名单经常改到最后一刻改 txt 文件还得刷新页面而粘贴直接在页面上完成所见即所得改完立即生效。奖项配置放在 JS 文件顶部结构就是一个对象数组// 奖项配置现场只需要改这里 const PRIZES [ { name: 三等奖, count: 10 }, { name: 二等奖, count: 3 }, { name: 一等奖, count: 1 }, ]; // 名单解析支持每行一个名字忽略空行和首尾空格 function parseNames(text) { return text .split(/\r?\n/) // 兼容 Windows 与 macOS/Linux 换行 .map((line) line.trim()) .filter((line) line.length 0); }逻辑说明PRIZES 数组的顺序就是现场抽奖的顺序按三等奖、二等奖、一等奖排列符合大多数年会的节奏如果你需要先抽大奖再抽小奖直接调整数组顺序即可。count 表示这一轮抽几个人会被用来控制洗牌后取出的人数。parseNames 函数把多行文本拆成数组关键在于用\r?\n同时兼容两种换行符并过滤掉空行避免复制 Excel 名单时带进来的空行被当成一个“人”。参数说明count 必须小于等于剩余人数否则在抽奖函数里会弹窗提醒。如果名单里有重名parseNames 不过滤重复重复会被当成两个人这通常符合“重名但工号不同”的现实如果业务上不允许重名你可以加一句return [...new Set(names)]去重。另外建议在活动前用一组测试名单把 PRIZES 的 count 总和比对一遍如果总人数小于总奖项数趁早调整别等上台才发现人不够。3.3 抽奖核心逻辑抽取、去重、状态恢复的 JS 实现这是整个方案最核心的代码块。先说状态模型我用一个 state 对象保存剩余池、中奖记录和当前轮次三个字段全部塞进 localStorage确保现场任何一次刷新都能恢复。const STORAGE_KEY lottery_static_v1; const state { remainingPool: [], // 还没被抽中的人洗牌后有序 winners: [], // 已中奖记录含轮次、奖项、姓名 currentRound: 0, // 当前抽到第几轮从 0 开始 }; // 原地洗牌Fisher-Yates 变体 function shuffle(arr) { for (let i arr.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [arr[i], arr[j]] [arr[j], arr[i]]; } return arr; } // 从剩余池里顺序取 count 个洗过牌后天然不重复 function drawFromPool(count) { if (state.remainingPool.length count) { alert(剩余可抽人数不足仅剩 ${state.remainingPool.length} 人); return []; } // remainingPool 在加载时已经洗过牌这里按顺序取即可 return state.remainingPool.slice(0, count); } // 立即把状态写入 localStorage function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } // 页面启动时恢复状态 function loadState() { const raw localStorage.getItem(STORAGE_KEY); if (raw) { Object.assign(state, JSON.parse(raw)); } } // 执行一键抽奖从当前奖池取人记录并落盘 function onDraw() { const prize PRIZES[state.currentRound]; if (!prize) { alert(所有奖项已抽完); return; } const result drawFromPool(prize.count); if (result.length 0) return; result.forEach((name) { state.winners.push({ round: state.currentRound, prize: prize.name, name: name, }); }); const winnerSet new Set(result); state.remainingPool state.remainingPool.filter((name) !winnerSet.has(name)); state.currentRound 1; saveState(); render(); // 把获奖名单渲染到页面 DOM具体实现随页面结构而定 }逻辑说明洗牌只需要在页面加载时执行一次所有轮次共享洗牌后的 remainingPool每轮只取前 count 个抽完立刻把这些人从池子里移出。这样从概率上保证了一个人最多中一次而且每一轮的抽取概率对剩余所有人均等。特别要注意 drawFromPool 用的是 slice(0, count)不是 splice因为 splice 会修改原数组顺序而我们希望 remainingPool 一直保持“剩余池顺序”这样下一轮还能接着取移除中奖者的动作统一放在 onDraw 里用 filter 做两步分开逻辑不绕。参数说明STORAGE_KEY 建议带版本号和场次标识比如改成lottery_2024_annual_v1避免同一台电脑上往年年会的数据残留干扰本届。Object.assign 用来把 localStorage 读出的 JSON 对象合并回 state这样以后加字段旧数据读取时不会因为缺字段抛异常。alert 在演示版里够用正式场合我一般换成页面内嵌提示条否则主持人误点回车会弹窗打断节奏。3.4 让手机也能投屏局域网静态服务的启动命令无后台方案最容易被忽略的一步是如果你想在现场让手机或其他电脑访问同一个抽奖页面不能双击 index.html 用 file:// 打开因为那样手机根本访问不到文件。常见做法是在文件夹里起一个极轻量的本地静态服务以 Python 为例# 在 lottery-static 目录下执行 python3 -m http.server 8080 --bind 0.0.0.0启动后电脑和手机连同一个 Wi-Fi在手机浏览器里输入“电脑局域网 IP:8080”就能打开。查看电脑局域网 IPWindows 用 ipconfigmacOS 和 Linux 用 ifconfig 或 ip addr。8080 端口如果被占用就换 8899换端口时手机访问地址的端口也要同步换。这里只在局域网内提供静态文件不涉及任何对外暴露适合年会这种临时场景。参数说明--bind 0.0.0.0 表示监听所有网卡如果漏掉这个参数服务可能只对 127.0.0.1 生效手机自然打不开。地址栏的 IP 一定要填电脑的局域网 IP不是 127.0.0.1。提示如果手机打开一直转圈优先检查电脑防火墙是否拦截该端口以及路由器是否开启了 AP 隔离这两个坑我在第 4 章展开。4. 年会现场常用避坑刷新丢名单、重复中奖、手机连不上4.1 页面一刷新中奖名单全没了现象抽奖抽到第二轮手滑按了 F5页面重载后名单恢复成初始状态中奖记录全丢。原因很多简单实现只把“参与名单”放进了 HTML 或 JS 变量从未写入 localStorage。页面刷新后变量被销毁一切归零。另一种隐藏原因是只保存了名单没保存 currentRound刷新后名单还在但轮次回到第一轮相当于让已经抽过的人再抽一次。解决把 state 整体序列化后写入 localStorage启动时按 3.3 里的 loadState 恢复。特别注意 currentRound、剩余池、中奖记录三者必须同时落盘缺一个都会造成“数据还在但流程错乱”。我还会在 onDraw 里每轮末尾强制 saveState抽奖这种低频动作不会带来性能问题但换来的是足够强的现场安全感。4.2 同一轮次有人抽了两次现象第二轮抽奖结果里出现了第一轮已经中奖的人主持人和观众当场愣住。原因抽奖逻辑写成了每轮都从“全员数组”里重新随机而不是从“剩余池”里抽。即便用了洗牌如果你每轮都洗一遍原数组上一轮中奖的人没有被移出重复无法避免。解决明确维护一个 remainingPool 字段每轮抽完后用 filter 把中奖者移除下一轮只操作这个池也就是 3.3 里的这一行state.remainingPool state.remainingPool.filter((name) !winnerSet.has(name));这一行一定要放在 saveState() 之前先改内存再落盘。如果你在调试时看到中奖名单正常但剩余池没变优先检查是不是把这行注释掉了。现场遇到这种情况不用慌剩余池里把重复中奖的人移除重新抽下一轮即可页面上的已中奖记录不会受影响。4.3 手机死活打不开电脑上的页面现象手机扫了现场投屏上的二维码浏览器转圈半分钟最后提示无法访问。原因这类问题 90% 出在三个地方一是电脑防火墙拦截了 8080 端口的入站连接二是手机连的 Wi-Fi 跟电脑不在同一网段年会场地常见“员工 Wi-Fi”和“访客 Wi-Fi”物理隔离三是服务启动时没带 --bind 0.0.0.0只监听了本机回环地址。解决按顺序排查。先在本机浏览器访问 http://127.0.0.1:8080 确认服务本身正常再用手机浏览器访问电脑的局域网 IP如果本机能开手机不能开检查防火墙放行对应端口如果两台设备显示连同一个 Wi-Fi 还是不通大概率是 AP 隔离换到同一接入点或直接让手机开电脑的移动热点。顺便说一句我一般会在抽奖前半小时用手机实际点一遍页面而不是到开场前才测。4.4 大屏动画卡顿抽奖点下去没反应现象本地笔记本测试流畅接到投影后点抽奖按钮要等一两秒才出人中奖名单显示一顿一顿。原因投影仪分辨率高但刷新率低加上老笔记本的核显性能有限而页面如果用了大量 DOM 节点动画、大尺寸背景图或一次性渲染所有参与者名单CPU 会瞬间跑满。解决把动效从“所有参与者一起滚动”改成“只滚动当前中奖者或当前候选窗口”背景图在上场前压缩成 WebP 或 JPG宽度控制在 1920 以内动画属性尽量使用 transform 和 opacity避免动画 left/top 这类会触发重排的属性。如果真的还会卡就在代码入口加一个 lowPerf 开关把所有粒子特效直接关掉保证抽奖核心动作不卡顿。现场演示的数据永远比特效重要。4.5 活动结束想要 Excel 版中奖名单现象活动结束后领导要一份中奖名单现场嘉宾用手机拍了屏幕整理时还发现漏了一个名字。原因无后台方案没有任何服务端日志中奖记录只存在于浏览器 localStorage 里不主动导出就等于没有记录。解决在页面里内置一个“导出 CSV”按钮从 state.winners 生成 CSV 文件并下载。下面是我常用的导出函数function exportWinnersCSV() { const rows [ [轮次, 奖项, 姓名], ...state.winners.map((w) [${w.round 1}, w.prize, w.name]), ]; const csv rows .map((row) row.map((cell) ${cell}).join(,)) .join(\n); // 加 BOM 前缀避免 Excel 打开中文乱码 const blob new Blob([\ufeff csv], { type: text/csv;charsetutf-8; }); const a document.createElement(a); a.href URL.createObjectURL(blob); a.download 年会中奖名单.csv; a.click(); URL.revokeObjectURL(a.href); }逻辑说明CSV 的每一行用逗号分隔我给每个字段都加了双引号这样即使名单里有逗号或特殊字符也不会破坏列结构。文件头加 \ufeff 是专门用来告诉 Excel 这个文件是 UTF-8 编码不加的话中文大概率显示成乱码。导出按钮建议放在页面右上角活动一结束立刻点一下数据就有了官方留档。5. 进阶玩法键盘控制、名单一键导入与数据复用5.1 键盘与遥控器控制现场抽奖最怕主持人握着鼠标乱点误把弹窗点掉。我给页面加一个全局键盘监听空格键触发抽奖回车键跳到下一轮document.addEventListener(keydown, (e) { if (e.code Space !e.repeat) onDraw(); if (e.code Enter) nextRound(); });加!e.repeat是为了防止长按空格触发多次抽奖这个细节在现场特别重要。如果主持人用翻页笔把翻页笔的“下一个”键映射成回车整个抽奖过程就能彻底离开鼠标节奏会顺很多。5.2 名单从本地文件一键导入年会名单经常到开会前还在改反复粘贴文本容易出错。我习惯在页面上放一个文件选择框用 FileReader 读取 txt 文件直接喂给 parseNames关键在于 FileReader 的 onload 回调里拿到文件文本随后调用 parseNames 并重新洗牌初始化。txt 文件格式保持每行一个名字编码保存成 UTF-8避免从 Windows 记事本出来的 ANSI 编码导致中文乱码。这个功能让名单更新成本降到零也是无后台方案里最实用的补强。5.3 跨场次数据隔离与复用同一台电脑今年用完明年再用localStorage 里还留着去年数据容易串场。解决办法是把 STORAGE_KEY 带上场次标识比如lottery_2024_annual_v1每年年会换一个 key旧数据自然隔离。配合导出 CSV去年的记录还能继续留档新场次从零开始。我现在做这类小工具第一步就是先想清楚“这台电脑明年换个场次数据会不会互相污染”这个习惯是吃了亏才养成的。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询