从零构建3D球体抽奖:基于Express+Three.js的企业年会互动方案

张开发
2026/4/21 1:54:58 15 分钟阅读

分享文章

从零构建3D球体抽奖:基于Express+Three.js的企业年会互动方案
1. 为什么选择ExpressThree.js搭建3D抽奖系统每年年底企业年会都少不了一个激动人心的环节——抽奖。传统的抽奖程序大多采用平面转盘或名单滚动的方式视觉效果单一互动性不足。而采用3D球体抽奖不仅能给参与者带来更强的视觉冲击还能提升整个活动的科技感和趣味性。Express作为轻量级的Node.js框架非常适合快速搭建后台服务。它处理Excel导入导出、数据持久化等需求非常方便而且学习成本低。Three.js则是目前最流行的Web 3D渲染库用它来实现3D球体旋转、粒子效果等动画再合适不过。我在去年为公司年会开发这套系统时对比过几种方案。纯前端的方案虽然简单但缺乏数据持久化能力而重量级的全栈框架又显得杀鸡用牛刀。最终选择ExpressThree.js的组合只用不到500行代码就实现了所有功能现场运行稳定效果超出预期。2. 环境准备与项目初始化2.1 安装Node.js环境首先需要安装Node.js运行环境。建议使用LTS版本目前是18.x稳定性更有保障。安装完成后在命令行执行以下命令检查是否安装成功node -v npm -v如果看到版本号输出说明环境已经就绪。我推荐使用nvmNode Version Manager来管理Node版本特别是当你需要同时维护多个项目时。2.2 初始化Express项目创建一个新文件夹作为项目根目录执行npm init -y npm install express body-parser multer xlsx --save这些依赖包的作用分别是expressWeb框架核心body-parser处理HTTP请求体multer处理文件上传xlsx读写Excel文件接着创建基本的Express应用结构// server.js const express require(express); const app express(); const port 8888; app.use(express.static(public)); app.use(express.json()); app.listen(port, () { console.log(Server running at http://localhost:${port}); });这个基础骨架已经能提供静态文件服务和API接口能力。public文件夹将存放前端资源包括我们的Three.js代码。3. 实现3D抽奖球体效果3.1 Three.js基础场景搭建在public/js目录下创建lottery.js开始构建3D场景const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.getElementById(container).appendChild(renderer.domElement); // 添加光源 const light new THREE.AmbientLight(0x404040); scene.add(light); const directionalLight new THREE.DirectionalLight(0xffffff, 0.5); scene.add(directionalLight); camera.position.z 5;这段代码创建了一个基本的3D场景包含相机、渲染器和基础光照。接下来我们要创建球体并为其添加材质。3.2 创建粒子球体效果为了让球体更有科技感我们使用粒子系统来构建const particleCount 1000; const particles new THREE.BufferGeometry(); const positions new Float32Array(particleCount * 3); const colors new Float32Array(particleCount * 3); // 初始化粒子位置和颜色 for(let i 0; i particleCount; i) { const theta Math.random() * Math.PI * 2; const phi Math.acos(2 * Math.random() - 1); const radius 2; positions[i*3] radius * Math.sin(phi) * Math.cos(theta); positions[i*31] radius * Math.sin(phi) * Math.sin(theta); positions[i*32] radius * Math.cos(phi); colors[i*3] Math.random(); colors[i*31] Math.random(); colors[i*32] Math.random(); } particles.setAttribute(position, new THREE.BufferAttribute(positions, 3)); particles.setAttribute(color, new THREE.BufferAttribute(colors, 3)); const particleMaterial new THREE.PointsMaterial({ size: 0.05, vertexColors: true, transparent: true, opacity: 0.8 }); const particleSystem new THREE.Points(particles, particleMaterial); scene.add(particleSystem);这段代码创建了一个由1000个彩色粒子组成的球体每个粒子都有随机位置和颜色视觉效果相当炫酷。4. 后台数据管理实现4.1 Excel数据导入导出在server目录下创建dataHandler.js处理Excel文件const XLSX require(xlsx); const path require(path); function importUsers(filePath) { const workbook XLSX.readFile(filePath); const sheet workbook.Sheets[workbook.SheetNames[0]]; return XLSX.utils.sheet_to_json(sheet); } function exportResults(results) { const worksheet XLSX.utils.json_to_sheet(results); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 抽奖结果); const exportPath path.join(__dirname, data, result.xlsx); XLSX.writeFile(workbook, exportPath); return exportPath; } module.exports { importUsers, exportResults };然后在server.js中添加路由const { importUsers, exportResults } require(./dataHandler); const multer require(multer); const upload multer({ dest: uploads/ }); app.post(/api/upload, upload.single(file), (req, res) { try { const users importUsers(req.file.path); // 保存到内存或数据库 res.json({ success: true, count: users.length }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.get(/api/export, (req, res) { const filePath exportResults(lotteryResults); res.download(filePath); });4.2 抽奖逻辑实现抽奖的核心是随机选择去重。我们在server.js中添加let lotteryUsers []; // 从Excel导入的用户 let drawnUsers []; // 已抽中用户 let prizes []; // 奖品配置 function drawLottery(prizeType, count) { const availableUsers lotteryUsers.filter(user !drawnUsers.some(drawn drawn.id user.id) ); if(availableUsers.length count) { throw new Error(可用用户不足); } const winners []; for(let i 0; i count; i) { const randomIndex Math.floor(Math.random() * availableUsers.length); winners.push(availableUsers[randomIndex]); availableUsers.splice(randomIndex, 1); } const prize prizes.find(p p.type prizeType); const result winners.map(user ({ ...user, prize: prize.text, time: new Date() })); drawnUsers.push(...result); return result; }5. 前后端交互与功能整合5.1 抽奖控制API在server.js中添加以下路由app.get(/api/prizes, (req, res) { res.json(prizes); }); app.post(/api/draw, (req, res) { try { const { prizeType, count } req.body; const result drawLottery(prizeType, count); res.json({ success: true, winners: result }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.post(/api/reset, (req, res) { drawnUsers []; res.json({ success: true }); });5.2 前端交互实现在lottery.js中添加控制逻辑let rotating true; let rotationSpeed 0.01; function animate() { requestAnimationFrame(animate); if(rotating) { particleSystem.rotation.x rotationSpeed; particleSystem.rotation.y rotationSpeed; } renderer.render(scene, camera); } animate(); async function startDraw(prizeType, count) { rotating false; rotationSpeed 0.1; try { const response await fetch(/api/draw, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prizeType, count }) }); const result await response.json(); if(result.success) { showWinners(result.winners); } } catch (err) { console.error(抽奖失败:, err); } finally { setTimeout(() { rotating true; rotationSpeed 0.01; }, 3000); } }6. 项目部署与优化建议6.1 生产环境部署开发完成后可以使用PM2来管理Node进程npm install pm2 -g pm2 start server.js --name lottery-system为了提高性能可以添加静态文件缓存app.use(express.static(public, { maxAge: 1d, setHeaders: (res, path) { if(path.endsWith(.js)) { res.setHeader(Cache-Control, public, max-age31536000); } } }));6.2 性能优化技巧在Three.js渲染方面我有几个实测有效的优化建议减少粒子数量在移动设备上1000个粒子可能造成卡顿可以动态调整const particleCount window.innerWidth 768 ? 1000 : 500;使用性能更好的材质const particleMaterial new THREE.PointsMaterial({ size: 0.05, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true // 根据距离调整粒子大小 });添加加载动画避免白屏const loadingManager new THREE.LoadingManager(); loadingManager.onLoad () { document.getElementById(loader).style.display none; }; const textureLoader new THREE.TextureLoader(loadingManager);这套系统在去年年会现场运行非常稳定支持了200多人的抽奖活动。最让我自豪的是当3D球体开始旋转抽奖时全场发出的惊叹声。开发过程中最大的收获是认识到技术不一定要多复杂关键是选对工具组合把每个环节都做扎实。

更多文章