学习DeepSeek搭建Node.js+Express+MongoDB+Vue.js新闻发布系统


本文将详细介绍如何使用Node.js、Express、MongoDB和Vue3开发一个完整的新闻发布系统,包括富文本编辑器的集成和项目部署上线。

以下是新闻发布系统的项目结构图,采用树状形式展示,包含前后端主要文件和目录:

news-system/
├── backend/ # 后端Express项目
│ ├── node_modules/ # 后端依赖
│ ├── config/ # 配置文件
│ │ └── db.js # 数据库配置
│ ├── controllers/ # 控制器
│ │ ├── newsController.js # 新闻业务逻辑
│ │ └── authController.js # 认证逻辑(可选)
│ ├── models/ # 数据模型
│ │ ├── News.js # 新闻模型
│ │ └── User.js # 用户模型(可选)
│ ├── routes/ # 路由
│ │ ├── news.js # 新闻路由
│ │ └── auth.js # 认证路由(可选)
│ ├── middleware/ # 中间件
│ │ └── auth.js # 认证中间件(可选)
│ ├── uploads/ # 上传文件目录
│ ├── .env # 环境变量
│ ├── app.js # 主应用文件
│ ├── package.json
│ └── package-lock.json

└── frontend/ # 前端Vue3项目
├── node_modules/ # 前端依赖
├── public/ # 静态资源
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ │ ├── RichTextEditor.vue # 富文本编辑器
│ │ ├── NewsCard.vue # 新闻卡片组件
│ │ └── …
│ ├── composables/ # 组合式函数
│ ├── stores/ # Pinia状态管理
│ │ └── news.js # 新闻状态管理
│ ├── router/ # 路由配置
│ │ └── index.js
│ ├── views/ # 页面视图
│ │ ├── HomeView.vue # 首页
│ │ ├── NewsList.vue # 新闻列表
│ │ ├── NewsCreate.vue # 新闻创建
│ │ ├── NewsEdit.vue # 新闻编辑
│ │ └── NewsDetail.vue # 新闻详情
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── vite.config.js # Vite配置
├── package.json
└── package-lock.json

关键文件说明:

后端结构

  1. config/db.js – MongoDB数据库连接配置
  2. controllers/ – 处理业务逻辑
  3. models/News.js – 定义新闻数据模型
  4. routes/news.js – 新闻相关API路由
  5. uploads/ – 存储上传的图片文件
  6. app.js – Express应用入口

前端结构

  1. components/RichTextEditor.vue – 富文本编辑器组件
  2. stores/news.js – Pinia状态管理(新闻数据)
  3. views/News*.vue – 各新闻相关页面
  4. router/index.js – 前端路由配置
  5. vite.config.js – Vite构建配置

数据流示意图:

前端(Vue3) → API请求 → 后端(Express) → 数据库(MongoDB)
↑ |
| ↓
←─────── 响应数据 ──────────────←

开发工作流:

  1. 前端通过Pinia store发起API请求
  2. 后端路由接收请求并转发到控制器
  3. 控制器调用模型进行数据库操作
  4. 操作结果通过路由返回给前端
  5. 前端更新Pinia状态和UI显示

这个结构遵循了现代Web应用的最佳实践,保持了前后端分离的架构,组件化和模块化的设计便于维护和扩展。

一、项目概述

技术栈

  • 后端:Node.js + Express + MongoDB
  • 前端:Vue3 + Vite + Element Plus
  • 富文本编辑器:TinyMCE 或 Quill
  • 构建工具:Vite
  • 部署:Docker + Nginx 或 PM2

二、环境准备

1. 安装必要软件

  • Node.js (建议16.x或18.x LTS版本)
  • MongoDB (4.4或更高版本)
  • Git
  • 代码编辑器(VSCode推荐)

2. 检查安装

node -v
npm -v
mongod --version
git --version

三、后端开发(Express + MongoDB)

1. 创建项目目录

mkdir news-system
cd news-system
mkdir backend
cd backend
npm init -y

2. 安装依赖

npm install express mongoose cors body-parser multer dotenv
npm install --save-dev nodemon

3. 基础Express应用结构

创建app.js: javascript

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

dotenv.config();

const app = express();

<em>// 中间件</em>
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

<em>// 数据库连接</em>
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));

<em>// 路由</em>
app.get('/', (req, res) => {
  res.send('News System API');
});

<em>// 错误处理</em>
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
JavaScript

创建.env文件:

MONGODB_URI=mongodb://localhost:27017/news-system
PORT=5000
JWT_SECRET=your_jwt_secret

4. 创建新闻模型和路由

创建models/News.js:javascript

const mongoose = require('mongoose');

const newsSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  category: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  coverImage: { type: String },
  isPublished: { type: Boolean, default: false },
  publishedAt: { type: Date },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('News', newsSchema);

创建routes/news.js:javascript

const express = require('express');
const router = express.Router();
const News = require('../models/News');
const multer = require('multer');
const path = require('path');

<em>// 配置multer用于文件上传</em>
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + path.extname(file.originalname));
  }
});

const upload = multer({ storage: storage });

<em>// 创建新闻</em>
router.post('/', upload.single('coverImage'), async (req, res) => {
  try {
    const { title, content, category } = req.body;
    const coverImage = req.file ? req.file.path : '';
    
    const news = new News({
      title,
      content,
      category,
      coverImage,
      isPublished: true,
      publishedAt: new Date()
    });
    
    await news.save();
    res.status(201).json(news);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

<em>// 获取所有新闻</em>
router.get('/', async (req, res) => {
  try {
    const news = await News.find().sort({ createdAt: -1 });
    res.json(news);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

<em>// 获取单条新闻</em>
router.get('/:id', async (req, res) => {
  try {
    const news = await News.findById(req.params.id);
    if (!news) return res.status(404).json({ error: 'News not found' });
    res.json(news);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

<em>// 更新新闻</em>
router.put('/:id', upload.single('coverImage'), async (req, res) => {
  try {
    const { title, content, category } = req.body;
    const updateData = { title, content, category, updatedAt: new Date() };
    
    if (req.file) {
      updateData.coverImage = req.file.path;
    }
    
    const news = await News.findByIdAndUpdate(req.params.id, updateData, { new: true });
    if (!news) return res.status(404).json({ error: 'News not found' });
    res.json(news);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

<em>// 删除新闻</em>
router.delete('/:id', async (req, res) => {
  try {
    const news = await News.findByIdAndDelete(req.params.id);
    if (!news) return res.status(404).json({ error: 'News not found' });
    res.json({ message: 'News deleted successfully' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

module.exports = router;
JavaScript

app.js中添加路由:javascript

<em>// 在数据库连接后添加</em>
const newsRoutes = require('./routes/news');
app.use('/api/news', newsRoutes);

<em>// 静态文件访问</em>
app.use('/uploads', express.static('uploads'));
JavaScript

5. 添加用户认证(可选)

创建models/User.jsroutes/auth.js,实现JWT认证。

6. 启动后端

全局安装 nodemon(推荐)
npm install -g nodemon

检查 nodemon 是否安装成功:
nodemon -v

nodemon app.js

四、前端开发(Vue3 + Vite)

1. 创建Vue3项目

cd ..
npm create vite@latest frontend --template vue
cd frontend

2. 安装必要依赖

npm install axios vue-router@4 pinia element-plus @element-plus/icons-vue
npm install @tinymce/tinymce-vue tinymce  # 富文本编辑器
# 或 npm install quill @vueup/vue-quill@beta  # 另一个富文本编辑器选项

3. 配置Vite

修改vite.config.js: javascript

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

<em>// https://vitejs.dev/config/</em>
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})
JavaScript

4. 配置Element Plus

修改main.js:javascript

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

<em>// Element Plus</em>
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)

<em>// 注册Element Plus图标</em>
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
JavaScript

5. 创建路由

创建src/router/index.js:javascript

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import NewsList from '../views/NewsList.vue'
import NewsCreate from '../views/NewsCreate.vue'
import NewsEdit from '../views/NewsEdit.vue'
import NewsDetail from '../views/NewsDetail.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/news',
    name: 'news-list',
    component: NewsList
  },
  {
    path: '/news/create',
    name: 'news-create',
    component: NewsCreate
  },
  {
    path: '/news/:id/edit',
    name: 'news-edit',
    component: NewsEdit,
    props: true
  },
  {
    path: '/news/:id',
    name: 'news-detail',
    component: NewsDetail,
    props: true
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router
JavaScript

6. 创建Pinia store

创建src/stores/news.js:javascript

import { defineStore } from 'pinia'
import axios from 'axios'

export const useNewsStore = defineStore('news', {
  state: () => ({
    newsList: [],
    currentNews: null,
    loading: false,
    error: null
  }),
  actions: {
    async fetchNews() {
      this.loading = true
      try {
        const response = await axios.get('/api/news')
        this.newsList = response.data
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    async fetchNewsById(id) {
      this.loading = true
      try {
        const response = await axios.get(`/api/news/${id}`)
        this.currentNews = response.data
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    async createNews(newsData) {
      this.loading = true
      try {
        const formData = new FormData()
        formData.append('title', newsData.title)
        formData.append('content', newsData.content)
        formData.append('category', newsData.category)
        if (newsData.coverImage) {
          formData.append('coverImage', newsData.coverImage)
        }
        
        const response = await axios.post('/api/news', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
        return response.data
      } catch (error) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    },
    async updateNews({ id, ...newsData }) {
      this.loading = true
      try {
        const formData = new FormData()
        formData.append('title', newsData.title)
        formData.append('content', newsData.content)
        formData.append('category', newsData.category)
        if (newsData.coverImage) {
          formData.append('coverImage', newsData.coverImage)
        }
        
        const response = await axios.put(`/api/news/${id}`, formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
        return response.data
      } catch (error) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    },
    async deleteNews(id) {
      this.loading = true
      try {
        await axios.delete(`/api/news/${id}`)
      } catch (error) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    }
  }
})
JavaScript

7. 实现富文本编辑器(TinyMCE)

创建src/components/RichTextEditor.vue:

<template>
  <div class="rich-text-editor">
    <editor
      v-model="content"
      api-key="your-tinymce-api-key"  
      :init="editorConfig"
    /><!--//从TinyMCE官网获取免费API key-->
  </div>
</template>

<script>
import Editor from '@tinymce/tinymce-vue'
import { ref, watch } from 'vue'

export default {
  name: 'RichTextEditor',
  components: {
    Editor
  },
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const content = ref(props.modelValue)
    
    const editorConfig = {
      height: 500,
      menubar: true,
      plugins: [
        'advlist autolink lists link image charmap print preview anchor',
        'searchreplace visualblocks code fullscreen',
        'insertdatetime media table paste code help wordcount'
      ],
      toolbar:
        'undo redo | formatselect | bold italic backcolor | \
        alignleft aligncenter alignright alignjustify | \
        bullist numlist outdent indent | removeformat | help | image',
      images_upload_handler: (blobInfo, success, failure) => {
        <em>// 这里可以实现图片上传逻辑</em>
        const formData = new FormData()
        formData.append('file', blobInfo.blob(), blobInfo.filename())
        
        axios.post('/api/upload', formData)
          .then(res => {
            success(res.data.location)
          })
          .catch(err => {
            failure('Image upload failed')
            console.error(err)
          })
      }
    }
    
    watch(content, (newVal) => {
      emit('update:modelValue', newVal)
    })
    
    watch(() => props.modelValue, (newVal) => {
      if (newVal !== content.value) {
        content.value = newVal
      }
    })
    
    return {
      content,
      editorConfig
    }
  }
}
</script>

<style>
.tox-tinymce {
  border-radius: 4px !important;
}
</style>
JavaScript

8. 创建新闻相关组件

创建src/views/NewsCreate.vue:

<template>
  <div class="news-create">
    <el-card>
      <template #header>
        <div class="card-header">
          <h2>创建新闻</h2>
        </div>
      </template>
      
      <el-form :model="form" label-width="120px" ref="formRef">
        <el-form-item label="新闻标题" prop="title" required>
          <el-input v-model="form.title" placeholder="请输入新闻标题"></el-input>
        </el-form-item>
        
        <el-form-item label="新闻分类" prop="category" required>
          <el-select v-model="form.category" placeholder="请选择分类">
            <el-option label="政治" value="politics"></el-option>
            <el-option label="经济" value="economy"></el-option>
            <el-option label="科技" value="technology"></el-option>
            <el-option label="体育" value="sports"></el-option>
            <el-option label="娱乐" value="entertainment"></el-option>
          </el-select>
        </el-form-item>
        
        <el-form-item label="封面图片" prop="coverImage">
          <el-upload
            class="avatar-uploader"
            action="#"
            :show-file-list="false"
            :auto-upload="false"
            :on-change="handleCoverImageChange"
          >
            <img v-if="form.coverImageUrl" :src="form.coverImageUrl" class="avatar" />
            <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
          </el-upload>
        </el-form-item>
        
        <el-form-item label="新闻内容" prop="content" required>
          <RichTextEditor v-model="form.content" />
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="submitForm">发布新闻</el-button>
          <el-button @click="resetForm">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Plus } from '@element-plus/icons-vue'
import { useNewsStore } from '@/stores/news'
import RichTextEditor from '@/components/RichTextEditor.vue'

const router = useRouter()
const newsStore = useNewsStore()

const form = ref({
  title: '',
  category: '',
  content: '',
  coverImage: null,
  coverImageUrl: ''
})

const formRef = ref(null)

const handleCoverImageChange = (file) => {
  form.value.coverImage = file.raw
  form.value.coverImageUrl = URL.createObjectURL(file.raw)
}

const submitForm = async () => {
  try {
    await newsStore.createNews(form.value)
    ElMessage.success('新闻创建成功')
    router.push({ name: 'news-list' })
  } catch (error) {
    ElMessage.error('新闻创建失败: ' + error.message)
  }
}

const resetForm = () => {
  if (formRef.value) {
    formRef.value.resetFields()
  }
  form.value.content = ''
  form.value.coverImage = null
  form.value.coverImageUrl = ''
}
</script>

<style scoped>
.avatar-uploader .avatar {
  width: 178px;
  height: 178px;
  display: block;
}
.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  text-align: center;
}
</style>
JavaScript

类似地创建NewsList.vueNewsEdit.vueNewsDetail.vue

五、项目部署

1. 生产环境准备

后端:

cd backend
npm install --production

前端:

cd frontend
npm run build

本地访问构建后的静态文件

构建完成后会生成 dist 目录(包含编译后的静态文件),你可以通过以下方式访问:

使用本地静态服务器

# 安装 serve 工具(若未安装)
npm install -g serve

# 进入 dist 目录并启动服务
cd dist
serve -s

访问网址 http://localhost:3000

2. 使用PM2部署

安装PM2:

npm install -g pm2

启动后端:

cd backend
pm2 start app.js --name "news-system-api"

启动前端服务(如果需要):

cd frontend
pm2 serve dist 8080 --name "news-system-frontend" --spa

3. 使用Docker部署

创建docker-compose.yml:yaml

version: '3.8'

services:
  mongodb:
    image: mongo:5.0
    container_name: mongodb
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    volumes:
      - mongodb_data:/data/db
    ports:
      - "27017:27017"
    restart: always

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: backend
    environment:
      MONGODB_URI: mongodb://root:example@mongodb:27017/news-system?authSource=admin
      PORT: 5000
    ports:
      - "5000:5000"
    depends_on:
      - mongodb
    restart: always

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: frontend
    ports:
      - "8080:80"
    depends_on:
      - backend
    restart: always

volumes:
  mongodb_data:

后端Dockerfile:dockerfile

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install --production

COPY . .

EXPOSE 5000

CMD ["node", "app.js"]

前端Dockerfile:dockerfile

FROM node:16-alpine as builder

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

FROM nginx:alpine

COPY --from=builder /app/dist /usr/share/nginx/html

COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

前端nginx.conf:nginx

server {
  listen 80;
  
  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html;
  }
  
  location /api {
    proxy_pass http://backend:5000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

启动服务:

docker-compose up -d --build

六、常见问题解决

  1. Vue3依赖版本冲突:
    • 确保所有Vue相关包版本一致
    • 使用npm ls vue检查版本
    • 可以尝试删除node_modulespackage-lock.json后重新安装
  2. 富文本编辑器问题:
    • TinyMCE需要API key(免费版可用)
    • 图片上传需要正确配置后端接口
    • 如果使用Quill,确保安装@vueup/vue-quill@beta版本
  3. 跨域问题:
    • 开发时配置Vite代理
    • 生产环境确保Nginx正确配置代理
  4. 静态资源404:
    • 确保上传目录存在且有写入权限
    • 生产环境需要将上传目录映射到容器中
  5. MongoDB连接问题:
    • 检查MongoDB服务是否运行
    • 验证连接字符串是否正确
    • 生产环境需要设置认证

七、项目优化建议

  1. 性能优化:
    • 实现新闻分页
    • 添加缓存层(Redis)
    • 图片压缩和CDN
  2. 安全优化:
    • 实现JWT认证
    • 输入验证和XSS防护
    • 限制上传文件类型和大小
  3. 功能扩展:
    • 新闻分类管理
    • 用户评论功能
    • 新闻审核流程
    • 数据统计和分析
  4. 监控和日志:
    • 添加日志系统
    • 实现错误监控
    • 性能监控

通过以上步骤,你应该能够成功搭建并运行一个基于Node.js + Express + MongoDB + Vue3的新闻发布系统。如果在具体实施过程中遇到问题,可以根据错误信息搜索解决方案或查阅相关文档。


发表评论