最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Node.js+MongoDB搭建RESTful?API實戰(zhàn)示例

 更新時間:2025年08月29日 08:24:39   作者:夜雨hiyeyu.com  
在當(dāng)今的Web開發(fā)領(lǐng)域,Node.js和MongoDB已經(jīng)成為構(gòu)建現(xiàn)代應(yīng)用程序的首選技術(shù)組合,本文就來介紹一下Node.js+MongoDB搭建RESTful?API實戰(zhàn)示例,感興趣的可以了解一下

第一章:引言與概述

1.1 為什么選擇 Node.js 和 MongoDB

在當(dāng)今的 Web 開發(fā)領(lǐng)域,Node.js 和 MongoDB 已經(jīng)成為構(gòu)建現(xiàn)代應(yīng)用程序的首選技術(shù)組合。Node.js 是一個基于 Chrome V8 引擎的 JavaScript 運行時環(huán)境,它采用事件驅(qū)動、非阻塞 I/O 模型,使其輕量且高效。MongoDB 是一個基于分布式文件存儲的 NoSQL 數(shù)據(jù)庫,由 C++ 語言編寫,旨在為 Web 應(yīng)用提供可擴展的高性能數(shù)據(jù)存儲解決方案。
這個技術(shù)棧的優(yōu)勢體現(xiàn)在多個方面。首先,JavaScript 全棧開發(fā)使得前后端開發(fā)人員能夠使用同一種語言,降低了學(xué)習(xí)成本和上下文切換的開銷。其次,JSON 數(shù)據(jù)格式在兩者之間的無縫流轉(zhuǎn)——Node.js 使用 JSON 作為數(shù)據(jù)交換格式,MongoDB 使用 BSON(Binary JSON)存儲數(shù)據(jù)——這種一致性大大簡化了開發(fā)流程。第三,非阻塞異步特性讓 Node.js 特別適合處理高并發(fā)的 I/O 密集型應(yīng)用,而 MongoDB 的橫向擴展能力能夠很好地支持這種應(yīng)用場景。

1.2 RESTful API 設(shè)計原則

REST(Representational State Transfer)是一種軟件架構(gòu)風(fēng)格,而不是標(biāo)準(zhǔn)或協(xié)議。它由 Roy Fielding 在 2000 年的博士論文中提出,定義了一組約束和原則,用于創(chuàng)建可擴展、可靠和高效的 Web 服務(wù)。
RESTful API 的核心原則包括:

  • 無狀態(tài)性(Stateless):每個請求都包含處理該請求所需的所有信息,服務(wù)器不存儲客戶端的狀態(tài)信息
  • 統(tǒng)一接口(Uniform Interface):使用標(biāo)準(zhǔn)的 HTTP 方法(GET、POST、PUT、DELETE 等)和狀態(tài)碼
  • 資源導(dǎo)向(Resource-Based):所有內(nèi)容都被抽象為資源,每個資源有唯一的標(biāo)識符(URI)
  • 表述性(Representation):客戶端與服務(wù)器交換的是資源的表述,而不是資源本身
  • 可緩存性(Cacheable):響應(yīng)應(yīng)該被標(biāo)記為可緩存或不可緩存,以提高性能
  • 分層系統(tǒng)(Layered System):客戶端不需要知道是否直接連接到最后端的服務(wù)器

1.3 教程目標(biāo)與內(nèi)容概述

本教程將帶領(lǐng)您從零開始構(gòu)建一個完整的博客平臺 API,實現(xiàn)文章的增刪改查、用戶認(rèn)證、文件上傳、分頁查詢等核心功能。通過這個實踐項目,您將掌握:

  1. Node.js 和 Express 框架的核心概念和用法
  2. MongoDB 數(shù)據(jù)庫的設(shè)計和操作
  3. Mongoose ODM 庫的高級用法
  4. RESTful API 的設(shè)計原則和最佳實踐
  5. JWT 身份認(rèn)證和授權(quán)機制
  6. 錯誤處理、日志記錄和性能優(yōu)化
  7. API 測試和文檔編寫
  8. 項目部署和運維考慮

第二章:環(huán)境搭建與項目初始化

2.1 開發(fā)環(huán)境要求

在開始之前,請確保您的系統(tǒng)滿足以下要求:
操作系統(tǒng)要求:

  • Windows 7 或更高版本
  • macOS 10.10 或更高版本
  • Ubuntu 16.04 或更高版本(推薦 LTS 版本)
    軟件依賴:
  • Node.js 版本 14.0.0 或更高版本(推薦 LTS 版本)
  • MongoDB 版本 4.0 或更高版本
  • npm 版本 6.0.0 或更高版本
  • Git 版本控制工具
    開發(fā)工具推薦:
  • 代碼編輯器:Visual Studio Code(推薦)、WebStorm、Sublime Text
  • API 測試工具:Postman、Insomnia、Thunder Client(VSCode 擴展)
  • 數(shù)據(jù)庫管理工具:MongoDB Compass、Studio 3T
  • 命令行工具:Windows Terminal、iTerm2(macOS)、Git Bash

2.2 安裝和配置 Node.js

Windows 系統(tǒng)安裝:

訪問 Node.js 官網(wǎng)(https://nodejs.org/)

下載 LTS 版本的安裝程序

運行安裝程序,按照向?qū)瓿砂惭b

安裝完成后,打開命令提示符或 PowerShell,驗證安裝:

node --version
npm --version

macOS 系統(tǒng)安裝:

推薦使用 Homebrew 包管理器:

# 安裝 Homebrew(如果尚未安裝)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 使用 Homebrew 安裝 Node.js
brew install node

Linux(Ubuntu)系統(tǒng)安裝:

# 使用 NodeSource 安裝腳本
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

2.3 安裝和配置 MongoDB

本地安裝 MongoDB:

  1. 訪問 MongoDB 官網(wǎng)(https://www.mongodb.com/try/download/community
  2. 選擇適合您操作系統(tǒng)的版本下載
  3. 按照官方文檔完成安裝和配置
    使用 MongoDB Atlas(云數(shù)據(jù)庫):
  4. 訪問 https://www.mongodb.com/atlas/database
  5. 注冊賬號并創(chuàng)建免費集群
  6. 配置網(wǎng)絡(luò)訪問和白名單
  7. 獲取連接字符串
    使用 Docker 運行 MongoDB:
# 拉取 MongoDB 鏡像
docker pull mongo:latest

# 運行 MongoDB 容器
docker run --name mongodb -d -p 27017:27017 -v ~/mongo/data:/data/db mongo:latest

# 帶認(rèn)證的啟動方式
docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -v ~/mongo/data:/data/db mongo:latest

2.4 項目初始化與結(jié)構(gòu)設(shè)計

創(chuàng)建項目目錄并初始化:

# 創(chuàng)建項目目錄
mkdir blog-api
cd blog-api

# 初始化 npm 項目
npm init -y

# 創(chuàng)建項目目錄結(jié)構(gòu)
mkdir -p src/
mkdir -p src/controllers
mkdir -p src/models
mkdir -p src/routes
mkdir -p src/middleware
mkdir -p src/utils
mkdir -p src/config
mkdir -p tests
mkdir -p docs

# 創(chuàng)建基礎(chǔ)文件
touch src/app.js
touch src/server.js
touch .env
touch .gitignore
touch README.md

安裝項目依賴:

# 生產(chǎn)依賴
npm install express mongoose dotenv bcryptjs jsonwebtoken cors helmet morgan multer express-rate-limit express-validator

# 開發(fā)依賴
npm install --save-dev nodemon eslint prettier eslint-config-prettier eslint-plugin-prettier jest supertest mongodb-memory-server

配置 package.json 腳本:

{
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint src/**/*.js",
    "lint:fix": "eslint src/**/*.js --fix",
    "format": "prettier --write src/**/*.js"
  }
}

配置 .gitignore 文件:

# 依賴目錄
node_modules/

# 環(huán)境變量文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# 日志文件
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# 運行時數(shù)據(jù)
pids/
*.pid
*.seed
*.pid.lock

# 覆蓋率目錄
coverage/
.nyc_output

# 系統(tǒng)文件
.DS_Store
Thumbs.db

# IDE文件
.vscode/
.idea/
*.swp
*.swo

# 操作系統(tǒng)文件
*.DS_Store
Thumbs.db

第三章:Express 服務(wù)器基礎(chǔ)搭建

3.1 創(chuàng)建基本的 Express 服務(wù)器

首先創(chuàng)建主要的應(yīng)用文件 src/app.js:

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
require('dotenv').config();

// 導(dǎo)入路由
const postRoutes = require('./routes/posts');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');

// 導(dǎo)入中間件
const errorHandler = require('./middleware/errorHandler');
const notFound = require('./middleware/notFound');

const app = express();

// 安全中間件
app.use(helmet());

// CORS 配置
app.use(cors({
  origin: process.env.NODE_ENV === 'production' 
    ? process.env.FRONTEND_URL 
    : 'http://localhost:3000',
  credentials: true
}));

// 速率限制
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分鐘
  max: 100, // 限制每個IP每15分鐘最多100個請求
  message: {
    error: '請求過于頻繁,請稍后再試。',
    status: 429
  }
});
app.use(limiter);

// 日志記錄
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));

// 解析請求體
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// 靜態(tài)文件服務(wù)
app.use('/uploads', express.static('uploads'));

// 路由配置
app.use('/api/posts', postRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

// 健康檢查端點
app.get('/api/health', (req, res) => {
  res.status(200).json({
    status: 'OK',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    environment: process.env.NODE_ENV || 'development'
  });
});

// 404處理
app.use(notFound);

// 錯誤處理
app.use(errorHandler);

module.exports = app;

創(chuàng)建服務(wù)器啟動文件 src/server.js:

const app = require('./app');
const connectDB = require('./config/database');

// 環(huán)境變量配置
const PORT = process.env.PORT || 5000;
const NODE_ENV = process.env.NODE_ENV || 'development';

// 優(yōu)雅關(guān)閉處理
const gracefulShutdown = (signal) => {
  console.log(`收到 ${signal},開始優(yōu)雅關(guān)閉服務(wù)器...`);
  process.exit(0);
};

// 啟動服務(wù)器
const startServer = async () => {
  try {
    // 連接數(shù)據(jù)庫
    await connectDB();
    
    // 啟動Express服務(wù)器
    const server = app.listen(PORT, () => {
      console.log(`
?? 服務(wù)器已啟動!
?? 環(huán)境: ${NODE_ENV}
?? 端口: ${PORT}
?? 時間: ${new Date().toLocaleString()}
?? 健康檢查: http://localhost:${PORT}/api/health
      `);
    });

    // 優(yōu)雅關(guān)閉處理
    process.on('SIGINT', () => gracefulShutdown('SIGINT'));
    process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));

    // 處理未捕獲的異常
    process.on('uncaughtException', (error) => {
      console.error('未捕獲的異常:', error);
      gracefulShutdown('uncaughtException');
    });

    process.on('unhandledRejection', (reason, promise) => {
      console.error('未處理的Promise拒絕:', reason);
      gracefulShutdown('unhandledRejection');
    });

  } catch (error) {
    console.error('服務(wù)器啟動失敗:', error);
    process.exit(1);
  }
};

// 啟動應(yīng)用
startServer();

3.2 環(huán)境變量配置

創(chuàng)建 .env 文件:

# 服務(wù)器配置
NODE_ENV=development
PORT=5000
FRONTEND_URL=http://localhost:3000

# 數(shù)據(jù)庫配置
MONGODB_URI=mongodb://localhost:27017/blog_api
MONGODB_URI_TEST=mongodb://localhost:27017/blog_api_test

# JWT配置
JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
JWT_EXPIRE=7d
JWT_COOKIE_EXPIRE=7

# 文件上傳配置
MAX_FILE_UPLOAD=5
FILE_UPLOAD_PATH=./uploads

# 速率限制配置
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100

# 郵件配置(可選)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_EMAIL=your_email@gmail.com
SMTP_PASSWORD=your_app_password
FROM_EMAIL=noreply@blogapi.com
FROM_NAME=Blog API

創(chuàng)建環(huán)境配置工具 src/config/env.js:

const Joi = require('joi');

// 環(huán)境變量驗證規(guī)則
const envVarsSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .default('development'),
  PORT: Joi.number().default(5000),
  MONGODB_URI: Joi.string().required().description('MongoDB連接字符串'),
  JWT_SECRET: Joi.string().required().description('JWT密鑰'),
  JWT_EXPIRE: Joi.string().default('7d').description('JWT過期時間'),
}).unknown().required();

// 驗證環(huán)境變量
const { value: envVars, error } = envVarsSchema.validate(process.env);
if (error) {
  throw new Error(`環(huán)境變量配置錯誤: ${error.message}`);
}

// 導(dǎo)出配置對象
module.exports = {
  env: envVars.NODE_ENV,
  port: envVars.PORT,
  mongoose: {
    url: envVars.MONGODB_URI + (envVars.NODE_ENV === 'test' ? '_test' : ''),
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    },
  },
  jwt: {
    secret: envVars.JWT_SECRET,
    expire: envVars.JWT_EXPIRE,
  },
};

第四章:MongoDB 數(shù)據(jù)庫連接與配置

4.1 數(shù)據(jù)庫連接配置

創(chuàng)建數(shù)據(jù)庫連接文件 src/config/database.js:

const mongoose = require('mongoose');
const config = require('./env');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(config.mongoose.url, config.mongoose.options);

    console.log(`
? MongoDB連接成功!
?? 主機: ${conn.connection.host}
?? 數(shù)據(jù)庫: ${conn.connection.name}
?? 狀態(tài): ${conn.connection.readyState === 1 ? '已連接' : '斷開'}
?? 時間: ${new Date().toLocaleString()}
    `);

    // 監(jiān)聽連接事件
    mongoose.connection.on('connected', () => {
      console.log('Mongoose已連接到數(shù)據(jù)庫');
    });

    mongoose.connection.on('error', (err) => {
      console.error('Mongoose連接錯誤:', err);
    });

    mongoose.connection.on('disconnected', () => {
      console.log('Mongoose已斷開數(shù)據(jù)庫連接');
    });

    // 進(jìn)程關(guān)閉時關(guān)閉數(shù)據(jù)庫連接
    process.on('SIGINT', async () => {
      await mongoose.connection.close();
      console.log('Mongoose連接已通過應(yīng)用終止關(guān)閉');
      process.exit(0);
    });

  } catch (error) {
    console.error('? MongoDB連接失敗:', error.message);
    process.exit(1);
  }
};

module.exports = connectDB;

4.2 數(shù)據(jù)庫連接優(yōu)化

創(chuàng)建高級數(shù)據(jù)庫配置 src/config/databaseAdvanced.js:

const mongoose = require('mongoose');
const config = require('./env');

class DatabaseManager {
  constructor() {
    this.isConnected = false;
    this.connection = null;
    this.retryAttempts = 0;
    this.maxRetryAttempts = 5;
    this.retryDelay = 5000; // 5秒
  }

  async connect() {
    try {
      // 連接選項配置
      const options = {
        ...config.mongoose.options,
        poolSize: 10, // 連接池大小
        bufferMaxEntries: 0, // 禁用緩沖
        connectTimeoutMS: 10000, // 10秒連接超時
        socketTimeoutMS: 45000, // 45秒套接字超時
        family: 4, // 使用IPv4
        useCreateIndex: true,
        useFindAndModify: false
      };

      this.connection = await mongoose.connect(config.mongoose.url, options);
      this.isConnected = true;
      this.retryAttempts = 0;

      this.setupEventListeners();
      return this.connection;

    } catch (error) {
      console.error('數(shù)據(jù)庫連接失敗:', error.message);
      
      if (this.retryAttempts < this.maxRetryAttempts) {
        this.retryAttempts++;
        console.log(`嘗試重新連接 (${this.retryAttempts}/${this.maxRetryAttempts})...`);
        
        await new Promise(resolve => setTimeout(resolve, this.retryDelay));
        return this.connect();
      } else {
        throw new Error(`數(shù)據(jù)庫連接失敗,已達(dá)到最大重試次數(shù): ${this.maxRetryAttempts}`);
      }
    }
  }

  setupEventListeners() {
    mongoose.connection.on('connected', () => {
      console.log('Mongoose已連接到數(shù)據(jù)庫');
      this.isConnected = true;
    });

    mongoose.connection.on('error', (error) => {
      console.error('Mongoose連接錯誤:', error);
      this.isConnected = false;
    });

    mongoose.connection.on('disconnected', () => {
      console.log('Mongoose已斷開數(shù)據(jù)庫連接');
      this.isConnected = false;
    });

    mongoose.connection.on('reconnected', () => {
      console.log('Mongoose已重新連接到數(shù)據(jù)庫');
      this.isConnected = true;
    });
  }

  async disconnect() {
    if (this.isConnected) {
      await mongoose.disconnect();
      this.isConnected = false;
      console.log('Mongoose連接已關(guān)閉');
    }
  }

  getConnectionStatus() {
    return {
      isConnected: this.isConnected,
      readyState: mongoose.connection.readyState,
      host: mongoose.connection.host,
      name: mongoose.connection.name,
      retryAttempts: this.retryAttempts
    };
  }
}

// 創(chuàng)建單例實例
const databaseManager = new DatabaseManager();

module.exports = databaseManager;

4.3 數(shù)據(jù)庫健康檢查中間件

創(chuàng)建數(shù)據(jù)庫健康檢查中間件 src/middleware/dbHealthCheck.js:

const mongoose = require('mongoose');

const dbHealthCheck = async (req, res, next) => {
  try {
    const dbState = mongoose.connection.readyState;
    
    // readyState 值說明:
    // 0 = disconnected
    // 1 = connected
    // 2 = connecting
    // 3 = disconnecting
    
    if (dbState !== 1) {
      return res.status(503).json({
        success: false,
        error: '數(shù)據(jù)庫連接異常',
        details: {
          status: dbState,
          statusText: ['斷開連接', '已連接', '連接中', '斷開中'][dbState],
          timestamp: new Date().toISOString()
        }
      });
    }
    
    // 執(zhí)行簡單的查詢來驗證數(shù)據(jù)庫響應(yīng)
    await mongoose.connection.db.admin().ping();
    
    next();
  } catch (error) {
    res.status(503).json({
      success: false,
      error: '數(shù)據(jù)庫健康檢查失敗',
      details: {
        message: error.message,
        timestamp: new Date().toISOString()
      }
    });
  }
};

module.exports = dbHealthCheck;

第五章:數(shù)據(jù)模型設(shè)計與 Mongoose 進(jìn)階

5.1 用戶模型設(shè)計

創(chuàng)建用戶模型 src/models/User.js:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('../config/env');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, '用戶名不能為空'],
    unique: true,
    trim: true,
    minlength: [3, '用戶名至少3個字符'],
    maxlength: [20, '用戶名不能超過20個字符'],
    match: [/^[a-zA-Z0-9_]+$/, '用戶名只能包含字母、數(shù)字和下劃線']
  },
  email: {
    type: String,
    required: [true, '郵箱不能為空'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '請輸入有效的郵箱地址']
  },
  password: {
    type: String,
    required: [true, '密碼不能為空'],
    minlength: [6, '密碼至少6個字符'],
    select: false // 默認(rèn)不返回密碼字段
  },
  role: {
    type: String,
    enum: ['user', 'author', 'admin'],
    default: 'user'
  },
  avatar: {
    type: String,
    default: 'default-avatar.png'
  },
  bio: {
    type: String,
    maxlength: [500, '個人簡介不能超過500個字符'],
    default: ''
  },
  website: {
    type: String,
    match: [/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, '請輸入有效的網(wǎng)址']
  },
  isVerified: {
    type: Boolean,
    default: false
  },
  isActive: {
    type: Boolean,
    default: true
  },
  lastLogin: {
    type: Date,
    default: Date.now
  }
}, {
  timestamps: true,
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// 虛擬字段:用戶的文章
userSchema.virtual('posts', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'author',
  justOne: false
});

// 索引優(yōu)化
userSchema.index({ email: 1 });
userSchema.index({ username: 1 });
userSchema.index({ createdAt: 1 });

// 密碼加密中間件
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  try {
    const salt = await bcrypt.genSalt(12);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// 比較密碼方法
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

// 生成JWT令牌方法
userSchema.methods.generateAuthToken = function() {
  return jwt.sign(
    { 
      userId: this._id,
      role: this.role 
    },
    config.jwt.secret,
    { 
      expiresIn: config.jwt.expire,
      issuer: 'blog-api',
      audience: 'blog-api-users'
    }
  );
};

// 獲取用戶基本信息(不包含敏感信息)
userSchema.methods.getPublicProfile = function() {
  const userObject = this.toObject();
  delete userObject.password;
  delete userObject.__v;
  return userObject;
};

// 靜態(tài)方法:通過郵箱查找用戶
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

// 靜態(tài)方法:通過用戶名查找用戶
userSchema.statics.findByUsername = function(username) {
  return this.findOne({ username: new RegExp('^' + username + '$', 'i') });
};

// 查詢中間件:自動過濾已刪除的用戶
userSchema.pre(/^find/, function(next) {
  this.find({ isActive: { $ne: false } });
  next();
});

module.exports = mongoose.model('User', userSchema);

5.2 文章模型設(shè)計

創(chuàng)建文章模型 src/models/Post.js:

const mongoose = require('mongoose');
const slugify = require('slugify');

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, '文章標(biāo)題不能為空'],
    trim: true,
    minlength: [5, '文章標(biāo)題至少5個字符'],
    maxlength: [200, '文章標(biāo)題不能超過200個字符']
  },
  slug: {
    type: String,
    unique: true,
    lowercase: true
  },
  content: {
    type: String,
    required: [true, '文章內(nèi)容不能為空'],
    minlength: [50, '文章內(nèi)容至少50個字符'],
    maxlength: [20000, '文章內(nèi)容不能超過20000個字符']
  },
  excerpt: {
    type: String,
    maxlength: [300, '文章摘要不能超過300個字符']
  },
  coverImage: {
    type: String,
    default: 'default-cover.jpg'
  },
  author: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: true
  },
  tags: [{
    type: String,
    trim: true,
    lowercase: true
  }],
  category: {
    type: String,
    required: [true, '文章分類不能為空'],
    trim: true,
    enum: [
      'technology', 'programming', 'design', 'business', 
      'lifestyle', 'travel', 'food', 'health', 'education'
    ]
  },
  status: {
    type: String,
    enum: ['draft', 'published', 'archived'],
    default: 'draft'
  },
  isFeatured: {
    type: Boolean,
    default: false
  },
  viewCount: {
    type: Number,
    default: 0
  },
  likeCount: {
    type: Number,
    default: 0
  },
  commentCount: {
    type: Number,
    default: 0
  },
  readingTime: {
    type: Number, // 閱讀時間(分鐘)
    default: 0
  },
  meta: {
    title: String,
    description: String,
    keywords: [String]
  },
  publishedAt: Date
}, {
  timestamps: true,
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// 虛擬字段:評論
postSchema.virtual('comments', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'post',
  justOne: false
});

// 虛擬字段:點贊用戶
postSchema.virtual('likes', {
  ref: 'Like',
  localField: '_id',
  foreignField: 'post',
  justOne: false
});

// 索引優(yōu)化
postSchema.index({ title: 'text', content: 'text' });
postSchema.index({ author: 1, createdAt: -1 });
postSchema.index({ category: 1, status: 1 });
postSchema.index({ tags: 1 });
postSchema.index({ status: 1, publishedAt: -1 });
postSchema.index({ slug: 1 });

// 生成slug中間件
postSchema.pre('save', function(next) {
  if (this.isModified('title') && this.title) {
    this.slug = slugify(this.title, {
      lower: true,
      strict: true,
      remove: /[*+~.()'"!:@]/g
    });
  }
  next();
});

// 計算閱讀時間和摘要中間件
postSchema.pre('save', function(next) {
  if (this.isModified('content')) {
    // 計算閱讀時間(按每分鐘200字計算)
    const wordCount = this.content.trim().split(/\s+/).length;
    this.readingTime = Math.ceil(wordCount / 200);
    
    // 自動生成摘要
    if (!this.excerpt) {
      this.excerpt = this.content
        .replace(/[#*`~>]/g, '') // 移除Markdown標(biāo)記
        .substring(0, 200)
        .trim() + '...';
    }
  }
  next();
});

// 發(fā)布文章時設(shè)置發(fā)布時間
postSchema.pre('save', function(next) {
  if (this.isModified('status') && this.status === 'published' && !this.publishedAt) {
    this.publishedAt = new Date();
  }
  next();
});

// 靜態(tài)方法:獲取已發(fā)布文章
postSchema.statics.getPublishedPosts = function() {
  return this.find({ status: 'published' });
};

// 靜態(tài)方法:按分類獲取文章
postSchema.statics.getPostsByCategory = function(category) {
  return this.find({ 
    category: category.toLowerCase(), 
    status: 'published' 
  });
};

// 靜態(tài)方法:搜索文章
postSchema.statics.searchPosts = function(query) {
  return this.find({
    status: 'published',
    $text: { $search: query }
  }, { score: { $meta: 'textScore' } })
  .sort({ score: { $meta: 'textScore' } });
};

// 實例方法:增加瀏覽量
postSchema.methods.incrementViews = function() {
  this.viewCount += 1;
  return this.save();
};

// 查詢中間件:自動填充作者信息
postSchema.pre(/^find/, function(next) {
  this.populate({
    path: 'author',
    select: 'username avatar bio'
  });
  next();
});

module.exports = mongoose.model('Post', postSchema);

5.3 評論和點贊模型

創(chuàng)建評論模型 src/models/Comment.js:

const mongoose = require('mongoose');

const commentSchema = new mongoose.Schema({
  content: {
    type: String,
    required: [true, '評論內(nèi)容不能為空'],
    trim: true,
    minlength: [1, '評論內(nèi)容至少1個字符'],
    maxlength: [1000, '評論內(nèi)容不能超過1000個字符']
  },
  author: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: true
  },
  post: {
    type: mongoose.Schema.ObjectId,
    ref: 'Post',
    required: true
  },
  parentComment: {
    type: mongoose.Schema.ObjectId,
    ref: 'Comment',
    default: null
  },
  likes: {
    type: Number,
    default: 0
  },
  isEdited: {
    type: Boolean,
    default: false
  },
  isApproved: {
    type: Boolean,
    default: true
  }
}, {
  timestamps: true,
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// 虛擬字段:回復(fù)評論
commentSchema.virtual('replies', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'parentComment',
  justOne: false
});

// 索引優(yōu)化
commentSchema.index({ post: 1, createdAt: -1 });
commentSchema.index({ author: 1 });
commentSchema.index({ parentComment: 1 });

// 保存后更新文章的評論計數(shù)
commentSchema.post('save', async function() {
  const Post = mongoose.model('Post');
  await Post.findByIdAndUpdate(this.post, {
    $inc: { commentCount: 1 }
  });
});

// 刪除后更新文章的評論計數(shù)
commentSchema.post('findOneAndDelete', async function(doc) {
  if (doc) {
    const Post = mongoose.model('Post');
    await Post.findByIdAndUpdate(doc.post, {
      $inc: { commentCount: -1 }
    });
  }
});

// 查詢中間件:自動填充作者信息
commentSchema.pre(/^find/, function(next) {
  this.populate({
    path: 'author',
    select: 'username avatar'
  }).populate({
    path: 'replies',
    populate: {
      path: 'author',
      select: 'username avatar'
    }
  });
  next();
});

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

創(chuàng)建點贊模型 src/models/Like.js:

const mongoose = require('mongoose');

const likeSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: true
  },
  post: {
    type: mongoose.Schema.ObjectId,
    ref: 'Post',
    required: true
  },
  type: {
    type: String,
    enum: ['like', 'love', 'laugh', 'wow', 'sad', 'angry'],
    default: 'like'
  }
}, {
  timestamps: true
});

// 復(fù)合唯一索引,確保一個用戶只能對一篇文章點一次贊
likeSchema.index({ user: 1, post: 1 }, { unique: true });

// 索引優(yōu)化
likeSchema.index({ post: 1 });
likeSchema.index({ user: 1 });

// 保存后更新文章的點贊計數(shù)
likeSchema.post('save', async function() {
  const Post = mongoose.model('Post');
  await Post.findByIdAndUpdate(this.post, {
    $inc: { likeCount: 1 }
  });
});

// 刪除后更新文章的點贊計數(shù)
likeSchema.post('findOneAndDelete', async function(doc) {
  if (doc) {
    const Post = mongoose.model('Post');
    await Post.findByIdAndUpdate(doc.post, {
      $inc: { likeCount: -1 }
    });
  }
});

module.exports = mongoose.model('Like', likeSchema);

到此這篇關(guān)于Node.js+MongoDB搭建RESTful API實戰(zhàn)示例的文章就介紹到這了,更多相關(guān)Node.js搭建RESTful API內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

麻阳| 西乌| 珲春市| 平武县| 天长市| 富阳市| 泉州市| 阜阳市| 华宁县| 沂水县| 阿鲁科尔沁旗| 高邮市| 佛坪县| 扬中市| 六盘水市| 江达县| 海口市| 抚松县| 赤城县| 澄江县| 平陆县| 波密县| 凌云县| 潜山县| 达拉特旗| 峨眉山市| 德庆县| 天台县| 梁平县| 嘉定区| 富源县| 甘孜县| 榆社县| 手机| 商河县| 高雄市| 南安市| 大渡口区| 中山市| 鄯善县| 丹东市|