使用 Node.js 开发在线评论和评分应用程序

使用 Node.js 开发在线评论和评分应用程序

引言

大家好,欢迎来到今天的讲座!今天我们要一起探讨如何使用 Node.js 来开发一个在线评论和评分应用程序。如果你对编程感兴趣,或者想了解如何构建一个功能丰富的 Web 应用程序,那么你来对地方了!我们将从头到尾一步步地讲解整个开发过程,确保每个步骤都清晰易懂。😊

在开始之前,让我们先明确一下我们要实现的目标:我们希望创建一个用户可以发表评论、给产品或服务打分的应用程序。用户还可以查看其他用户的评论和评分,并根据这些信息做出决策。听起来是不是很有趣?那就让我们开始吧!

1. 环境准备

1.1 安装 Node.js 和 npm

首先,我们需要安装 Node.js 和 npm(Node Package Manager)。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时,它允许我们在服务器端运行 JavaScript 代码。npm 是 Node.js 的包管理工具,可以帮助我们轻松安装和管理第三方库。

要安装 Node.js 和 npm,你可以访问 Node.js 官方网站 下载最新版本的安装包。安装完成后,打开终端或命令行工具,输入以下命令来验证是否安装成功:

node -v
npm -v

如果一切正常,你应该会看到类似如下的输出:

v16.13.0
7.24.0

1.2 初始化项目

接下来,我们需要为我们的项目创建一个新的文件夹,并初始化一个 package.json 文件。package.json 是 Node.js 项目的配置文件,它包含了项目的依赖、脚本等信息。

在终端中,导航到你想要创建项目的目录,然后执行以下命令:

mkdir comment-rating-app
cd comment-rating-app
npm init -y

npm init -y 会自动生成一个默认的 package.json 文件,而不需要你手动填写所有信息。现在,你的项目结构应该如下所示:

comment-rating-app/
├── package.json

1.3 安装必要的依赖

为了简化开发过程,我们将使用一些流行的 Node.js 框架和库。以下是我们将要安装的主要依赖:

  • Express:一个轻量级的 Web 框架,用于处理 HTTP 请求和响应。
  • Mongoose:一个 MongoDB 对象建模工具,帮助我们与 MongoDB 数据库进行交互。
  • Body-parser:用于解析 HTTP 请求体中的数据。
  • EJS:一个简单的模板引擎,用于渲染 HTML 页面。
  • dotenv:用于加载环境变量,方便我们在不同的环境中配置应用程序。

在终端中,执行以下命令来安装这些依赖:

npm install express mongoose body-parser ejs dotenv

安装完成后,你的 package.json 文件中应该会多出一个 dependencies 部分,类似于这样:

"dependencies": {
  "body-parser": "^1.19.0",
  "dotenv": "^10.0.0",
  "ejs": "^3.1.6",
  "express": "^4.17.1",
  "mongoose": "^5.12.3"
}

1.4 配置环境变量

为了保护敏感信息(如数据库连接字符串),我们将使用 .env 文件来存储环境变量。在项目根目录下创建一个名为 .env 的文件,并添加以下内容:

PORT=3000
MONGO_URI=mongodb://localhost:27017/comment-rating-app

PORT 是应用程序运行的端口号,MONGO_URI 是 MongoDB 数据库的连接字符串。如果你使用的是本地 MongoDB 实例,默认端口是 27017。如果你使用的是远程数据库(例如 MongoDB Atlas),请将 MONGO_URI 替换为相应的连接字符串。

1.5 创建基本的 Express 服务器

现在我们已经准备好开始编写代码了!首先,我们需要创建一个基本的 Express 服务器。在项目根目录下创建一个名为 app.js 的文件,并添加以下代码:

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

const app = express();
const PORT = process.env.PORT || 3000;

// 解析 JSON 和 URL 编码的请求体
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 设置 EJS 为模板引擎
app.set('view engine', 'ejs');

// 连接 MongoDB 数据库
mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then(() => {
  console.log('Connected to MongoDB');
}).catch((err) => {
  console.error('Failed to connect to MongoDB:', err);
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

这段代码做了以下几件事:

  1. 加载环境变量并配置 Express 应用程序。
  2. 使用 body-parser 解析请求体中的数据。
  3. 设置 EJS 作为模板引擎。
  4. 连接到 MongoDB 数据库。
  5. 启动服务器并监听指定的端口。

保存文件后,在终端中运行以下命令启动服务器:

node app.js

如果一切正常,你应该会在终端中看到类似如下的输出:

Connected to MongoDB
Server is running on http://localhost:3001

恭喜你,你已经成功创建了一个基本的 Express 服务器!接下来,我们将继续完善应用程序的功能。

2. 设计数据库模型

2.1 创建评论和评分模型

为了让用户能够发表评论和评分,我们需要设计一个合理的数据库模型。我们将使用 Mongoose 来定义两个模型:CommentRating

2.1.1 Comment 模型

Comment 模型将包含以下字段:

  • username:评论者的用户名。
  • content:评论的内容。
  • createdAt:评论创建的时间戳。
  • product:评论所属的产品 ID(假设我们有一个产品列表)。

在项目根目录下创建一个名为 models 的文件夹,并在其中创建一个名为 Comment.js 的文件。添加以下代码:

const mongoose = require('mongoose');

const commentSchema = new mongoose.Schema({
  username: { type: String, required: true },
  content: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }
});

module.exports = mongoose.model('Comment', commentSchema);

2.1.2 Rating 模型

Rating 模型将包含以下字段:

  • username:评分者的用户名。
  • score:评分的分数(1 到 5 之间的整数)。
  • createdAt:评分创建的时间戳。
  • product:评分所属的产品 ID。

models 文件夹中创建一个名为 Rating.js 的文件,并添加以下代码:

const mongoose = require('mongoose');

const ratingSchema = new mongoose.Schema({
  username: { type: String, required: true },
  score: { type: Number, min: 1, max: 5, required: true },
  createdAt: { type: Date, default: Date.now },
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }
});

module.exports = mongoose.model('Rating', ratingSchema);

2.2 创建产品模型

为了让评论和评分有归属的对象,我们还需要创建一个 Product 模型。Product 模型将包含以下字段:

  • name:产品的名称。
  • description:产品的描述。
  • price:产品的价格。
  • image:产品的图片 URL(可选)。

models 文件夹中创建一个名为 Product.js 的文件,并添加以下代码:

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  description: { type: String, required: true },
  price: { type: Number, required: true },
  image: { type: String }
});

module.exports = mongoose.model('Product', productSchema);

2.3 种子数据

为了方便测试,我们可以创建一些种子数据(即初始数据)。在项目根目录下创建一个名为 seeds.js 的文件,并添加以下代码:

const mongoose = require('mongoose');
const Product = require('./models/Product');
const Comment = require('./models/Comment');
const Rating = require('./models/Rating');

const seedProducts = [
  {
    name: 'Apple iPhone 13',
    description: 'The latest iPhone with advanced camera and A15 Bionic chip.',
    price: 999.99,
    image: 'https://example.com/iphone-13.jpg'
  },
  {
    name: 'Samsung Galaxy S21',
    description: 'A powerful Android smartphone with a stunning display and triple camera system.',
    price: 799.99,
    image: 'https://example.com/galaxy-s21.jpg'
  }
];

const seedComments = [
  {
    username: 'John Doe',
    content: 'Great phone! The camera is amazing!',
    product: mongoose.Types.ObjectId('60c3b2a1e7f0b8001b6d4e00')
  },
  {
    username: 'Jane Smith',
    content: 'I love the design of this phone. Very sleek!',
    product: mongoose.Types.ObjectId('60c3b2a1e7f0b8001b6d4e01')
  }
];

const seedRatings = [
  {
    username: 'John Doe',
    score: 5,
    product: mongoose.Types.ObjectId('60c3b2a1e7f0b8001b6d4e00')
  },
  {
    username: 'Jane Smith',
    score: 4,
    product: mongoose.Types.ObjectId('60c3b2a1e7f0b8001b6d4e01')
  }
];

async function seedDB() {
  try {
    await Product.deleteMany({});
    await Comment.deleteMany({});
    await Rating.deleteMany({});

    await Product.insertMany(seedProducts);
    await Comment.insertMany(seedComments);
    await Rating.insertMany(seedRatings);

    console.log('Database seeded successfully!');
  } catch (err) {
    console.error('Error seeding database:', err);
  }

  mongoose.connection.close();
}

seedDB();

这段代码会删除现有的产品、评论和评分数据,并插入一些新的数据。要运行种子脚本,请在终端中执行以下命令:

node seeds.js

如果你没有看到任何错误消息,说明种子数据已经成功插入到数据库中。

3. 构建路由和控制器

3.1 创建路由

为了处理不同的 HTTP 请求,我们需要为应用程序创建路由。我们将创建以下路由:

  • /products:显示所有产品的列表。
  • /products/:id:显示特定产品的详细信息,包括评论和评分。
  • /products/:id/comments:提交新评论。
  • /products/:id/ratings:提交新评分。

在项目根目录下创建一个名为 routes 的文件夹,并在其中创建一个名为 productRoutes.js 的文件。添加以下代码:

const express = require('express');
const router = express.Router();
const Product = require('../models/Product');
const Comment = require('../models/Comment');
const Rating = require('../models/Rating');

// 显示所有产品
router.get('/', async (req, res) => {
  try {
    const products = await Product.find();
    res.render('products/index', { products });
  } catch (err) {
    res.status(500).send(err.message);
  }
});

// 显示特定产品的详细信息
router.get('/:id', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id).populate('comments ratings');
    if (!product) return res.status(404).send('Product not found');
    res.render('products/show', { product });
  } catch (err) {
    res.status(500).send(err.message);
  }
});

// 提交新评论
router.post('/:id/comments', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) return res.status(404).send('Product not found');

    const comment = new Comment({
      username: req.body.username,
      content: req.body.content,
      product: product._id
    });

    await comment.save();
    product.comments.push(comment);
    await product.save();

    res.redirect(`/products/${product._id}`);
  } catch (err) {
    res.status(500).send(err.message);
  }
});

// 提交新评分
router.post('/:id/ratings', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) return res.status(404).send('Product not found');

    const rating = new Rating({
      username: req.body.username,
      score: req.body.score,
      product: product._id
    });

    await rating.save();
    product.ratings.push(rating);
    await product.save();

    res.redirect(`/products/${product._id}`);
  } catch (err) {
    res.status(500).send(err.message);
  }
});

module.exports = router;

3.2 注册路由

接下来,我们需要在 app.js 中注册这些路由。打开 app.js 文件,并在 app.listen 之前添加以下代码:

const productRoutes = require('./routes/productRoutes');

// 注册产品路由
app.use('/products', productRoutes);

3.3 创建控制器

为了保持代码的整洁和可维护性,我们可以将逻辑从路由中提取出来,放到控制器中。在项目根目录下创建一个名为 controllers 的文件夹,并在其中创建一个名为 productController.js 的文件。将 productRoutes.js 中的逻辑移到 productController.js 中,并在 productRoutes.js 中调用控制器方法。

productController.js 示例:

const Product = require('../models/Product');
const Comment = require('../models/Comment');
const Rating = require('../models/Rating');

exports.index = async (req, res) => {
  try {
    const products = await Product.find();
    res.render('products/index', { products });
  } catch (err) {
    res.status(500).send(err.message);
  }
};

exports.show = async (req, res) => {
  try {
    const product = await Product.findById(req.params.id).populate('comments ratings');
    if (!product) return res.status(404).send('Product not found');
    res.render('products/show', { product });
  } catch (err) {
    res.status(500).send(err.message);
  }
};

exports.createComment = async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) return res.status(404).send('Product not found');

    const comment = new Comment({
      username: req.body.username,
      content: req.body.content,
      product: product._id
    });

    await comment.save();
    product.comments.push(comment);
    await product.save();

    res.redirect(`/products/${product._id}`);
  } catch (err) {
    res.status(500).send(err.message);
  }
};

exports.createRating = async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) return res.status(404).send('Product not found');

    const rating = new Rating({
      username: req.body.username,
      score: req.body.score,
      product: product._id
    });

    await rating.save();
    product.ratings.push(rating);
    await product.save();

    res.redirect(`/products/${product._id}`);
  } catch (err) {
    res.status(500).send(err.message);
  }
};

productRoutes.js 示例:

const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');

// 显示所有产品
router.get('/', productController.index);

// 显示特定产品的详细信息
router.get('/:id', productController.show);

// 提交新评论
router.post('/:id/comments', productController.createComment);

// 提交新评分
router.post('/:id/ratings', productController.createRating);

module.exports = router;

4. 创建视图

4.1 创建产品列表页面

接下来,我们需要创建视图来展示产品列表和详细信息。我们将使用 EJS 作为模板引擎。在项目根目录下创建一个名为 views 的文件夹,并在其中创建一个名为 products 的文件夹。

views/products 文件夹中创建一个名为 index.ejs 的文件,并添加以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Products</title>
  <style>
    table {
      width: 100%;
      border-collapse: collapse;
    }
    th, td {
      padding: 8px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
  </style>
</head>
<body>
  <h1>Product List</h1>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Description</th>
        <th>Price</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <% products.forEach(product => { %>
        <tr>
          <td><%= product.name %></td>
          <td><%= product.description %></td>
          <td>$<%= product.price.toFixed(2) %></td>
          <td>
            <a href="/products/<%= product._id %>">View Details</a>
          </td>
        </tr>
      <% }) %>
    </tbody>
  </table>
</body>
</html>

4.2 创建产品详细页面

views/products 文件夹中创建一个名为 show.ejs 的文件,并添加以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= product.name %></title>
  <style>
    .container {
      max-width: 800px;
      margin: 0 auto;
    }
    .product-image {
      max-width: 100%;
      height: auto;
    }
    .comments, .ratings {
      margin-top: 20px;
    }
    .comment-form, .rating-form {
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1><%= product.name %></h1>
    <p><%= product.description %></p>
    <p>Price: $<%= product.price.toFixed(2) %></p>

    <% if (product.image) { %>
      <img src="<%= product.image %>" alt="<%= product.name %>" class="product-image">
    <% } %>

    <h2>Comments</h2>
    <div class="comments">
      <% if (product.comments.length > 0) { %>
        <ul>
          <% product.comments.forEach(comment => { %>
            <li>
              <strong><%= comment.username %>:</strong> <%= comment.content %> (<%= new Date(comment.createdAt).toLocaleString() %>)
            </li>
          <% }) %>
        </ul>
      <% } else { %>
        <p>No comments yet.</p>
      <% } %>
    </div>

    <h2>Ratings</h2>
    <div class="ratings">
      <% if (product.ratings.length > 0) { %>
        <ul>
          <% product.ratings.forEach(rating => { %>
            <li>
              <strong><%= rating.username %>:</strong> <%= rating.score %> stars (<%= new Date(rating.createdAt).toLocaleString() %>)
            </li>
          <% }) %>
        </ul>
      <% } else { %>
        <p>No ratings yet.</p>
      <% } %>
    </div>

    <h2>Leave a Comment</h2>
    <form action="/products/<%= product._id %>/comments" method="POST" class="comment-form">
      <label for="username">Username:</label>
      <input type="text" id="username" name="username" required>
      <br>
      <label for="content">Comment:</label>
      <textarea id="content" name="content" required></textarea>
      <br>
      <button type="submit">Submit Comment</button>
    </form>

    <h2>Rate This Product</h2>
    <form action="/products/<%= product._id %>/ratings" method="POST" class="rating-form">
      <label for="username">Username:</label>
      <input type="text" id="username" name="username" required>
      <br>
      <label for="score">Score (1-5):</label>
      <input type="number" id="score" name="score" min="1" max="5" required>
      <br>
      <button type="submit">Submit Rating</button>
    </form>

    <a href="/products">Back to Product List</a>
  </div>
</body>
</html>

4.3 创建布局文件

为了保持页面的一致性,我们可以创建一个布局文件来包含公共的 HTML 结构。在 views 文件夹中创建一个名为 layout.ejs 的文件,并添加以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
    }
    header {
      background-color: #333;
      color: white;
      padding: 10px;
      text-align: center;
    }
    main {
      padding: 20px;
    }
  </style>
</head>
<body>
  <header>
    <h1>Online Comment and Rating App</h1>
  </header>
  <main>
    <%- body %>
  </main>
</body>
</html>

4.4 修改视图渲染

最后,我们需要修改 app.js 中的视图渲染逻辑,以使用布局文件。打开 app.js 文件,并在 app.set('view engine', 'ejs') 之后添加以下代码:

app.set('views', path.join(__dirname, 'views'));
app.set('view options', { layout: 'layout' });

5. 测试应用程序

现在,我们的应用程序已经基本完成了!让我们来测试一下它的功能。

  1. 启动服务器:

    node app.js
  2. 打开浏览器,访问 http://localhost:3000/products,你应该会看到产品列表页面。

  3. 点击某个产品的“View Details”链接,进入产品详细页面。在这里,你可以查看该产品的评论和评分,也可以提交新的评论和评分。

  4. 尝试提交一些评论和评分,看看它们是否正确地保存到了数据库中,并显示在页面上。

6. 总结与展望

恭喜你,你已经成功创建了一个完整的在线评论和评分应用程序!通过这个项目,我们学习了如何使用 Node.js、Express、Mongoose 和 EJS 来构建一个功能丰富的 Web 应用程序。我们还了解了如何设计数据库模型、创建路由和控制器、以及使用模板引擎来渲染页面。

当然,这只是一个基础版本的应用程序。如果你有兴趣,可以尝试为它添加更多功能,例如:

  • 用户认证和授权:允许用户注册、登录,并限制只有登录用户才能提交评论和评分。
  • 分页和搜索:当产品数量较多时,可以实现分页功能,并允许用户根据关键字搜索产品。
  • API 接口:为应用程序提供 RESTful API,方便其他系统与之集成。
  • 前端优化:使用 React 或 Vue.js 等前端框架来提升用户体验。

希望今天的讲座对你有所帮助!如果你有任何问题或建议,欢迎随时与我交流。祝你在编程的道路上越走越远,编码愉快!🚀


感谢大家的聆听,我们下次再见!👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注