:從數(shù)據(jù)清洗到動畫控制完整指南)
簡介這是一份面向前端開發(fā)者的JavaScript手機號批量滾動抽獎實現(xiàn)方案常用于年會、晚會、現(xiàn)場互動等活動場景解決的是在大量手機號中隨機且不重復(fù)產(chǎn)生中獎號碼的核心問題。資源以單個PDF文檔形式提供壓縮包大小約56KB文檔通過原理講解和關(guān)鍵代碼片段結(jié)合的方式說明了隨機索引生成、已選號碼去重、setInterval定時器控制滾動、每輪中獎人數(shù)限制以及由后臺系統(tǒng)最終確認中獎號碼以確保公平性等實現(xiàn)要素。內(nèi)容中還展示了包含獎項等級、中獎人數(shù)、金額設(shè)置項的頁面界面示意。示例代碼使用數(shù)組加includes方法規(guī)避重復(fù)邏輯清晰便于讀者遷移到自己的項目中。目前已有368人學(xué)習(xí)瀏覽適合想快速理解手機號批量滾動抽獎并動手實現(xiàn)類似功能的開發(fā)者。1. JS手機號批量滾動抽獎為什么說它不只是「讓數(shù)字滾起來」活動現(xiàn)場最讓人捏把汗的往往不是網(wǎng)絡(luò)而是那塊大屏上的號碼能不能正常滾動、停下來時會不會出錯。JS手機號批量滾動抽獎實現(xiàn)代碼解決的是運營把一批手機號粘貼進來、點一下開始、屏幕滾動刷號、喊停落定、再導(dǎo)出中獎名單這一整套流程。它不復(fù)雜但細節(jié)非常多號碼可能帶著空格、橫線甚至科學(xué)計數(shù)法狀態(tài)一亂會連續(xù)開獎后臺標(biāo)簽頁切走動畫就停最后導(dǎo)出的 CSV 還可能亂碼。這套代碼適合三類人前端要搭內(nèi)部活動頁的開發(fā)運營想用靜態(tài)頁面救場的非技術(shù)同學(xué)以及想搞明白滾動抽獎前端實現(xiàn)的新手。讀完這一篇你能從空白的 HTML 文件一路寫到能上臺抽獎的完整版本。2. 手機號怎么進抽獎池粘貼解析、去重過濾與批量校驗先做一個最小頁面把入口定下來。我一般用三個按鈕加一個大屏區(qū)域?qū)搿㈤_始、停止中間一塊顯示號碼的 screen底部放中獎列表。代碼結(jié)構(gòu)不必酷炫活動頁面求的是穩(wěn)。div idapp textarea idrawInput rows6 placeholder粘貼手機號每行一個支持逗號、分號、空格分隔/textarea button idimportBtn導(dǎo)入號碼/button button idstartBtn disabled開始/button button idstopBtn disabled停止/button div idscreen--/div ul idwinners/ul button idexportBtn disabled導(dǎo)出中獎名單/button /div這里有個細節(jié)開始和停止按鈕初始是 disabled 的只有導(dǎo)入號碼后才放開開始只有滾動中才放開停止。這個習(xí)慣能擋住一多半誤操作。下面把導(dǎo)入邏輯寫出來它決定了后面所有環(huán)節(jié)的數(shù)據(jù)質(zhì)量。2.1 從粘貼文本里解析手機號一條正則和它的四個邊界條件運營從 Excel 或通訊錄里復(fù)制出來的文本格式永遠比想象中亂有每行一個的有用逗號、分號、頓號分隔的還有中間帶橫線和空格的。我一般先用一條分隔正則切分再做清洗。const rawInput document.getElementById(rawInput); const importBtn document.getElementById(importBtn); const screenEl document.getElementById(screen); let pool []; let winners []; function parsePhones(text) { const parts text.split(/[\s,;、\n]/); const phones []; const seen new Set(); for (const part of parts) { const p part.replace(/[-\s]/g, ); if (/^1[3-9]\d{9}$/.test(p) !seen.has(p)) { seen.add(p); phones.push(p); } } return phones; } importBtn.addEventListener(click, () { const phones parsePhones(rawInput.value); if (phones.length 0) { screenEl.textContent 沒有解析到有效手機號請檢查輸入格式; return; } pool phones.slice(); winners []; screenEl.textContent 已導(dǎo)入 ${pool.length} 個號碼; });分隔正則里的\s覆蓋空格、Tab 和換行再把中英文逗號、分號、頓號都列進去基本能兼容主流粘貼格式。replace(/[-\s]/g, )處理的是138-1234-5678這類帶橫線的號碼先把橫線去掉再校驗否則會被攔在外面。正則^1[3-9]\d{9}$是 11 位手機號的常見校驗第二位限制在 3 到 9能覆蓋當(dāng)前主流號段同時過濾掉一長串?dāng)?shù)字的座機或亂粘貼的文本。最后用Set去重重復(fù)號碼只保留第一個。這里要特別提醒如果粘貼進來的內(nèi)容已經(jīng)變成1.38123E10這種科學(xué)計數(shù)法前端是救不回來的。Excel 默認把長數(shù)字顯示為科學(xué)計數(shù)法復(fù)制出來的就是顯示值低位數(shù)字已經(jīng)丟了一部分。遇到這種情況只能讓運營回到 Excel 里把那一列改成文本格式再重新復(fù)制沒有后悔藥。2.2 導(dǎo)入時的三個默認動作去重、過濾空行、按來源保留順序解析函數(shù)其實已經(jīng)同時做了三件事去重、過濾空行、過濾非手機號文本。保留順序這一點容易被忽略它其實很重要。運營拿到的名單往往有排序意義比如按部門、按報名時間排好解析時從頭到尾 push 進數(shù)組保持了原始順序繪制大屏?xí)r不按這個順序顯示但導(dǎo)入預(yù)覽和中獎導(dǎo)出會沿用這個順序運營核對起來更方便。有些做法會在解析后調(diào)用Array.sort把號碼排序我建議不要。排序會讓同樣的三個號在不同的輸入順序下產(chǎn)生完全相同的展示序列反而容易讓觀眾覺得「每次刷過去的順序都一樣」?!缚雌饋黼S機」不僅取決于抽樣算法也取決于基礎(chǔ)數(shù)組的順序是否自然保留原始順序是最省事的。導(dǎo)入預(yù)覽可以做一層校驗讓運營在抽獎之前看到有效號碼數(shù)和中獎列表占用情況function renderPreview(phones) { const preview document.getElementById(preview); if (!preview) return; preview.textContent 解析成功 ${phones.length} 個號碼已自動去重、過濾空行和非 11 位手機號; }如果號碼數(shù)量少于 10 個我會直接在導(dǎo)入后提示「號碼太少滾動效果會很差建議至少 20 人參與」。這不是代碼問題是現(xiàn)場體驗問題號碼池只有 5 個時滾動屏翻來覆去就那幾個號觀眾一眼就能看出規(guī)律抽獎的緊張感就沒有了。2.3 沒有 Excel 也能先跑通內(nèi)置測試數(shù)據(jù)與導(dǎo)入預(yù)覽開發(fā)階段最怕的就是手上沒有真實號碼數(shù)據(jù)。我習(xí)慣在頁面里放一個「填入測試數(shù)據(jù)」按鈕點一下自動生成 50 個假手機號。這樣不用打開 Excel不用問運營要名單就能把滾動、停止、導(dǎo)出整條鏈路跑一遍。function fillDemoData() { const demo Array.from({ length: 50 }, (_, i) { const suffix String(i).padStart(4, 0); return 1381234${suffix}; }); rawInput.value demo.join(\n); screenEl.textContent 已填入 ${demo.length} 個測試號碼請點擊導(dǎo)入; }padStart(4, 0)保證從13812340000到13812340049都是完整的 11 位。測試數(shù)據(jù)要避免真的手機號段被抽到真實用戶頭上所以固定用1381234前綴的虛構(gòu)號碼。等到彩排時再換成運營提供的脫敏或者測試名單。這個內(nèi)置測試數(shù)據(jù)還有個額外用途給新人做演示。有一次活動前運營臨時要看看效果整個流程從打開頁面到跑完一輪不到一分鐘靠的就是這個按鈕。不要把 Demo 數(shù)據(jù)當(dāng)成正式數(shù)據(jù)留著正式活動導(dǎo)入前會先清空 textarea免得兩個名單混在一起。3. 滾動動畫與抽取邏輯讓屏幕快速刷號又要停得讓人信服導(dǎo)入號碼之后核心問題變成兩塊屏幕上的號碼怎么滾以及最終停在中獎?wù)呱砩系哪且凰查g邏輯上怎么保證公平。這兩件事要分開想否則代碼寫出來容易變成「滾動是隨機停了再隨機一次」導(dǎo)致視覺和中獎結(jié)果脫節(jié)。活動場景下觀眾對滾動動畫的預(yù)期是開始后號碼快速刷新主持人喊停后速度明顯慢下來最后定格的號碼就是中獎?wù)摺H绻V顾查g才用Math.random抽取那滾動過程就只是裝飾談不上公平性信任問題如果滾動過程中已經(jīng)確定好中獎?wù)咧皇峭ㄟ^減速把它展示出來邏輯上更完整。所以我傾向于在點擊「開始」的那一刻就鎖定本輪的 index滾動只是把最終結(jié)果「演」出來。3.1 刷號動畫用 requestAnimationFramesetInterval 的問題在哪滾動刷號最直接的寫法是setInterval每 60ms 換一個號碼。它也能跑但有兩個問題一是瀏覽器對定時器有嵌套調(diào)用限制二是后臺標(biāo)簽頁會把定時器降到每秒一次甚至?xí)和??;顒哟笃寥绻械絼e的窗口再切回來動畫節(jié)奏會變得很奇怪。requestAnimationFrame跟隨屏幕刷新率執(zhí)行通常 60Hz也就是每幀約 16.7ms。我們不每幀都換號而是記錄上次換號的時間真正按時間間隔刷新這叫「基于時間戳控制」。這樣即使某一幀卡頓后續(xù)也不會累積偏差。const FAST_GAP 60; const STOP_DURATION 2600; const MIN_GAP 60; const MAX_GAP 220; let mode idle; let finalIndex -1; let lastShownIndex -1; let lastUpdateTime 0; let stopStartTime 0; let frameId null; function mask(phone) { return phone.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); } function updateScreen() { let idx; do { idx Math.floor(Math.random() * pool.length); } while (idx lastShownIndex pool.length 1); lastShownIndex idx; screenEl.textContent mask(pool[idx]); }代碼里do...while防止同一個號碼連續(xù)出現(xiàn)在大屏上。Math.random抽中同一個 index 的概率約 1/N號碼池小的時候幾乎必然發(fā)生觀眾直接會喊「假的吧」。重復(fù)抽一次雖然不能保證全局不重復(fù)但能消除最刺眼的連續(xù)重復(fù)。mask把手機號中間四位換成星號大屏上只顯示138****5678這是活動場景下的隱私習(xí)慣。3.2 從全部號碼里抽當(dāng)前顯示號中獎?wù)弑仨殢氖S喑刈永镞x滾動顯示過程中updateScreen是從整個pool里隨機取一個來做展示。這里有個容易踩坑的邊界如果pool只有 1 個號do...while會死循環(huán)。代碼里的pool.length 1條件就是為了擋住這個邊界。如果只剩最后一個人那他就是本輪中獎?wù)咧苯诱故炯纯伞,F(xiàn)在看核心的中獎鎖定邏輯。點擊「開始」的那一刻我們用Math.floor(Math.random() * pool.length)鎖定一個下標(biāo)這個下標(biāo)對應(yīng)的就是本輪中獎?wù)?。之后整個過程里無論是快速滾動還是減速停止真正的結(jié)果都已經(jīng)是這個下標(biāo)。誰也不知道這個下標(biāo)對應(yīng)哪個號碼因為pool有幾十上百個元素操作員從屏幕上也看不出規(guī)律所以它仍是一個公平的黑匣子。function startRoll() { if (mode ! idle || pool.length 0) return; finalIndex Math.floor(Math.random() * pool.length); mode running; lastShownIndex -1; lastUpdateTime performance.now(); frameId requestAnimationFrame(rollLoop); } function finishRoll() { const winner pool.splice(finalIndex, 1)[0]; winners.push(winner); screenEl.textContent mask(winner); renderWinners(); mode idle; }finishRoll里做兩件事把pool[finalIndex]從池中移除push 進winners。splice之后的數(shù)組長度少了 1下一輪抽獎無論怎么隨機都不會再抽到這個人。這是「中獎不重復(fù)」的關(guān)鍵比每次去比較 winners 列表要可靠得多。那滾動過程中updateScreen也會隨機到pool[finalIndex]這個號碼也就是最終中獎?wù)邥崆俺霈F(xiàn)在屏幕上幾次。這沒問題反而增加了真實性。如果滾動過程中絕不出現(xiàn)中獎?wù)叨械娜艘谎劬椭肋@輪是誰懸念就沒了。3.3 滾動速度三段式快速掃描、減速逼近、鎖定定格滾動速度的設(shè)計直接決定現(xiàn)場氣氛。我習(xí)慣分成兩段點開始后保持恒定快速刷新點停止后進入一段 2.6 秒的減速區(qū)減速區(qū)里刷新間隔從快到慢最終定格。function stopRoll() { if (mode ! running) return; mode stopping; stopStartTime performance.now(); } function rollLoop(timestamp) { if (mode running) { if (timestamp - lastUpdateTime FAST_GAP) { updateScreen(); lastUpdateTime timestamp; } frameId requestAnimationFrame(rollLoop); } else if (mode stopping) { const progress Math.min((timestamp - stopStartTime) / STOP_DURATION, 1); const gap MIN_GAP (MAX_GAP - MIN_GAP) * progress; if (timestamp - lastUpdateTime gap) { updateScreen(); lastUpdateTime timestamp; } if (progress 1) { finishRoll(); } else { frameId requestAnimationFrame(rollLoop); } } }progress從 0 到 1 線性增長gap也隨之從 60ms 增長到 220ms。60ms 時每秒刷新約 16 次220ms 時每秒只剩約 4 次人眼能明顯感覺到號碼越滾越慢。最后progress 1時直接調(diào)用finishRoll屏幕上定格的就是最終中獎?wù)?。這里的關(guān)鍵參數(shù)是STOP_DURATION2.6 秒是反復(fù)試出來的范圍太短顯得倉促太長觀眾等著著急。如果主持人習(xí)慣喊「3、2、1」可以把它調(diào)到 3 秒。如果希望停止后立刻出結(jié)果可以縮短到 1.5 秒但減速會不夠明顯少了懸念感。還有一個細節(jié)stopRoll在stopping狀態(tài)下再次點擊不會生效這避免了現(xiàn)場緊張時連點按鈕導(dǎo)致狀態(tài)錯亂。4. 開始停止批量開獎把按鈕、狀態(tài)機和結(jié)果導(dǎo)出串起來滾動邏輯寫好后按鈕和狀態(tài)管理是另一塊翻車高發(fā)區(qū)。很多同學(xué)把代碼寫成「點擊開始執(zhí)行一個函數(shù)點擊停止執(zhí)行另一個函數(shù)」看起來能跑但一旦快速連點、批量抽獎、抽完再抽狀態(tài)就亂了。我習(xí)慣先用狀態(tài)機把整個生命周期定死再寫事件綁定。4.1 抽獎按鈕只有三個狀態(tài)卻有四個先畫狀態(tài)機再寫事件完整的狀態(tài)不是「開始」和「停止」兩個而是 idle、running、stopping 三種互相制約的狀態(tài)。idle 可以開始running 可以停止stopping 不可打斷定格后自動回到 idle。還有一個隱性狀態(tài)是「無號碼」但這種情況連開始按鈕都不會放開所以不需要單列。const startBtn document.getElementById(startBtn); const stopBtn document.getElementById(stopBtn); const exportBtn document.getElementById(exportBtn); function updateButtons() { startBtn.disabled mode ! idle; stopBtn.disabled mode ! running; importBtn.disabled mode ! idle; exportBtn.disabled winners.length 0; } startBtn.addEventListener(click, () { startBatch(1); }); stopBtn.addEventListener(click, stopRoll); function renderWinners() { const winnersEl document.getElementById(winners); winnersEl.innerHTML winners .map((phone, i) li${i 1}. ${mask(phone)}/li) .join(); updateButtons(); }updateButtons根據(jù)當(dāng)前 mode 控制三個按鈕的可用性這樣從 UI 上就杜絕了連點。導(dǎo)入按鈕在抽獎過程中也被禁用防止有人中途把號碼池換掉。mask展示是為了投屏安全但要注意結(jié)果列表如果只顯示掩碼運營無法直接核對完整號碼所以導(dǎo)出 CSV 時必須導(dǎo)出完整手機號這個區(qū)分要想清楚。4.2 批量抽獎的實現(xiàn)一次抽多個號碼還要保證不重復(fù)活動里經(jīng)常有「三等獎 5 名」這種批量需求。最簡單粗暴的做法是一次性從 pool 里隨機取 5 個但這失去了滾動抽獎的儀式感。我一般把批量實現(xiàn)成「排隊逐輪執(zhí)行」每輪走一遍完整的滾動、停止、定格流程自動進入下一輪。這樣大屏上始終只有一個號碼在滾節(jié)奏和單抽一樣。let batchQueue 0; function startBatch(count) { if (mode ! idle || pool.length 0) return; batchQueue count; runNextBatch(); } function runNextBatch() { if (batchQueue 0) { batchQueue--; startRoll(); } } function finishRoll() { const winner pool.splice(finalIndex, 1)[0]; winners.push(winner); screenEl.textContent mask(winner); renderWinners(); mode idle; if (batchQueue 0) { runNextBatch(); } }finishRoll在推入中獎名單后檢查隊列如果還有剩余輪次就繼續(xù)startRoll。因為每輪結(jié)束后pool已經(jīng)少了中獎?wù)呦乱惠喸俅坞S機時不會抽到重復(fù)號碼。這就是批量不重復(fù)的機制不需要額外的if判斷或臨時數(shù)組。這里有個邊界如果請求抽 5 個但 pool 里只剩 2 個怎么辦startBatch只檢查了 pool 非空沒檢查數(shù)量。常見的做法是在runNextBatch里加一層判斷號碼不足時直接結(jié)束隊列并提示。function runNextBatch() { if (batchQueue 0 || pool.length 0) { batchQueue 0; if (pool.length 0) screenEl.textContent 號碼已全部抽完; return; } batchQueue--; startRoll(); }這個處理比在彈窗里阻止更平靜抽到哪算哪剩余的人自動不參與名單照常導(dǎo)出。4.3 中獎結(jié)果收集與 CSV 導(dǎo)出讓運營可以直接拿走名單中獎名單最終要給運營他們最習(xí)慣用的是 Excel。瀏覽器導(dǎo)出 CSV 是最省事的路徑但有兩個坑中文或特殊字符亂碼以及大屏掩碼被導(dǎo)出去。我統(tǒng)一在導(dǎo)出時寫完整手機號并在 CSV 頭部加 UTF-8 BOM。function exportCsv() { const header 序號,手機號,中獎時間\n; const rows winners.map((phone, index) { const time new Date().toLocaleString(zh-CN); return ${index 1},${phone},${time}; }); const csv \uFEFF header rows.join(\n); const blob new Blob([csv], { type: text/csv;charsetutf-8 }); const a document.createElement(a); a.href URL.createObjectURL(blob); a.download 中獎名單.csv; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } exportBtn.addEventListener(click, exportCsv);\uFEFF是 Byte Order MarkExcel 打開 CSV 時用它識別 UTF-8 編碼不加的話中文列名和備注大概率亂碼。document.body.appendChild(a)再移除這一步老版本瀏覽器里不把a掛進 DOM 就觸發(fā)不了下載加上沒有壞處。導(dǎo)出按鈕在 winners 為空時是 disabled 的防止運營導(dǎo)出空文件。到這個階段一個可用的頁面已經(jīng)成型。剩下的問題不是功能缺失而是邊界條件——下面這章集中寫我踩過最深的幾個坑。5. 手機號滾動抽獎避坑排查號碼顯示、隨機性、掉幀與重復(fù)開獎功能寫完不等于能上活動。以下幾條都是真實出現(xiàn)過的問題按「現(xiàn)象、原因、解決」的方式記錄。每一條背后都是現(xiàn)場的血淚經(jīng)驗。5.1 號碼顯示不全的坑空格、橫線和科學(xué)計數(shù)法混入現(xiàn)象是屏幕上的號碼經(jīng)常短一截比如138123 45678顯示成1381234567或者直接顯示1.38E10。原因是 Excel 默認處理長數(shù)字復(fù)制時要么把空格去掉要么把超過 11 位的數(shù)字轉(zhuǎn)成科學(xué)計數(shù)法粘貼進 textarea 后已經(jīng)不是原始的 11 位文本。解決分兩層。第一層是解析時做replace(/[-\s]/g, )去掉橫線和空格第二層是在導(dǎo)入預(yù)覽里直接提示號碼數(shù)量如果數(shù)量比 Excel 里的行數(shù)少很多立即檢查源頭格式。科學(xué)計數(shù)法一旦出現(xiàn)就無解因為丟失的位數(shù)是永久性的唯一的辦法是調(diào)整 Excel 列格式為文本后重新復(fù)制。我后來每次都把這條寫進活動前的郵件里讓運營提前轉(zhuǎn)文本省得現(xiàn)場手忙腳亂。5.2 滾動抽獎「看起來不隨機」的真相連續(xù)重復(fù)和固定序列現(xiàn)象是彈幕里有人刷「怎么又是這個號」。從概率上看號碼池 50 人隨機滾動時同一個號碼短時間內(nèi)再次出現(xiàn)是完全正常的概率約 1/50。但觀眾不認概率只看觀感。而且如果滾動更新邏輯每幀都從數(shù)組頭部取號或者隨機源寫法寫成了Math.random() * pool.length忘記取整展示序列就會有明顯規(guī)律。解決手段有兩層。代碼層updateScreen里用do...while避免連續(xù)兩次顯示同一個號。流程層導(dǎo)入時保留原始順序而不排序讓每次滾動的起始序列不同。還有一個容易忽略的點多個瀏覽器標(biāo)簽頁同時打開同一個頁面每個頁面的滾動序列完全不同主持人看到的是當(dāng)前頁面的滾動導(dǎo)出的卻是另一頁的中獎結(jié)果兩頭對不上這是另一種「看起來不隨機」?;顒忧爸槐A粢粋€標(biāo)簽頁或者鎖定大屏頁面不被誤開。5.3 動畫掉幀與頁面卡住后臺標(biāo)簽頁的定時器限制現(xiàn)象是抽獎進行中切到別的窗口回來發(fā)現(xiàn)屏幕已經(jīng)定格或者停在半路不再滾動。原因是requestAnimationFrame在后臺標(biāo)簽頁會被瀏覽器暫停performance.now()在切回時會瞬間跨過很大一段時間progress直接變成 1finishRoll被立即觸發(fā)。解決有兩個方向。第一個方向是接受現(xiàn)實在visibilitychange里取消當(dāng)前滾動把狀態(tài)重置為 idle本次抽獎作廢重新開始。這樣最安全不會在沒人操作的情況下偷偷開獎。document.addEventListener(visibilitychange, () { if (document.hidden mode ! idle) { cancelAnimationFrame(frameId); mode idle; screenEl.textContent 頁面切走本次抽獎已取消請重新開始; updateButtons(); } });第二個方向是活動開始前把大屏頁面設(shè)為焦點窗口并鎖掉多窗口誤操作?,F(xiàn)場設(shè)備如果不夠熟悉我建議兩個都用。要注意如果finishRoll已經(jīng)把中獎?wù)邔戇M winners這個取消只影響當(dāng)前未完成的一輪已中獎名單不會丟失。5.4 中獎名單重復(fù)的坑連點、刷新和多開窗口現(xiàn)象是第二輪抽出來的號碼和第一輪重復(fù)或者刷新頁面后曾經(jīng)中獎的人又出現(xiàn)在號碼池。原因有三個中獎?wù)邲]有從 pool 移除、刷新導(dǎo)致內(nèi)存里的 pool 重置、多個標(biāo)簽頁各自維護一份 pool。第一點靠splice解決。后面兩點需要做一層本地持久化。前端沒有數(shù)據(jù)庫但至少可以把已中獎名單寫進localStorage導(dǎo)入號碼時自動過濾掉這些號碼。const STORAGE_KEY draw_winners_v1; function saveWinners() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(winners)); } catch (e) {} } function loadWinners() { try { winners JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } catch (e) { winners []; } } function importFiltered(phones) { const used new Set(winners); pool phones.filter(p !used.has(p)); }localStorage只能擋住同一瀏覽器里的刷新和多開頁面跨設(shè)備或多人同時操作時它并不安全。如果活動獎品比較正式中獎名單必須以服務(wù)端為準(zhǔn)前端只負責(zé)展示。這個邊界要在需求階段跟運營講清楚別讓前端代碼去承擔(dān)它扛不起的事務(wù)一致性。我習(xí)慣在finishRoll里加一行斷言console.assert( !pool.includes(winner), 中獎號碼仍在抽獎池中邏輯錯誤 );斷言在開發(fā)階段幫了大忙有一次我重構(gòu)批量邏輯時把splice寫成了slice就是靠它發(fā)現(xiàn)的。上線活動時開著控制臺跑幾輪比肉眼檢查可靠得多。6. 讓這個 Demo 真正上線活動參數(shù)表、驗證方法與驗收習(xí)慣前面幾章把功能寫出來了但一臺活動電腦上的運行表現(xiàn)和開發(fā)機往往差別很大。最后把可調(diào)參數(shù)和驗收步驟整理成一張表這算是從 Demo 到上線之間最后一道工序。參數(shù)推薦范圍說明FAST_GAP50~80ms快速滾動期的刷新間隔太快看不清太慢沒緊張感STOP_DURATION2200~3200ms點停止到定格的總時長正常活動建議 2600msMIN_GAP / MAX_GAP60 / 220ms減速期刷新間隔的上下限末期每秒約 4 次刷新號碼池數(shù)量至少 20 個少于 10 個時滾動幾乎看不出效果單輪批量上限1~10 個超過 10 個建議改為連續(xù)多輪而不是一屏多號參數(shù)不是越大越好。FAST_GAP調(diào)到 20ms 時屏幕反而閃得讓人頭暈STOP_DURATION超過 4 秒觀眾會忍不住催。最佳做法是活動前用測試數(shù)據(jù)把參數(shù)固定下來現(xiàn)場不要再動。驗證方法我用兩步。第一步是隨機分布抽查在控制臺跑一段模擬確認隨機源沒有明顯偏向。真正的抽獎流程不需要跑 1000 次因為每輪都會從 pool 移除一個號碼分布驗證針對的是Math.random本身。function verifyRandom(times 10000) { const n 100; const counter new Array(n).fill(0); for (let i 0; i times; i) { counter[Math.floor(Math.random() * n)]; } const avg times / n; const max Math.max(...counter); const min Math.min(...counter); console.log(平均 ${avg} 次最大 ${max}最小 ${min}偏差 ${((max - min) / avg * 100).toFixed(1)}%); }偏差在 15% 以內(nèi)都算正常超過這個范圍說明隨機算法寫歪了。第二步是斷網(wǎng)試跑把所有 JS 和樣式放在本地文件里打開 DevTools 的網(wǎng)絡(luò)面板切到 Offline導(dǎo)入 50 個測試號連抽三輪導(dǎo)出 CSV檢查行數(shù)、重復(fù)和中文亂碼?;顒哟笃磷钆屡R時依賴外部資源斷網(wǎng)驗證能提前暴露所有本地化問題。我現(xiàn)在的固定習(xí)慣是正式活動前一晚用測試號碼把「導(dǎo)入 → 抽三輪 → 導(dǎo)出 CSV → 檢查重復(fù)」完整跑一遍然后關(guān)掉頁面重新打開再導(dǎo)入真實名單確認 localStorage 里恢復(fù)的中獎名單已經(jīng)把已抽中的人過濾掉。做完這一輪心里才有底。這套流程也推薦給你畢竟抽獎頁面是現(xiàn)場出錯成本最高的那類頁面寧可多花十分鐘準(zhǔn)備。希望幫到你。本文還有配套的精品資源點擊獲取