Demo源碼解析與二次開發(fā)指南)
簡(jiǎn)介這份資源是微信小程序投票系統(tǒng)的完整源碼包面向希望學(xué)習(xí)小程序開發(fā)或快速搭建投票應(yīng)用的開發(fā)者可用于意見收集、評(píng)選活動(dòng)與市場(chǎng)調(diào)研等場(chǎng)景。解壓后包含創(chuàng)建投票、發(fā)布投票、結(jié)果展示等核心頁(yè)面以及全局配置、自定義網(wǎng)絡(luò)請(qǐng)求模塊、數(shù)據(jù)模型、可復(fù)用組件與模擬數(shù)據(jù)便于理解小程序的架構(gòu)、頁(yè)面生命周期、網(wǎng)絡(luò)通信與數(shù)據(jù)可視化實(shí)現(xiàn)。資源共90個(gè)文件以49個(gè)wxss樣式、15個(gè)png圖片、8個(gè)js腳本、8個(gè)json配置、6個(gè)wxml結(jié)構(gòu)為主另含php接口與sql建表文件壓縮包約153KB目錄結(jié)構(gòu)清晰適合按模塊檢索學(xué)習(xí)。目前已有3644人學(xué)習(xí)下載讀者可據(jù)此掌握組件化開發(fā)、請(qǐng)求封裝與數(shù)據(jù)管理思路并在此基礎(chǔ)上擴(kuò)展登錄、權(quán)限控制與防刷機(jī)制。1. 從一份能跑通的投票 Demo 說起它到底解決了什么問題如果你正在找一個(gè)能直接跑起來的微信小程序投票系統(tǒng)大概率不是為了研究投票算法本身而是想快速驗(yàn)證一個(gè)想法能不能在微信里發(fā)起一次投票讓用戶點(diǎn)幾下就完成選擇后臺(tái)還能看到結(jié)果。這份「微信小程序投票系統(tǒng)創(chuàng)建投票發(fā)布demo完整源碼下載.zip」就是沖著這個(gè)場(chǎng)景去的——它把創(chuàng)建投票、發(fā)布投票、參與投票、查看結(jié)果這條最短鏈路完整地做了一遍前端用小程序原生框架頁(yè)面結(jié)構(gòu)以 HTML 風(fēng)格的 WXML 為主配合 WXSS 做樣式邏輯層走 JavaScript數(shù)據(jù)交互按小程序常見的wx.request方式對(duì)接后端接口。它適合兩類人一類是剛接觸小程序開發(fā)、想拿一個(gè)完整項(xiàng)目練手的新手照著跑一遍就能理解小程序從頁(yè)面到接口的完整流程另一類是有二次開發(fā)需求的前端或全棧開發(fā)者想在這個(gè) Demo 基礎(chǔ)上改成自己業(yè)務(wù)里的投票模塊比如活動(dòng)評(píng)選、意見收集、課程分組選擇等。源碼包里沒有過度封裝目錄結(jié)構(gòu)清晰改起來不會(huì)像拆黑匣子一樣無從下手。2. 項(xiàng)目結(jié)構(gòu)拆解從目錄到頁(yè)面先看清骨架再動(dòng)手拿到一個(gè)壓縮包最忌諱的就是直接雙擊打開然后亂翻。我一般會(huì)先看目錄層級(jí)判斷這個(gè)項(xiàng)目是「單頁(yè)堆邏輯」還是「按功能分模塊」。這份投票 Demo 的結(jié)構(gòu)屬于后者雖然體量不大但該分的目錄都分了對(duì)二次開發(fā)比較友好。2.1 根目錄與小程序配置文件解壓后你會(huì)看到類似下面的結(jié)構(gòu)不同版本可能略有差異但核心文件不會(huì)少vote-demo/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json ├── pages/ │ ├── index/ │ ├── create/ │ ├── detail/ │ └── result/ ├── utils/ │ └── request.js └── images/app.json是小程序的全局配置里面注冊(cè)了所有頁(yè)面路徑也決定了底部導(dǎo)航欄tabBar長(zhǎng)什么樣。這份 Demo 里通常會(huì)把「首頁(yè)」和「創(chuàng)建投票」放進(jìn) tabBar方便快速切換。project.config.json是開發(fā)者工具的配置里面記錄了 appid 占位、編譯模式等信息。你導(dǎo)入項(xiàng)目時(shí)如果提示 appid 不合法改成自己的測(cè)試號(hào)或者直接選「不使用云服務(wù)」的普通模式即可。utils/request.js是我比較在意的一個(gè)文件。很多 Demo 會(huì)把wx.request直接寫在頁(yè)面里改起來要滿項(xiàng)目搜。這份源碼把它抽了一層統(tǒng)一處理請(qǐng)求頭、baseUrl 和錯(cuò)誤提示二次開發(fā)時(shí)只需要改這一個(gè)文件就能切換后端地址。2.2 頁(yè)面目錄與職責(zé)劃分pages下每個(gè)文件夾對(duì)應(yīng)一個(gè)頁(yè)面通常包含四個(gè)文件.wxml、.wxss、.js、.json。以create頁(yè)面為例create.wxml負(fù)責(zé)表單結(jié)構(gòu)投票標(biāo)題輸入框、選項(xiàng)列表、添加選項(xiàng)按鈕、發(fā)布時(shí)間選擇器。create.js負(fù)責(zé)收集表單數(shù)據(jù)、校驗(yàn)必填項(xiàng)、調(diào)用創(chuàng)建接口。create.json一般只寫navigationBarTitleText用來設(shè)置頁(yè)面標(biāo)題。create.wxss處理表單樣式輸入框間距、按鈕圓角這些。detail頁(yè)面是投票詳情頁(yè)用戶在這里看到選項(xiàng)并點(diǎn)擊投票。result頁(yè)面展示票數(shù)統(tǒng)計(jì)通常用進(jìn)度條或百分比條來呈現(xiàn)。index是列表頁(yè)展示已發(fā)布的投票點(diǎn)擊進(jìn)入詳情。這種「一個(gè)功能一個(gè)頁(yè)面」的劃分方式好處是邏輯邊界清楚。你要加一個(gè)「我參與的投票」頁(yè)面只需要在pages下新建目錄然后在app.json的pages數(shù)組里注冊(cè)一行不會(huì)影響到現(xiàn)有頁(yè)面。2.3 數(shù)據(jù)流與接口約定小程序本身不提供數(shù)據(jù)庫(kù)所以這份 Demo 的投票數(shù)據(jù)一定依賴后端接口。源碼里通常會(huì)在utils/request.js里定義一個(gè)baseUrl比如http://localhost:3000/api然后頁(yè)面里調(diào)用類似request(/vote/create, POST, data)的方法。接口大致分四類接口路徑方法作用關(guān)鍵參數(shù)/vote/listGET獲取投票列表page, pageSize/vote/createPOST創(chuàng)建投票title, options, endTime/vote/detailGET獲取投票詳情voteId/vote/submitPOST提交投票voteId, optionId如果你沒有現(xiàn)成后端可以用微信云開發(fā)的云函數(shù)替代或者用 Mock 數(shù)據(jù)先把前端跑通。我一般會(huì)先用mockjs或者直接在request.js里寫死返回?cái)?shù)據(jù)確認(rèn)頁(yè)面渲染沒問題后再接真實(shí)接口。提示小程序正式環(huán)境要求請(qǐng)求域名必須備案并配置在后臺(tái)白名單里本地調(diào)試可以在開發(fā)者工具里勾選「不校驗(yàn)合法域名」。3. 核心功能實(shí)現(xiàn)創(chuàng)建投票與發(fā)布投票的代碼鏈路這一章是整份源碼里最值得逐行看的部分。創(chuàng)建投票和發(fā)布投票看似簡(jiǎn)單但涉及表單狀態(tài)管理、選項(xiàng)動(dòng)態(tài)增刪、時(shí)間格式處理、接口提交后的頁(yè)面跳轉(zhuǎn)每一環(huán)都有容易翻車的地方。3.1 創(chuàng)建投票頁(yè)的表單狀態(tài)管理先看create.js里數(shù)據(jù)初始化的部分Page({ data: { title: , options: [, ], endTime: , isSubmitting: false }, onTitleInput(e) { this.setData({ title: e.detail.value }); }, onOptionInput(e) { const index e.currentTarget.dataset.index; const options this.data.options; options[index] e.detail.value; this.setData({ options }); }, addOption() { if (this.data.options.length 10) { wx.showToast({ title: 最多10個(gè)選項(xiàng), icon: none }); return; } this.setData({ options: [...this.data.options, ] }); }, removeOption(e) { const index e.currentTarget.dataset.index; if (this.data.options.length 2) { wx.showToast({ title: 至少保留2個(gè)選項(xiàng), icon: none }); return; } const options this.data.options.filter((_, i) i ! index); this.setData({ options }); } });這里有幾個(gè)細(xì)節(jié)值得說。options初始給兩個(gè)空字符串保證用戶一進(jìn)頁(yè)面就能看到兩個(gè)輸入框而不是一個(gè)空白區(qū)域。addOption里做了上限判斷防止用戶無限添加導(dǎo)致頁(yè)面卡頓或后端字段超長(zhǎng)。removeOption用filter而不是splice是因?yàn)閟etData對(duì)數(shù)組的更新需要新引用才能觸發(fā)視圖刷新直接改原數(shù)組有時(shí)候會(huì)出現(xiàn)「數(shù)據(jù)變了但頁(yè)面沒變」的玄學(xué)問題。onOptionInput里用dataset.index來定位當(dāng)前編輯的是第幾個(gè)選項(xiàng)這是小程序列表輸入的常見做法。注意setData時(shí)傳的是整個(gè)options數(shù)組而不是options[index]因?yàn)樾〕绦驅(qū)β窂礁碌闹С钟邢拚w替換更穩(wěn)妥。3.2 發(fā)布投票的校驗(yàn)與提交表單填完后點(diǎn)擊「發(fā)布」按鈕會(huì)觸發(fā)提交邏輯submitVote() { const { title, options, endTime } this.data; if (!title.trim()) { wx.showToast({ title: 請(qǐng)輸入投票標(biāo)題, icon: none }); return; } const validOptions options.filter(item item.trim() ! ); if (validOptions.length 2) { wx.showToast({ title: 至少填寫兩個(gè)有效選項(xiàng), icon: none }); return; } if (this.data.isSubmitting) return; this.setData({ isSubmitting: true }); request(/vote/create, POST, { title: title.trim(), options: validOptions, endTime: endTime || null }).then(res { wx.showToast({ title: 發(fā)布成功, icon: success }); setTimeout(() { wx.navigateTo({ url: /pages/detail/detail?id${res.data.voteId} }); }, 800); }).catch(err { wx.showToast({ title: err.message || 發(fā)布失敗, icon: none }); }).finally(() { this.setData({ isSubmitting: false }); }); }這段代碼里isSubmitting是一個(gè)防重復(fù)提交的鎖。用戶手快連點(diǎn)兩下如果沒有這個(gè)標(biāo)記可能會(huì)創(chuàng)建出兩條一模一樣的投票記錄。validOptions過濾掉空選項(xiàng)避免后端收到空字符串。提交成功后用setTimeout延遲跳轉(zhuǎn)是為了讓showToast的提示能完整顯示出來否則頁(yè)面一換提示就沒了。request方法來自u(píng)tils/request.js它內(nèi)部封裝了wx.request統(tǒng)一拼接baseUrl、設(shè)置header、處理 HTTP 狀態(tài)碼。你如果換成云開發(fā)這里可以改成wx.cloud.callFunction頁(yè)面邏輯基本不用動(dòng)。3.3 投票詳情頁(yè)的選項(xiàng)渲染與提交detail.wxml里用wx:for渲染選項(xiàng)列表view classvote-detail view classvote-title{{vote.title}}/view view classoption-item wx:for{{vote.options}} wx:keyid>const total options.reduce((sum, item) sum item.count, 0); const list options.map(item ({ ...item, percent: total 0 ? 0 : Math.round((item.count / total) * 100) })); this.setData({ list, total });百分比用Math.round取整避免出現(xiàn)33.33333%這種不美觀的數(shù)字。進(jìn)度條寬度直接用percent拼成stylewidth: {{item.percent}}%簡(jiǎn)單直接。如果總票數(shù)為零要單獨(dú)處理否則會(huì)出現(xiàn)除以零得到NaN的情況頁(yè)面上顯示NaN%就很尷尬。4. 跑起來之前環(huán)境準(zhǔn)備與常見報(bào)錯(cuò)排查源碼能不能跑起來一半看代碼一半看環(huán)境。這一章把導(dǎo)入、配置、調(diào)試過程中最容易卡住的地方列出來每條都按「現(xiàn)象 → 原因 → 解決」寫方便你對(duì)號(hào)入座。4.1 開發(fā)者工具導(dǎo)入與 appid 配置現(xiàn)象導(dǎo)入項(xiàng)目后開發(fā)者工具提示「appid 不合法」或「無法編譯」。原因源碼里的project.config.json通常填的是一個(gè)占位 appid或者作者自己的測(cè)試號(hào)你本地沒有權(quán)限。解決打開project.config.json把a(bǔ)ppid字段改成你自己的小程序測(cè)試號(hào)或者直接在開發(fā)者工具導(dǎo)入時(shí)選擇「測(cè)試號(hào)」。如果你只是本地跑通不涉及真機(jī)預(yù)覽測(cè)試號(hào)完全夠用。4.2 接口請(qǐng)求失敗與域名校驗(yàn)現(xiàn)象頁(yè)面能打開但列表為空控制臺(tái)報(bào)request:fail url not in domain list。原因小程序正式環(huán)境要求所有請(qǐng)求域名在后臺(tái)配置白名單本地開發(fā)時(shí)如果沒勾選「不校驗(yàn)合法域名」請(qǐng)求會(huì)被攔截。解決在開發(fā)者工具右上角「詳情」→「本地設(shè)置」里勾選「不校驗(yàn)合法域名、web-view業(yè)務(wù)域名、TLS 版本以及 HTTPS 證書」。同時(shí)確認(rèn)utils/request.js里的baseUrl指向的后端服務(wù)已經(jīng)啟動(dòng)端口沒有被占用。4.3 數(shù)據(jù)渲染為空或 setData 不生效現(xiàn)象接口返回了數(shù)據(jù)但頁(yè)面上選項(xiàng)列表是空的或者修改數(shù)組后視圖沒更新。原因小程序setData對(duì)數(shù)組和對(duì)象的更新需要傳新引用直接修改原數(shù)組的某一項(xiàng)視圖層可能檢測(cè)不到變化。解決統(tǒng)一用擴(kuò)展運(yùn)算符或concat、filter生成新數(shù)組再setData。比如this.setData({ options: [...this.data.options, newItem] })而不是this.data.options.push(newItem)。4.4 投票提交后重復(fù)記錄現(xiàn)象用戶快速點(diǎn)擊「確認(rèn)投票」后端收到多條相同記錄。原因沒有做防重復(fù)提交處理按鈕點(diǎn)擊后沒有立即禁用。解決在data里加isSubmitting標(biāo)記提交前設(shè)為true請(qǐng)求結(jié)束后在finally里設(shè)回false。同時(shí)按鈕的disabled屬性綁定這個(gè)標(biāo)記雙保險(xiǎn)。4.5 時(shí)間格式與后端不一致現(xiàn)象創(chuàng)建投票時(shí)選擇了截止時(shí)間但后端收到的格式不對(duì)或者詳情頁(yè)顯示Invalid Date。原因小程序picker組件返回的是2025-01-01這樣的字符串后端可能期望時(shí)間戳或ISO 8601格式。解決在提交前統(tǒng)一轉(zhuǎn)換。如果后端要時(shí)間戳用new Date(endTime).getTime()如果要完整 ISO 格式用new Date(endTime).toISOString()。轉(zhuǎn)換邏輯放在create.js的submitVote里不要散落在多個(gè)頁(yè)面。注意真機(jī)調(diào)試時(shí)本地localhost接口是訪問不到的需要用局域網(wǎng) IP 或者部署到可訪問的測(cè)試服務(wù)器上。5. 二次開發(fā)怎么改從 Demo 到可用投票模塊的幾條路徑這份源碼最大的價(jià)值不是它現(xiàn)在能做什么而是它留出了哪些擴(kuò)展點(diǎn)。你拿到手之后大概率不會(huì)直接上線而是要根據(jù)自己的業(yè)務(wù)改幾處。下面是我在類似項(xiàng)目里常用的改造思路按改動(dòng)量從小到大排。5.1 替換數(shù)據(jù)源從本地接口到云開發(fā)如果你不想維護(hù)后端服務(wù)器最省事的改造是把utils/request.js里的wx.request換成微信云開發(fā)的數(shù)據(jù)庫(kù)調(diào)用。具體做法是在app.js的onLaunch里初始化云環(huán)境wx.cloud.init({ env: 你的環(huán)境ID })然后把創(chuàng)建投票的邏輯改成wx.cloud.database().collection(votes).add({ data: {...} })。查詢列表用.get()提交投票用.doc(voteId).update()配合原子操作_.inc(1)來增加票數(shù)。這樣改完之后你不需要自己買服務(wù)器、配域名、處理 HTTPS 證書對(duì)于小型投票場(chǎng)景足夠用。代價(jià)是云開發(fā)的免費(fèi)額度有限投票量特別大的時(shí)候要留意配額。5.2 增加投票類型單選改多選現(xiàn)在的 Demo 是單選投票selectedId是一個(gè)字符串。要改成多選需要把selectedId換成selectedIds數(shù)組點(diǎn)擊選項(xiàng)時(shí)判斷是否已選已選則移除未選則追加。提交接口的optionId也要改成optionIds數(shù)組。后端對(duì)應(yīng)的存儲(chǔ)結(jié)構(gòu)從「一條記錄一個(gè)選項(xiàng)」改成「一條記錄多個(gè)選項(xiàng)」統(tǒng)計(jì)票數(shù)時(shí)要注意去重邏輯。這個(gè)改動(dòng)涉及前端交互、接口參數(shù)、后端存儲(chǔ)三處建議先在紙上把數(shù)據(jù)流畫清楚再動(dòng)手否則很容易出現(xiàn)「前端傳了數(shù)組但后端還在按字符串解析」的翻車現(xiàn)場(chǎng)。5.3 增加登錄與權(quán)限控制Demo 里通常沒有用戶體系任何人都能創(chuàng)建和投票。要防止刷票至少需要拿到用戶的openid。小程序里可以通過wx.login獲取code傳給后端換openid然后在投票記錄里存一份同一個(gè)openid對(duì)同一個(gè)投票只能投一次。前端需要在app.js里做一次靜默登錄把openid存在globalData里。創(chuàng)建投票時(shí)帶上openid作為創(chuàng)建者標(biāo)識(shí)投票時(shí)帶上openid做去重判斷。這部分改動(dòng)不大但能擋住大部分重復(fù)投票的情況。5.4 樣式與交互的定制化app.wxss里通常定義了一些全局樣式變量比如主色調(diào)、圓角大小、按鈕高度。你要換成自己品牌的視覺風(fēng)格改這里比逐個(gè)頁(yè)面改.wxss高效得多。我一般會(huì)把顏色、間距、字號(hào)抽成 CSS 變量放在app.wxss的page選擇器下頁(yè)面里用var(--primary-color)引用。這樣換主題只需要改一處。另外投票選項(xiàng)的選中態(tài)、結(jié)果頁(yè)的進(jìn)度條動(dòng)畫這些交互細(xì)節(jié)可以根據(jù)業(yè)務(wù)場(chǎng)景調(diào)整。比如活動(dòng)評(píng)選類投票結(jié)果頁(yè)可以加一個(gè)「實(shí)時(shí)更新」的輪詢意見收集類投票結(jié)果頁(yè)可以默認(rèn)隱藏票數(shù)只給管理員看。5.5 一個(gè)容易忽略的細(xì)節(jié)投票截止后的狀態(tài)處理Demo 里可能只做了「創(chuàng)建時(shí)設(shè)置截止時(shí)間」但沒有處理「截止后禁止投票」的邏輯。你需要在詳情頁(yè)的onLoad里判斷當(dāng)前時(shí)間是否超過endTime如果已過期把選項(xiàng)區(qū)域替換成「投票已結(jié)束」的提示同時(shí)禁用提交按鈕。后端接口也要做同樣的校驗(yàn)防止有人繞過前端直接調(diào)接口。這個(gè)邏輯不復(fù)雜但漏掉的話用戶會(huì)在一個(gè)已經(jīng)結(jié)束的投票里繼續(xù)點(diǎn)選體驗(yàn)很差。我一般會(huì)在utils里加一個(gè)isVoteExpired(endTime)的工具函數(shù)前端后端各調(diào)一次保證一致性。從那以后我每次拿到類似的小程序 Demo都會(huì)先跑通主流程再按「數(shù)據(jù)源 → 用戶體系 → 業(yè)務(wù)規(guī)則 → 樣式」的順序逐層改造而不是一上來就大改頁(yè)面結(jié)構(gòu)。希望這份拆解能幫你少走點(diǎn)彎路順利把投票功能跑起來、改下去。本文還有配套的精品資源點(diǎn)擊獲取