實(shí)戰(zhàn):微信小程序開發(fā)與部署避坑指南)
簡(jiǎn)介一款專為喝酒與聚會(huì)場(chǎng)景設(shè)計(jì)的微信小程序源碼面向需要快速搭建派對(duì)互動(dòng)工具的小程序開發(fā)者、二次開發(fā)者及聚會(huì)組織者這個(gè)“千尋百念修復(fù)版”針對(duì)舊版進(jìn)行了優(yōu)化內(nèi)置多種互動(dòng)游戲與娛樂工具可用于活躍聚會(huì)氛圍、增強(qiáng)參與者互動(dòng)適合各類規(guī)模的社交場(chǎng)景。資源包為 zip 格式共 447 個(gè)文件整體僅 1.38MB以 png、js、json、mp3、wxss、wxml 等類型為主覆蓋圖片素材、邏輯腳本、配置文件、音頻資源和頁(yè)面結(jié)構(gòu)目錄分層清晰目前已有 53 人學(xué)習(xí)可作為小程序項(xiàng)目參考。源碼雖未列出全部功能細(xì)節(jié)但描述中強(qiáng)調(diào)功能完備且修復(fù)了舊版缺陷可直接部署使用目錄區(qū)分了公共組件、游戲邏輯、核心頁(yè)面等模塊方便開發(fā)者定位與二次開發(fā)節(jié)省基礎(chǔ)搭建時(shí)間。對(duì)于想學(xué)習(xí)小程序結(jié)構(gòu)或低成本落地聚會(huì)娛樂應(yīng)用的人這是一份小巧實(shí)用的基礎(chǔ)代碼包。1. 喝酒小程序源碼拿到修好的“千尋百念”先看這三處如果你搜過“喝酒小程序源碼”大概率見過那款叫“千尋百念”的微信小程序——轉(zhuǎn)盤、骰子、真心話大冒險(xiǎn)、輸贏罰酒一套典型的線下聚會(huì)助興工具。這套源碼流傳的版本很多但不少是半成品有的轉(zhuǎn)盤動(dòng)畫卡死有的分享回調(diào)拿不到參數(shù)有的登錄直接報(bào)錯(cuò)。我拿到這版“修復(fù)版”源碼之后從頁(yè)面結(jié)構(gòu)、事件綁定到微信登錄鏈路都過了一遍確認(rèn)它可以正常跑起來。這篇筆記把拆包過程、部署步驟和踩過的坑寫清楚讓你拿到的源碼不只是“能打開模擬器”而是真正能上傳體驗(yàn)版、拿手機(jī)號(hào)登錄、分享給好友一起玩的完整項(xiàng)目。適合有基礎(chǔ)小程序開發(fā)經(jīng)驗(yàn)、想直接改來用的讀者。2. 源碼頭緒小程序的頁(yè)面棧與“千尋百念”的文件結(jié)構(gòu)2.1 為什么這版源碼能叫“修復(fù)版”先看 app.json 和頁(yè)面路由微信小程序和普通網(wǎng)頁(yè)不同它沒有 URL 地址欄所有頁(yè)面都靠app.json里的pages數(shù)組注冊(cè)。打開項(xiàng)目第一件事不是看代碼而是看這個(gè)文件——頁(yè)面順序、窗口樣式、tabBar、插件都在這里定義。{ pages: [ pages/index/index, pages/game/dice, pages/game/turntable, pages/game/truth, pages/user/profile, pages/user/history ], window: { navigationBarTitleText: 千尋百念, navigationBarBackgroundColor: #2B2B2B, navigationBarTextStyle: white, backgroundColor: #1A1A1A }, style: v2, sitemapLocation: sitemap.json, lazyCodeLoading: requiredComponents }這段配置說明三件事第一這個(gè)項(xiàng)目有 6 個(gè)頁(yè)面核心玩法是骰子、轉(zhuǎn)盤、真心話用戶相關(guān)的是主頁(yè)和個(gè)人歷史記錄第二全局導(dǎo)航欄是深色底白字背景也是深色說明這版 UI 走的是“酒吧暗色風(fēng)”第三lazyCodeLoading開了按需注入能減少首包體積。拿到源碼后先核對(duì)pages數(shù)組如果目錄里少了一個(gè)頁(yè)面文件編譯時(shí)就會(huì)白屏報(bào)錯(cuò)。常見做法是直接從項(xiàng)目根目錄往下捋把頁(yè)面文件缺失表拉出來。2.2 關(guān)鍵目錄功能對(duì)照components / utils / services修復(fù)版和原始版的差別主要在代碼組織上。我拆開后的目錄結(jié)構(gòu)大致如下├── app.js ├── app.json ├── app.wxss ├── components/ │ ├── dice/ │ │ ├── dice.js │ │ ├── dice.json │ │ ├── dice.wxml │ │ └── dice.wxss │ ├── turntable/ │ │ ├── turntable.js │ │ ├── turntable.json │ │ ├── turntable.wxml │ │ └── turntable.wxss │ └── share-card/ │ ├── share-card.js │ ├── share-card.json │ ├── share-card.wxml │ └── share-card.wxss ├── pages/ │ ├── index/ │ ├── game/ │ ├── user/ │ └── ... ├── services/ │ ├── auth.js │ ├── game.js │ └── user.js ├── utils/ │ ├── wxapi.js │ ├── format.js │ └── config.js └── project.config.jsoncomponents存的是自定義組件骰子和轉(zhuǎn)盤是獨(dú)立組件包裝的方便復(fù)用services層封裝了登錄、游戲數(shù)據(jù)上報(bào)、用戶信息讀取的接口utils/wxapi.js統(tǒng)一包了一層微信 API 調(diào)用把成功和失敗回調(diào)轉(zhuǎn)成 Promise。這里有一點(diǎn)我之前翻車過的經(jīng)驗(yàn)很多流傳的源碼把 wx.request 直接寫進(jìn)頁(yè)面里改需求時(shí)要把每個(gè)頁(yè)面翻一遍這版把請(qǐng)求收攏到 services算是修復(fù)版里比較像樣的改動(dòng)。2.3 app.js 里的全局登錄態(tài)為什么拿手機(jī)號(hào)登錄前必須先 wx.login小程序的登錄和傳統(tǒng)網(wǎng)頁(yè)登錄不一樣它沒有 session 中間件核心是wx.login換取 code再由后端拿著 code 去微信接口換 openid??催@版 app.js 的邏輯App({ globalData: { openid: , userInfo: null, loggedIn: false }, onLaunch() { this.initLogin(); }, initLogin() { const token wx.getStorageSync(token); if (token) { this.globalData.loggedIn true; } else { this.doWxLogin(); } }, doWxLogin() { wx.login({ success: (res) { if (res.code) { // 將 code 發(fā)給后端后端用 code 換 openid 和 session_key this.exchangeCode(res.code); } } }); }, exchangeCode(code) { const { apiBase } require(./utils/config); wx.request({ url: ${apiBase}/auth/login, method: POST, data: { code }, success: (res) { if (res.data.code 0) { wx.setStorageSync(token, res.data.data.token); this.globalData.loggedIn true; } } }); } });wx.login拿到的 code 有效期只有 5 分鐘而且只能用一次。這里的參數(shù)res.code是一串臨時(shí)憑證后端拿它換 openid。設(shè)計(jì)上注意兩點(diǎn)一是服務(wù)端要處理 code 重復(fù)使用的情況否則并發(fā)請(qǐng)求時(shí)第二次必然失敗二是 token 存儲(chǔ)用wx.setStorageSync簡(jiǎn)單直接如果后續(xù)要多端同步再換成wx.setStorage的異步版本。之前有人問為什么不直接把 code 存進(jìn) storage其實(shí) code 是一次性的存了沒意義應(yīng)該存的是后端返回的 token。3. 部署到開發(fā)者工具三步把源碼跑起來附帶 project.config.json 調(diào)優(yōu)3.1 導(dǎo)入項(xiàng)目AppID 與編譯環(huán)境的正確設(shè)置用微信開發(fā)者工具導(dǎo)入源碼時(shí)關(guān)鍵不是點(diǎn)“導(dǎo)入”按鈕而是先看project.config.json里的appid字段。很多流傳源碼里的 appid 是別人的測(cè)試號(hào)直接導(dǎo)入會(huì)報(bào)“appid 不存在”。{ appid: 你的小程序AppID, compileType: miniprogram, libVersion: latest, setting: { es6: true, postcss: true, minified: true, urlCheck: false }, projectname: qianxun-bainian, simulatorType: wechat, condition: {} }urlCheck: false是我建議設(shè)的。不關(guān)掉的話后端接口域名沒配置時(shí)調(diào)試階段每個(gè)請(qǐng)求都會(huì)被攔截。正式發(fā)布之前記得去微信公眾平臺(tái)把 request 合法域名配好再把urlCheck改回 true。libVersion設(shè)為latest能讓工具自動(dòng)匹配最新的基礎(chǔ)庫(kù)但如果你用的組件庫(kù)有兼容性要求這里要鎖定到具體版本否則上線后低版本微信打不開。3.2 真機(jī)預(yù)覽前的必要檢查不校驗(yàn)合法域名、調(diào)試器開關(guān)開發(fā)者工具里跑通不代表真機(jī)沒問題。真機(jī)預(yù)覽之前我一般會(huì)強(qiáng)制走一遍這幾個(gè)開關(guān)詳情 - 本地設(shè)置 - 不校驗(yàn)合法域名、web-view 域名、TLS 版本以及 HTTPS 證書打開調(diào)試器 - 打開調(diào)試。真機(jī)預(yù)覽開啟調(diào)試才能在小程序右上角菜單里看到 vConsole 入口。這一步做不到的話真機(jī)一請(qǐng)求后端就白屏控制臺(tái)打不開你根本不知道是域名問題還是接口報(bào)錯(cuò)。最常見的現(xiàn)象是模擬器里一切正常真機(jī)上所有 wx.request 都失敗報(bào)ERR_CERT_COMMON_NAME_INVALID其實(shí)就是證書域名對(duì)不上。這屬于配置問題不是代碼問題但卡住新手很久。3.3 本地后端聯(lián)調(diào)用 Node 起一個(gè)臨時(shí)登錄接口源碼里大概率沒有后端只有前端。不要慌本地用一個(gè) Node 服務(wù)模擬后端登錄接口即可把流程跑通。我這邊常用的方案是 node express十幾行代碼就能搭一個(gè)登錄接口。const express require(express); const app express(); app.use(express.json()); // 模擬微信登錄接口 app.post(/auth/login, (req, res) { const { code } req.body; if (!code) { return res.json({ code: 1, msg: code is required }); } // 實(shí)際項(xiàng)目中這里用 code 請(qǐng)求微信接口拿到 openid 后下發(fā) token const mockOpenid mock_openid_ Date.now(); const token mock_token_ Math.random().toString(36).slice(2); res.json({ code: 0, data: { token, openid: mockOpenid } }); }); app.listen(3000, () { console.log(mock server running at http://localhost:3000); });然后修改utils/config.js里的apiBase把它指向本地地址module.exports { // 本地聯(lián)調(diào)用 http://localhost // 上線換成 https://your.domain.com apiBase: http://localhost:3000, version: 1.0.0 };注意微信開發(fā)者工具模擬器里請(qǐng)求 localhost 是沒問題的但真機(jī)預(yù)覽時(shí)不行——手機(jī)上的 localhost 指的是手機(jī)自己。真機(jī)調(diào)本地接口要把a(bǔ)piBase改成電腦的局域網(wǎng) IP比如http://192.168.1.101:3000。這一步很容易忽略我最初調(diào)試時(shí)在這里浪費(fèi)了一個(gè)晚上。4. 核心玩法代碼解讀骰子動(dòng)畫、轉(zhuǎn)盤抽獎(jiǎng)與分享參數(shù)的透?jìng)?.1 骰子組件的隨機(jī)數(shù)與動(dòng)畫狀態(tài)機(jī)骰子是小程序里最容易寫崩的部分原因不是隨機(jī)數(shù)而是動(dòng)畫狀態(tài)控制??催@版組件的核心邏輯Component({ data: { diceValue: 1, rolling: false }, methods: { rollDice() { if (this.data.rolling) return; this.setData({ rolling: true }); // 隨機(jī) 1-6 const value Math.floor(Math.random() * 6) 1; // 先播動(dòng)畫動(dòng)畫結(jié)束后再顯示結(jié)果 setTimeout(() { this.setData({ diceValue: value, rolling: false }); // 觸發(fā)自定義事件讓父頁(yè)面可以拿到結(jié)果 this.triggerEvent(rollend, { value }); }, 800); } } });這里的rolling標(biāo)志位就是防重復(fù)觸發(fā)。如果不加用戶在動(dòng)畫期間連點(diǎn)會(huì)連續(xù)觸發(fā)多個(gè) setTimeout結(jié)果就是骰子還沒停下又變了。triggerEvent是組件向父頁(yè)面通信的標(biāo)準(zhǔn)方式和 Vue 的$emit類似。父頁(yè)面這樣接onDiceRollEnd(e) { const value e.detail.value; // 根據(jù)點(diǎn)數(shù)決定誰(shuí)喝酒 this.setData({ currentDice: value }); }4.2 轉(zhuǎn)盤抽獎(jiǎng)canvas 繪制與指針停靠角度計(jì)算轉(zhuǎn)盤組件用的是 canvas繪制扇形和文字都在 canvas 里完成。停靠角度是這里的重災(zāi)區(qū)很多源碼轉(zhuǎn)動(dòng)結(jié)束沒有“歸位”邏輯指針停在兩個(gè)扇形交界處結(jié)果就出現(xiàn)爭(zhēng)議。修復(fù)版的做法是已知每個(gè)扇區(qū)角度為 60 度然后算出目標(biāo)扇區(qū)中心角再讓轉(zhuǎn)盤轉(zhuǎn) N 圈后精確停在中心位置。const sectors [一杯, 兩杯, 半杯, 免喝, 一杯, 兩杯]; const sectorAngle 360 / sectors.length; spin() { if (this.data.spinning) return; const targetIndex Math.floor(Math.random() * sectors.length); // 轉(zhuǎn) 5 圈 目標(biāo)扇區(qū)中心角 隨機(jī)偏移 15 度/-15 度 const targetAngle 360 * 5 targetIndex * sectorAngle sectorAngle / 2; this.setData({ spinning: true, currentAngle: targetAngle }); }關(guān)鍵參數(shù)是targetAngle。先隨機(jī)決定目標(biāo)扇區(qū)再圍繞它的中心角給一個(gè) ±15 度的偏移視覺上看起來像“差一點(diǎn)就轉(zhuǎn)到隔壁”實(shí)際上結(jié)果已經(jīng)定了。偏移量最好不要超過 15 度否則會(huì)滑到相鄰扇區(qū)你說是按中心算還是按邊緣算會(huì)吵起來。4.3 分享給好友onShareAppMessage 的參數(shù)透?jìng)骺雍染菩〕绦虻暮诵膱?chǎng)景是多人聚會(huì)必須依賴微信分享。這版的分享邏輯在pages/game/game.js里onShareAppMessage() { const roomId this.data.roomId || default; return { title: 來骰一把輸了認(rèn)罰, path: /pages/game/game?roomId${roomId}, imageUrl: /images/share-card.png }; }這里最容易踩的坑是path。如果頁(yè)面路徑拼錯(cuò)分享卡片點(diǎn)開就是“頁(yè)面不存在”。另外如果要帶參數(shù)參數(shù)必須是字符串不能是對(duì)象。imageUrl可以自定義分享卡片圖片不傳的話默認(rèn)截屏。之前有人問分享卡片能不能直接拉起骰子動(dòng)畫不行onShareAppMessage只負(fù)責(zé)分享不負(fù)責(zé)頁(yè)面行為。分享出去后好友點(diǎn)開的路徑解析方式onLoad(options) { if (options.roomId) { this.setData({ roomId: options.roomId }); } // 從分享鏈接進(jìn)來自動(dòng)加入房間 this.joinRoom(this.data.roomId); }從分享卡片進(jìn)入時(shí)options里能拿到 query 參數(shù)。注意第一次進(jìn)入小程序時(shí)onLoad一定觸發(fā)但如果小程序已經(jīng)在后臺(tái)存活分享鏈接再次打開只會(huì)觸發(fā)onShow。所以嚴(yán)謹(jǐn)做法是參數(shù)解析同時(shí)寫在onLoad和onShow里或者在onLoad里判斷場(chǎng)景值。這個(gè)坑在社交類小程序里很典型分享參數(shù)丟失很多人遇到過。5. 避坑指南千尋百念修復(fù)版里我踩過的四個(gè)實(shí)際問題5.1 轉(zhuǎn)盤在 iOS 真機(jī)上旋轉(zhuǎn)卡頓現(xiàn)象安卓模擬器里轉(zhuǎn)盤絲滑iPhone 上旋轉(zhuǎn)掉幀明顯卡頓感很強(qiáng)。原因轉(zhuǎn)盤用的不是 canvas 2D 繪制而是 CSS3transform: rotate每幀都在改變角度iOS 上 Safari 的合成器對(duì)高頻 transform 更新處理不夠好。加上轉(zhuǎn)盤背景圖是幾張合成的大圖內(nèi)存占用偏高。解決把背景圖先壓縮到 750px 寬并用canvas繪制每一幀。注意代碼里不要同時(shí)修改變形和透明度因?yàn)橐粋€(gè)觸發(fā)合成層一個(gè)觸發(fā)重繪。5.2 分享到群里好友點(diǎn)開收不到 roomId現(xiàn)象從分享卡片進(jìn)入后頁(yè)面上房間號(hào)為空直接進(jìn)到了默認(rèn)房間。原因分享路徑里的參數(shù)如果含有特殊字符比如或中文微信會(huì)編碼但onLoad里拿到后沒有 decode 導(dǎo)致錯(cuò)亂。這版源碼里的 roomId 是uuid格式本身安全但老版本里有用數(shù)字自增 id 的偏偏拼在 URL 里時(shí)漏了編碼處理。解決統(tǒng)一用encodeURIComponent包一層。另外不要用“客戶端生成 roomId”的方式多個(gè)端同時(shí)生成容易撞改了之后用服務(wù)端返的 id。5.3 真機(jī)預(yù)覽時(shí)手機(jī)號(hào)登錄一直轉(zhuǎn)圈現(xiàn)象點(diǎn)了“手機(jī)號(hào)登錄”按鈕loading 轉(zhuǎn)圈超過 10 秒最后提示“l(fā)ogin fail”。原因手機(jī)號(hào)登錄需要兩個(gè)條件同時(shí)滿足一是小程序是認(rèn)證過的企業(yè)主體個(gè)人主體沒有權(quán)限二是必須用button組件設(shè)置open-typegetPhoneNumber觸發(fā)不能直接用wx.getPhoneNumber。很多流傳源碼在這里用了 wx 方法所以個(gè)人開發(fā)者調(diào)試時(shí)必然失敗。解決檢查 app.json 里是否聲明了requiredPrivateInfos然后看按鈕寫法button open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber 手機(jī)號(hào)快捷登錄 /buttonbindgetphonenumber回調(diào)里拿到的e.detail.code也是臨時(shí)憑證后端要用這個(gè) code 換手機(jī)號(hào)不能直接解析手機(jī)號(hào)。5.4 后臺(tái)上傳代碼時(shí)提示“分包過大”現(xiàn)象開發(fā)者工具上傳代碼時(shí)提示主包超過 2MB無法上傳。原因項(xiàng)目里的圖片資源沒有壓縮尤其轉(zhuǎn)盤背景圖和骰子貼圖可能都是 1MB 級(jí)別。一個(gè)小程序的主包限制是 2MB超過就傳不上去。解決圖片全部走壓縮流程PNG 轉(zhuǎn) WebP 或壓縮到 60% 質(zhì)量代碼里引用的路徑同步改名。另外可以做分包處理把游戲頁(yè)面扔進(jìn)subpackages里。{ subpackages: [ { root: pages/game, name: game, pages: [ dice, turntable, truth ] } ] }注意subpackages里的頁(yè)面路徑不用再出現(xiàn)在pages數(shù)組中app.json里會(huì)以分包形式加載。改完之后真機(jī)預(yù)覽時(shí)這些頁(yè)面會(huì)延遲加載首次進(jìn)入會(huì)有一點(diǎn)白屏?xí)r間屬于正?,F(xiàn)象。6. 進(jìn)階把“千尋百念”接上真實(shí)后端登錄鏈路全通后的下一步當(dāng)你把前端的登錄、游戲、分享都跑通之后可以接入真實(shí)后端。微信小程序手機(jī)號(hào)登錄的正確流程是前端通過open-typegetPhoneNumber拿到code用這個(gè) code 請(qǐng)求后端后端再拿 code 去微信接口換取手機(jī)號(hào)。接口地址是固定的關(guān)鍵參數(shù)是code和openid。// services/auth.js const { apiBase } require(../utils/config); function phoneLogin(phoneCode) { return new Promise((resolve, reject) { wx.request({ url: ${apiBase}/auth/phone-login, method: POST, data: { phoneCode, // 這里同時(shí)帶上 wx.login 的 code服務(wù)端用來?yè)Q openid loginCode: this.globalData.loginCode }, success: (res) resolve(res.data), fail: (err) reject(err) }); }); }服務(wù)端拿phoneCode換手機(jī)號(hào)的請(qǐng)求返回的手機(jī)號(hào)是加密的需要結(jié)合session_key解密。具體流程標(biāo)準(zhǔn)實(shí)現(xiàn)是后端先用loginCode換session_key再拿phoneCode換手機(jī)號(hào)密文最后 AES 解密。不建議前端自己解密密鑰管理放后端更安全。接入真實(shí)后端之后建議做一件事把utils/config.js里的apiBase改成正式域名并且在微信公眾平臺(tái)配置 request 合法域名。這一步還有一個(gè)附加好處后續(xù)加排行榜、防沉迷提醒、歷史戰(zhàn)績(jī)同步等功能不用再動(dòng)前端登錄邏輯直接在 services 層加方法就行。另外一個(gè)我從這次拆包中養(yǎng)成的習(xí)慣每次拿到修復(fù)版源碼第一件事先全局搜索TODO和FIXME第二件事看project.config.json里的appid第三件事跑一遍登錄鏈路第四件事檢查圖片大小。這四步走完基本上是能用的少走一步后面都可能返工。希望這篇筆記幫到你讓你在“喝酒小程序源碼”這個(gè)關(guān)鍵詞下少走一些彎路。本文還有配套的精品資源點(diǎn)擊獲取