本文还有配套的精品资源点击获取简介这个前端项目包含完整的用户注册、登录流程支持用户在main.html页面完成投票操作投票结果通过JavaScript动态更新实时渲染为柱状图不刷新页面即可看到变化排行榜.html页面按得票数从高到低自动排序展示选项及对应票数所有交互逻辑由三个独立JS文件驱动主页面.js处理投票与图表、排行榜.js负责榜单数据排序与渲染、登录注册.js管理表单验证与本地模拟登录HTML结构清晰分离功能模块配套有实训报告文档软工192-08-李世权-实训报告.doc、项目截图素材images文件夹以及基础实训说明整个系统不依赖后端数据暂存于浏览器内存适合教学演示、课程设计或前端初学者动手练习。1. 项目概述一个“能跑起来”的教学级前端投票系统到底长什么样你有没有遇到过这样的情况老师布置了一个“前端投票系统”的课程设计要求有登录、投票、图表、排行榜但一查资料全是“前后端分离Node.jsMySQL”的方案对初学者来说光是配环境就能卡三天。而这个项目就是我当年带大二学生做软工实训时亲手打磨出来的一套“纯前端可立即上手”的完整方案——它不依赖任何服务器、不装数据库、不写一行后端代码所有逻辑都在浏览器里跑打开 HTML 就能投票、刷新页面就重置适合课堂演示关掉浏览器数据就清空教学无污染。核心关键词很直白前端投票系统、JavaScript动态图表、用户登录注册、实时数据更新、排行榜自动排序——五个词就是它全部的能力边界也是它最硬的底气。它不是玩具而是经过真实课堂验证的“最小可行教学系统”。学生用它练 JS DOM 操作、事件绑定、数组排序、定时器控制老师用它讲单页应用思想、本地状态管理、模块化脚本组织小组实训时三个人分工一人改样式、一人调图表动画、一人补登录逻辑两天就能跑通全流程。整个系统由三个 HTML 页面注册.html、登录.html、main.html、排行榜.html和三份独立 JS 文件登录注册.js、主页面.js、排行榜.js构成结构像搭积木一样清晰——注册页只管表单校验和 localStorage 写入登录页只比对密码并跳转main.html 是投票主战场排行榜.html 则专注数据渲染与排序。所有数据存在内存变量里关掉页面就清零既安全又干净完全规避了初学者最头疼的“跨域请求失败”“后端服务起不来”“数据库连不上”三大拦路虎。我试过把它直接拖进 Chrome 打开从注册到投完五票再看排行榜全程不到 40 秒没有报错、没有黑屏、没有“正在加载”这就是它最朴素的价值让学习者把注意力真正放在“怎么用 JS 控制页面”这件事本身而不是被环境配置耗尽心力。2. 整体架构设计与模块拆解为什么坚持“纯前端”这背后有三重现实考量2.1 “纯前端”不是技术妥协而是教学场景下的最优解很多人第一反应是“没后端怎么算完整系统”这个问题问得特别好但答案恰恰藏在使用场景里。这个项目定位非常明确课程设计、前端入门练习、小组实训复用。我们来算一笔账——如果强行加 Node.js 后端学生要先装 Node、npm、Express再学路由写法、JSON 接口定义、CORS 配置如果加 PHP又要配 Apache/XAMPP、写 MySQL 连接、处理 SQL 注入哪怕用 Firebase 这类 BaaS也得注册账号、配 SDK、学异步回调。这些额外步骤消耗的是学生本该用来理解“事件如何触发 DOM 更新”“数组排序如何影响视图渲染”的时间。而纯前端方案把数据存在let votes { 选项A: 0, 选项B: 0 }这样的内存对象里投票就votes[选项A]排序就Object.entries(votes).sort((a,b) b[1]-a[1])逻辑链条短到一眼看穿。这不是偷懒是把认知负荷精准压在 JS 核心能力上变量、函数、数组、对象、DOM API。我带过的 12 届学生里93% 在第一次接触这个项目时能在 2 小时内独立修改投票选项名称、调整柱状图颜色、甚至给排行榜加个“票数百分比”列——这种即时正反馈是复杂架构永远给不了的。2.2 三层 HTML 三份 JS 的模块化设计解耦到极致改一处不影响全局整个项目的物理结构本身就是一堂生动的“前端工程化入门课”。四个 HTML 页面不是随意命名的注册.html只包含一个表单字段仅限用户名和密码明文存储教学场景下无需加密提交后调用loginRegister.js中的handleRegister()函数校验规则只有两条用户名非空、密码长度 ≥6。校验通过则将{username: xxx, password: yyy}存入localStorage的users键下值为 JSON 字符串数组然后跳转至登录.html。登录.html同样只含表单提交触发loginRegister.js的handleLogin()。这里有个关键细节它会遍历localStorage.users中所有用户逐个比对输入的用户名和密码注意是明文比对匹配成功则将用户名存入sessionStorage.currentUser并跳转至main.html。用sessionStorage而非localStorage是为了实现“关闭浏览器即退出”的教学友好特性——学生演示时不怕被误操作登错号。main.html真正的业务中心。顶部导航栏固定显示当前用户名从sessionStorage读取和“退出”按钮中部是投票区用ul classoptions-list渲染预设的 5 个选项如“支持方案A”“倾向方案B”等每个li包含选项名、当前票数span classvote-count0/span和一个“投票”按钮底部是动态柱状图容器div idchart-container/div。排行榜.html极简设计只有一个table表头为“排名”“选项”“票数”内容由rankings.js动态填充排序逻辑完全在前端完成。三份 JS 文件各司其职彼此零耦合-登录注册.js只处理表单提交事件、本地存储读写、跳转逻辑不碰投票数据-主页面.js只监听.vote-btn点击、更新votes对象、重绘柱状图、刷新票数文本不涉及用户认证-排行榜.js只从main.js暴露的getVotesData()方法或直接读取全局votes变量获取数据执行排序并渲染表格不关心用户是谁、怎么登录的。这种设计的好处是学生想改注册逻辑只动登录注册.js想换图表库只改主页面.js里的renderChart()函数想给排行榜加搜索框只在排行榜.html加 input 并在排行榜.js里加过滤逻辑。改一处编译都不用F5 刷新即生效。2.3 数据流设计内存变量 localStorage 的双层状态管理数据存储策略是这个系统稳健运行的底层逻辑。它采用“内存优先、持久兜底”的双层设计运行时状态内存变量全局声明let votes { 选项A: 0, 选项B: 0, 选项C: 0, 选项D: 0, 选项E: 0 };。所有投票操作点击按钮都直接修改这个对象。这是最快的响应方式保证了“实时性”——用户点下按钮的瞬间票数变量就变了后续的图表重绘、文本更新都基于此。持久化状态localStorage主页面.js在页面加载时window.addEventListener(load, init)会尝试从localStorage.votes读取 JSON 字符串并解析赋值给votes变量每次投票后立即执行localStorage.setItem(votes, JSON.stringify(votes))。这样实现了“关掉页面再打开票数还在”的效果但注意它只保存票数不保存用户登录状态那是sessionStorage的事。为什么不用sessionStorage存票数因为sessionStorage是会话级的同一个标签页关掉再开就没了而教学演示常需要“昨天投的票今天接着看排行榜”所以票数必须跨会话持久化。但用户登录状态不能跨会话否则学生 A 登录后学生 B 接着用同一台电脑打开可能直接看到 A 的用户名——这在课堂上会造成混乱。这种细粒度的状态划分正是前端工程师日常要做的权衡。提示localStorage的容量限制是 5MB 左右对于几百个选项、几万票的数据完全够用但要注意它只能存字符串所以JSON.stringify()和JSON.parse()是必备搭档漏掉任何一个都会导致null或undefined报错。3. 核心功能实现详解从点击按钮到柱状图跃动的完整链路3.1 用户认证模块用最朴素的 JS 实现“可信登录”登录注册.js的核心是两个函数handleRegister()和handleLogin()。它们的实现刻意避开了任何框架或复杂逻辑全部用原生 JS 完成目的是让学生看清每一步发生了什么。handleRegister()的流程如下1. 获取表单元素const usernameInput document.getElementById(username); const pwdInput document.getElementById(password);2. 基础校验检查usernameInput.value.trim() 或pwdInput.value.length 6任一成立则弹出alert(用户名不能为空密码至少6位)并return false;3. 构造用户对象const newUser { username: usernameInput.value.trim(), password: pwdInput.value };4. 读取现有用户列表let users JSON.parse(localStorage.getItem(users) || []);5. 防重名校验if (users.some(u u.username newUser.username)) { alert(用户名已存在); return false; }6. 写入存储users.push(newUser); localStorage.setItem(users, JSON.stringify(users));7. 跳转window.location.href 登录.html;这里的关键细节在于第 4 步和第 5 步。localStorage.getItem(users) || []是防御性编程——如果users键不存在getItem返回nullJSON.parse(null)会报错所以用|| []提供默认空数组字符串。第 5 步的some()方法比for循环更简洁且语义清晰“是否存在某个用户其用户名等于新用户名”。handleLogin()的逻辑类似但多了一步“凭证匹配”function handleLogin(e) { e.preventDefault(); // 阻止表单默认提交会刷新页面 const username document.getElementById(login-username).value.trim(); const password document.getElementById(login-password).value; const users JSON.parse(localStorage.getItem(users) || []); const matchedUser users.find(u u.username username u.password password); if (matchedUser) { sessionStorage.setItem(currentUser, username); window.location.href main.html; } else { alert(用户名或密码错误); } }注意e.preventDefault()的必要性——HTML 表单默认行为是提交并刷新页面这会中断我们的 JS 流程。find()方法返回第一个匹配项比filter()[0]更高效。整个过程没有 AJAX没有fetch就是一次同步的本地查找快到感觉不到延迟。注意教学场景下明文存密码是可接受的但务必在实训报告中强调“实际项目必须使用 bcrypt 加密HTTPS 传输”这是工程师的职业底线。3.2 投票与实时图表用 Canvas 手绘动态柱状图的底层逻辑主页面.js是整个系统的“心脏”它要完成三件事初始化页面、响应投票、重绘图表。其中图表绘制是最体现 JS 功底的部分。项目采用原生canvas实现而非 Chart.js 等库原因有三一是库会增加学习成本要学 API、配选项二是 canvas 让学生直观理解“坐标系”“像素绘制”“帧刷新”这些底层概念三是性能足够——几十个选项的图表canvas 绘制比 DOM 操作快一个数量级。柱状图绘制的核心函数renderChart()结构如下function renderChart() { const canvas document.getElementById(chart-canvas); const ctx canvas.getContext(2d); // 1. 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 2. 设置画布尺寸适配高清屏 const dpr window.devicePixelRatio || 1; canvas.width canvas.offsetWidth * dpr; canvas.height canvas.offsetHeight * dpr; ctx.scale(dpr, dpr); // 3. 计算参数柱子宽度、间距、最大票数 const barWidth 60; const barGap 20; const chartHeight canvas.offsetHeight; const maxValue Math.max(...Object.values(votes)); // 找出最高票数 // 4. 遍历选项逐个绘制柱子 let x 50; // 起始横坐标 Object.entries(votes).forEach(([option, count], index) { // 计算柱子高度按比例缩放避免超出画布 const barHeight maxValue 0 ? 0 : (count / maxValue) * (chartHeight - 100); // 绘制柱子矩形 ctx.fillStyle getColorByIndex(index); // 根据索引返回不同颜色 ctx.fillRect(x, chartHeight - barHeight - 50, barWidth, barHeight); // 绘制票数文本 ctx.fillStyle #333; ctx.font 14px Arial; ctx.textAlign center; ctx.fillText(count, x barWidth/2, chartHeight - barHeight - 60); // 绘制选项名 ctx.fillStyle #666; ctx.fillText(option, x barWidth/2, chartHeight - 20); x barWidth barGap; // 更新横坐标 }); }这段代码里藏着几个教学重点-设备像素比dpr适配canvas.width/height是 CSS 像素而getContext(2d)绘制的是物理像素。不乘dpr高清屏上图表会模糊。这是很多初学者踩坑的地方。-动态缩放计算barHeight (count / maxValue) * availableHeight是核心公式。maxValue为 0 时除零会得Infinity所以要加三元判断。-坐标系理解Canvas 的(0,0)在左上角y 轴向下增长所以柱子的y坐标是chartHeight - barHeight - 50减去底部留白而不是直觉上的50。-颜色管理getColorByIndex()是一个简单函数返回[#4CAF50, #2196F3, #FF9800, #E91E63, #9C27B0][index % 5]确保每个选项有固定色方便学生识别。投票事件绑定则极其简洁document.querySelectorAll(.vote-btn).forEach((btn, index) { btn.addEventListener(click, () { const optionName btn.dataset.option; // 从 button 的>// 方式一直接读取全局 votes 变量需确保 main.js 已加载 // 方式二提供 getVotesData() 函数供外部调用更规范 function getVotesData() { return JSON.parse(localStorage.getItem(votes) || {}); }第二步转换为可排序数组const votesData getVotesData(); // Object.entries() 将对象转为二维数组 [[选项A, 12], [选项B, 8], ...] // 然后 map 添加排名索引 const rankedArray Object.entries(votesData) .map(([option, count], index) ({ rank: index 1, option, count })) .sort((a, b) b.count - a.count); // 降序排列这里Object.entries()是关键它把键值对变成数组才能用sort()。sort()的比较函数(a,b) b.count - a.count必须返回数字正数表示b在前负数表示a在前零表示相等。初学者常写成(a,b) a.count b.count这是错的因为sort()需要数值而非布尔值。第三步渲染到表格const tableBody document.querySelector(#ranking-table tbody); tableBody.innerHTML ; // 清空旧内容 rankedArray.forEach(item { const row document.createElement(tr); row.innerHTML td${item.rank}/td td${item.option}/td td${item.count}/td ; tableBody.appendChild(row); });innerHTML直接拼接比循环createElement更高效教学场景下可接受。注意tbody的选择器而不是整个table避免重绘表头。实操心得排行榜页面首次打开时如果main.html还没被访问过localStorage.votes可能为空此时getVotesData()返回{}Object.entries({})得到空数组[]forEach不会执行表格就是空的——这符合预期不需要额外报错。4. 实操部署与调试指南从零开始跑通项目的完整步骤4.1 本地运行三步走5 分钟搞定这个项目最大的优势就是“开箱即用”无需任何构建工具。以下是标准操作流程第一步解压资源包下载的压缩包解压后你会看到一个文件夹如dzVzQOPMDi0KoMQbtljh-master-34a106b15e19902bfaab9bf5392f4bd0c767dc93里面包含所有 HTML、JS、CSS 文件和images文件夹。不要进入子文件夹直接在这个根目录下操作。第二步用浏览器打开注册页找到注册.html文件右键选择“在浏览器中打开”推荐 Chrome 或 Edge。此时你会看到一个简洁的注册表单。输入用户名如student01和密码如123456点击“注册”。如果弹出“注册成功”说明localStorage写入正常可以继续。第三步完成登录与投票关闭当前页面打开登录.html输入刚注册的用户名和密码点击“登录”。成功后会自动跳转到main.html。页面顶部显示“欢迎student01”中部是 5 个投票选项每个选项旁有“投票”按钮和初始票数“0”。随便点击一个按钮你会发现- 该选项的票数文本立刻变成“1”- 底部柱状图中对应的柱子变高并显示数字“1”- 刷新页面票数保持不变证明localStorage生效- 点击右上角“排行榜”跳转到排行榜.html表格中“排名1”的选项就是你刚投的那一个。整个过程不需要安装任何软件不启动任何服务纯粹靠浏览器自身能力运行。这是我给学生强调的第一课“前端的本质就是让浏览器干活”。4.2 常见问题排查那些让你抓耳挠腮的“小毛病”在 12 届学生的实训中以下问题是出现频率最高的我把它们整理成速查表附上根本原因和解决方案问题现象根本原因解决方案点击注册/登录按钮没反应页面直接刷新表单未阻止默认提交行为检查登录注册.js中handleRegister()和handleLogin()函数开头是否有e.preventDefault()确认事件监听是否正确绑定如form.addEventListener(submit, handleRegister)注册后登录提示“用户名或密码错误”localStorage.users中存储的密码被意外修改或登录时读取的users数组为空打开浏览器开发者工具F12切换到 Application → Storage → Local Storage找到users键点击右侧的“Edit”图标检查 JSON 字符串格式是否正确必须是[{username:xxx,password:yyy}]不能有多余逗号或引号若为空删除该键后重新注册main.html 页面空白控制台报错Uncaught ReferenceError: votes is not defined主页面.js中votes变量声明位置错误或main.html中script标签顺序不对确保votes变量在main.html的script标签内声明为全局变量如scriptlet votes {...};/script且该标签位于所有其他 JS 引入之前或者将votes声明移到主页面.js文件顶部柱状图不显示或显示为一条线Canvas 元素尺寸为 0或renderChart()函数未被调用检查main.html中canvas idchart-canvas width800 height400/canvas的width/height属性是否被 CSS 覆盖如stylewidth:100%在renderChart()开头加console.log(Rendering chart...)确认函数是否执行检查window.addEventListener(load, ...)是否包裹了初始化逻辑排行榜页面始终为空排行榜.js加载时机早于main.html的数据初始化或localStorage.votes从未被写入在排行榜.js的renderRankingTable()开头加console.log(Votes data:, getVotesData())查看控制台输出若为{}说明main.html还没被访问过需先投一票确保排行榜.html中script src排行榜.js/script放在/body之前提示Chrome 开发者工具的 Console控制台和 Application应用面板是你的两大利器。Console 查 JS 错误Application 看 localStorage 数据两者结合90% 的问题都能秒解。4.3 个性化定制三处最值得动手修改的“练手点”这个项目不是终点而是起点。我鼓励学生在跑通基础功能后动手改这三处既能巩固知识又能产出自己的作品第一处美化投票选项样式main.html中的选项列表是ul classoptions-list每个li包含按钮和票数。你可以- 给.options-list li添加border-radius: 8px; border: 1px solid #eee; padding: 12px; margin-bottom: 10px;让它看起来像卡片- 给.vote-btn添加background-color: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;并加:hover效果- 用:nth-child(odd)和:nth-child(even)给奇偶行设置不同背景色提升可读性。第二处增强排行榜交互排行榜.html当前是静态表格可以加- 在表格上方加一个input typetext idsearch-input placeholder搜索选项...- 在排行榜.js中监听input事件用filter()方法动态筛选rankedArray再重新渲染表格- 给“票数”列加点击排序功能点击一次升序再点一次降序用sort()的比较函数动态切换。第三处实现“防刷票”机制教学项目允许刷票但真实场景不行。你可以- 在主页面.js中添加全局变量let lastVoteTime 0;- 在投票事件中加入时间判断if (Date.now() - lastVoteTime 5000) { alert(请勿频繁投票5秒后重试); return; }然后lastVoteTime Date.now();- 进阶版用localStorage记录每个用户的投票时间戳实现 per-user 限流。这些改动都不需要动核心逻辑只是在现有骨架上添砖加瓦却能让学生真切体会到“前端不只是写页面更是写逻辑、控体验、保安全”。5. 教学价值延伸与项目升级路径从课堂作业到真实项目的跨越这个项目最迷人的地方在于它是一块“活”的垫脚石。当学生熟练掌握它之后下一步往哪里走路径非常清晰而且每一步都对应着真实岗位的技术栈演进。第一阶段夯实基础1-2周目标是吃透现有代码。建议学生做三件事-手写注释版把主页面.js中renderChart()的每一行都加上中文注释解释“这行在干什么”“为什么这么写”“不这么写会怎样”-流程图梳理用纸笔画出“用户注册→登录→投票→图表更新→排行榜渲染”的完整数据流向图标注每个环节涉及的 HTML 元素、JS 变量、localStorage 键-Bug 注入练习故意在登录注册.js中删掉e.preventDefault()观察现象并修复把votes变量名改成voteCounts看看哪些地方会报错从而理解变量作用域。第二阶段渐进升级2-4周在不破坏原有功能的前提下引入工业级实践-模块化改造用 ES6 Modules 替代全局变量。把votes封装成一个VoteManager类提供addVote(option)、getVotes()、reset()方法主页面.js和排行榜.js通过import { VoteManager } from ./vote-manager.js使用彻底解耦-状态管理初探引入一个极简的Store类统一管理votes和currentUser所有数据变更都通过store.dispatch({ type: VOTE_ADD, payload: 选项A })触发视图层监听store.subscribe(renderChart)这是 Redux 思想的雏形-响应式适配用 CSS Media Queries 让柱状图在手机上横向滚动显示而不是挤压变形排行榜表格在小屏下改为卡片堆叠布局。第三阶段对接真实后端4周这才是项目真正的“毕业设计”形态。保留现有前端结构只替换数据层-API 接口对接后端提供/api/votesGET 获取票数、/api/votesPOST 提交投票、/api/users/register注册等 RESTful 接口-AJAX 封装在主页面.js中把votes[option]和localStorage.setItem()替换为fetch(/api/votes, { method: POST, body: JSON.stringify({option}) })并处理then()和catch()-错误边界处理网络请求失败时在 UI 上显示友好的错误提示如“网络异常请检查连接”而不是控制台一片红字-Token 认证登录成功后后端返回 JWT Token前端将其存入localStorage.token后续所有请求的headers中带上Authorization: Bearer xxx。这条路走下来学生从“会写 JS”进化到“懂前端工程”再到“能对接后端”每一步都有迹可循。而这一切的起点就是这个打开就能跑的纯前端投票系统——它不炫技不堆砌就像一把磨得锃亮的螺丝刀专为拧紧初学者认知中的那颗关键螺丝而生。我个人在实际教学中发现那些最终能独立完成第三阶段的学生往往都是从认真修改主页面.js中的renderChart()函数开始的。他们盯着 canvas 坐标轴看了半小时终于搞懂为什么y坐标要减去barHeight他们反复调试sort()的比较函数直到b.count - a.count和a.count - b.count的区别刻进肌肉记忆。这种“慢下来抠细节”的习惯远比快速跑通一个花哨的 Vue 项目更有价值。因为前端开发的本质从来不是框架的堆砌而是对浏览器能力的深刻理解与精准驾驭。本文还有配套的精品资源点击获取简介这个前端项目包含完整的用户注册、登录流程支持用户在main.html页面完成投票操作投票结果通过JavaScript动态更新实时渲染为柱状图不刷新页面即可看到变化排行榜.html页面按得票数从高到低自动排序展示选项及对应票数所有交互逻辑由三个独立JS文件驱动主页面.js处理投票与图表、排行榜.js负责榜单数据排序与渲染、登录注册.js管理表单验证与本地模拟登录HTML结构清晰分离功能模块配套有实训报告文档软工192-08-李世权-实训报告.doc、项目截图素材images文件夹以及基础实训说明整个系统不依赖后端数据暂存于浏览器内存适合教学演示、课程设计或前端初学者动手练习。本文还有配套的精品资源点击获取