使用 Node.js 开发在线调查和投票应用程序
前言
大家好,欢迎来到今天的讲座!今天我们要一起探讨如何使用 Node.js 开发一个在线调查和投票应用程序。如果你是第一次接触 Node.js,或者对开发这类应用感到好奇,那么你来对地方了!我们将从头到尾一步步地构建这个应用,确保每个环节都清晰易懂。
在开始之前,先让我介绍一下 Node.js。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它允许我们在服务器端运行 JavaScript 代码。Node.js 的非阻塞 I/O 模型使得它非常适合处理高并发的网络应用,比如我们今天要做的在线调查和投票系统。
好了,废话不多说,让我们直接进入正题吧!😊
1. 项目规划与需求分析
1.1 项目背景
假设你是一家公司或组织的 IT 部门负责人,领导希望你能开发一个在线调查和投票系统,用于收集员工的意见和建议。这个系统需要具备以下功能:
- 用户可以创建调查问卷。
- 用户可以选择不同的问题类型(如单选、多选、文本输入等)。
- 用户可以匿名或实名参与投票。
- 系统能够实时统计投票结果,并生成图表展示。
- 管理员可以查看所有调查的详细数据,并导出为 CSV 文件。
听起来是不是有点复杂?别担心,我们会一步一步来实现这些功能。😎
1.2 技术栈选择
为了实现这个项目,我们需要选择合适的技术栈。以下是我们的技术选型:
- 后端:Node.js + Express.js
- 数据库:MongoDB(NoSQL 数据库,适合存储结构化和非结构化的数据)
- 前端:HTML + CSS + JavaScript(React.js 或 Vue.js 可以作为可选框架)
- 身份验证:JWT(JSON Web Token)
- 图表展示:Chart.js(轻量级的图表库)
1.3 项目架构
我们的项目将分为前后端两部分:
- 后端:负责处理业务逻辑、数据库操作、用户认证等功能。
- 前端:负责展示页面、与用户交互、发送请求给后端。
后端和前端通过 RESTful API 进行通信。我们可以使用 Postman 或者浏览器的开发者工具来测试 API。
1.4 数据库设计
为了更好地理解如何设计数据库,我们先来看看系统的数据模型。我们将有以下几个主要的集合(Collection):
- Users:存储用户信息(用户名、密码、角色等)。
- Surveys:存储调查问卷的信息(标题、描述、创建者、创建时间等)。
- Questions:存储每个调查中的问题(问题类型、选项、答案等)。
- Votes:存储用户的投票记录(用户 ID、调查 ID、问题 ID、选择的答案等)。
1.4.1 Users 表结构
字段名 | 类型 | 描述 |
---|---|---|
_id | ObjectId | 用户唯一标识 |
username | String | 用户名 |
password | String | 加密后的密码 |
role | String | 用户角色(管理员/普通用户) |
createdAt | Date | 用户创建时间 |
1.4.2 Surveys 表结构
字段名 | 类型 | 描述 |
---|---|---|
_id | ObjectId | 调查唯一标识 |
title | String | 调查标题 |
description | String | 调查描述 |
creator | ObjectId | 创建者 ID |
createdAt | Date | 创建时间 |
questions | Array | 问题列表 |
1.4.3 Questions 表结构
字段名 | 类型 | 描述 |
---|---|---|
_id | ObjectId | 问题唯一标识 |
surveyId | ObjectId | 所属调查 ID |
type | String | 问题类型(单选/多选/文本) |
questionText | String | 问题内容 |
options | Array | 选项列表 |
1.4.4 Votes 表结构
字段名 | 类型 | 描述 |
---|---|---|
_id | ObjectId | 投票唯一标识 |
userId | ObjectId | 投票用户 ID |
surveyId | ObjectId | 所属调查 ID |
questionId | ObjectId | 所属问题 ID |
answer | Mixed | 用户选择的答案 |
2. 环境搭建
2.1 安装 Node.js 和 npm
首先,我们需要安装 Node.js 和 npm(Node 包管理器)。你可以从 Node.js 官方网站下载最新版本的安装包。安装完成后,打开终端或命令行工具,输入以下命令来检查是否安装成功:
node -v
npm -v
如果看到版本号输出,说明安装成功!
2.2 初始化项目
接下来,我们创建一个新的项目文件夹,并初始化 npm 项目。在终端中执行以下命令:
mkdir survey-app
cd survey-app
npm init -y
npm init -y
会自动生成一个 package.json
文件,里面包含了项目的依赖信息。
2.3 安装依赖
我们需要安装一些常用的依赖包。执行以下命令来安装必要的依赖:
npm install express mongoose bcryptjs jsonwebtoken cors dotenv
- express:Node.js 的 web 框架,用于快速搭建 API。
- mongoose:用于连接 MongoDB 并进行数据操作的 ORM 库。
- bcryptjs:用于加密用户密码。
- jsonwebtoken:用于生成和验证 JWT。
- cors:用于解决跨域问题。
- dotenv:用于加载环境变量。
2.4 配置环境变量
为了保护敏感信息(如数据库连接字符串、JWT 密钥等),我们应该将这些信息放在 .env
文件中。在项目根目录下创建一个 .env
文件,并添加以下内容:
PORT=5000
MONGO_URI=mongodb://localhost:27017/survey-app
JWT_SECRET=your_jwt_secret_key
然后,在 index.js
中引入 dotenv
并加载环境变量:
require('dotenv').config();
2.5 连接 MongoDB
我们使用 Mongoose 来连接 MongoDB。在 index.js
中添加以下代码:
const mongoose = require('mongoose');
// 连接 MongoDB
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => {
console.log('MongoDB connected');
}).catch((err) => {
console.error('MongoDB connection error:', err);
});
2.6 启动服务器
现在,我们可以编写一个简单的 Express 服务器来测试是否一切正常。在 index.js
中添加以下代码:
const express = require('express');
const app = express();
const port = process.env.PORT || 5000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
保存文件后,在终端中运行以下命令启动服务器:
node index.js
如果一切顺利,你应该会在终端中看到类似以下的输出:
MongoDB connected
Server is running on port 5000
打开浏览器,访问 http://localhost:5000
,你应该能看到 "Hello, World!" 的欢迎页面。🎉
3. 用户认证
3.1 注册和登录
为了让用户能够创建调查并参与投票,我们需要实现用户注册和登录功能。我们将使用 JWT 来进行身份验证。
3.1.1 注册用户
首先,我们创建一个 auth.js
文件,用于处理用户注册和登录的路由。在 routes
文件夹下创建 auth.js
,并添加以下代码:
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
// 注册用户
router.post('/register', async (req, res) => {
const { username, password } = req.body;
// 检查用户是否已存在
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
// 创建新用户
const newUser = new User({
username,
password: hashedPassword,
});
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
});
module.exports = router;
3.1.2 登录用户
接下来,我们实现用户登录功能。在 auth.js
中添加以下代码:
// 登录用户
router.post('/login', async (req, res) => {
const { username, password } = req.body;
// 查找用户
const user = await User.findOne({ username });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// 生成 JWT
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h',
});
res.json({ token });
});
3.1.3 设置路由
在 index.js
中引入 auth.js
并设置路由:
const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes);
3.2 保护路由
为了确保只有经过身份验证的用户才能访问某些路由,我们需要在这些路由上添加 JWT 验证。我们可以在 middleware
文件夹下创建一个 authMiddleware.js
文件,并添加以下代码:
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Access denied' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ message: 'Invalid token' });
}
};
module.exports = authenticateToken;
然后,在需要保护的路由上使用这个中间件。例如,在 survey.js
中创建一个受保护的路由:
const express = require('express');
const router = express.Router();
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');
// 创建调查
router.post('/', authenticateToken, async (req, res) => {
const { title, description, questions } = req.body;
const newSurvey = new Survey({
title,
description,
creator: req.user.id,
questions,
});
await newSurvey.save();
res.status(201).json(newSurvey);
});
module.exports = router;
4. 创建调查问卷
4.1 定义调查模型
在 models
文件夹下创建一个 Survey.js
文件,定义调查的 Mongoose 模型:
const mongoose = require('mongoose');
const questionSchema = new mongoose.Schema({
type: { type: String, required: true },
questionText: { type: String, required: true },
options: [String],
});
const surveySchema = new mongoose.Schema({
title: { type: String, required: true },
description: { type: String, required: true },
creator: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
createdAt: { type: Date, default: Date.now },
questions: [questionSchema],
});
module.exports = mongoose.model('Survey', surveySchema);
4.2 创建调查路由
在 routes
文件夹下创建一个 survey.js
文件,用于处理调查相关的路由。我们已经实现了创建调查的功能,接下来我们再添加获取所有调查和获取单个调查的路由:
const express = require('express');
const router = express.Router();
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');
// 获取所有调查
router.get('/', async (req, res) => {
const surveys = await Survey.find().populate('creator', 'username');
res.json(surveys);
});
// 获取单个调查
router.get('/:id', async (req, res) => {
const survey = await Survey.findById(req.params.id).populate('creator', 'username');
if (!survey) {
return res.status(404).json({ message: 'Survey not found' });
}
res.json(survey);
});
// 创建调查
router.post('/', authenticateToken, async (req, res) => {
const { title, description, questions } = req.body;
const newSurvey = new Survey({
title,
description,
creator: req.user.id,
questions,
});
await newSurvey.save();
res.status(201).json(newSurvey);
});
module.exports = router;
4.3 设置路由
在 index.js
中引入 survey.js
并设置路由:
const surveyRoutes = require('./routes/survey');
app.use('/api/surveys', surveyRoutes);
5. 投票功能
5.1 定义投票模型
在 models
文件夹下创建一个 Vote.js
文件,定义投票的 Mongoose 模型:
const mongoose = require('mongoose');
const voteSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
surveyId: { type: mongoose.Schema.Types.ObjectId, ref: 'Survey', required: true },
questionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Question', required: true },
answer: { type: mongoose.Schema.Types.Mixed, required: true },
});
module.exports = mongoose.model('Vote', voteSchema);
5.2 创建投票路由
在 routes
文件夹下创建一个 vote.js
文件,用于处理投票相关的路由。我们可以通过 POST 请求提交投票,并确保用户不能重复投票:
const express = require('express');
const router = express.Router();
const Vote = require('../models/Vote');
const Survey = require('../models/Survey');
const authenticateToken = require('../middleware/authMiddleware');
// 提交投票
router.post('/:surveyId/:questionId', authenticateToken, async (req, res) => {
const { surveyId, questionId } = req.params;
const { answer } = req.body;
// 检查用户是否已经投过票
const existingVote = await Vote.findOne({
userId: req.user.id,
surveyId,
questionId,
});
if (existingVote) {
return res.status(400).json({ message: 'You have already voted' });
}
// 创建新投票
const newVote = new Vote({
userId: req.user.id,
surveyId,
questionId,
answer,
});
await newVote.save();
res.status(201).json(newVote);
});
module.exports = router;
5.3 设置路由
在 index.js
中引入 vote.js
并设置路由:
const voteRoutes = require('./routes/vote');
app.use('/api/surveys/:surveyId/questions/:questionId/vote', voteRoutes);
6. 实时统计与图表展示
6.1 实时统计
为了实现实时统计投票结果,我们可以在每次提交投票后更新调查的统计数据。我们可以在 vote.js
中添加一个钩子函数,在每次保存投票时更新调查的统计数据:
voteSchema.post('save', async function () {
const survey = await Survey.findById(this.surveyId);
const question = survey.questions.id(this.questionId);
if (question.type === 'multiple-choice') {
question.options.forEach((option, index) => {
if (this.answer === index) {
option.votes++;
}
});
} else if (question.type === 'text') {
question.answers.push(this.answer);
}
await survey.save();
});
6.2 图表展示
为了展示投票结果,我们可以使用 Chart.js 库。在前端页面中引入 Chart.js,并根据后端返回的数据生成图表。以下是一个简单的示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Survey Results</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<canvas id="myChart" width="400" height="200"></canvas>
<script>
fetch('/api/surveys/123')
.then(response => response.json())
.then(data => {
const labels = data.questions[0].options;
const votes = data.questions[0].votes;
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Votes',
data: votes,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
});
</script>
</body>
</html>
7. 导出数据
7.1 导出为 CSV
为了方便管理员导出调查数据,我们可以实现一个导出功能。在 survey.js
中添加一个导出路由:
const csvParser = require('json2csv').parse;
// 导出调查数据为 CSV
router.get('/:id/export', authenticateToken, async (req, res) => {
const survey = await Survey.findById(req.params.id).populate('creator', 'username');
const fields = ['Question', 'Options', 'Votes'];
const data = survey.questions.map(question => {
return {
Question: question.questionText,
Options: question.options.join(', '),
Votes: question.votes.join(', '),
};
});
const csv = csvParser({ data, fields });
res.setHeader('Content-Disposition', 'attachment; filename=survey-results.csv');
res.set('Content-Type', 'text/csv');
res.status(200).send(csv);
});
8. 总结
恭喜你!我们已经成功完成了一个完整的在线调查和投票应用程序的开发。通过这次讲座,你学会了如何使用 Node.js、Express、MongoDB 和 JWT 来构建一个功能丰富的后端 API,并结合前端技术展示了调查结果。
当然,这只是一个基础版本,你还可以根据实际需求进一步扩展和完善这个应用。比如:
- 添加更多的问题类型(如评分、矩阵等)。
- 实现更复杂的权限管理(如不同角色的用户可以有不同的操作权限)。
- 优化性能,使用缓存机制来提高响应速度。
- 添加更多的图表类型,提供更丰富的数据分析功能。
希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎随时留言交流。😊
祝你编码愉快,再见!👋