告别复杂后端:5分钟实现json-server文件上传完整方案

告别复杂后端:5分钟实现json-server文件上传完整方案

【免费下载链接】json-server Get a full fake REST API with zero coding in less than 30 seconds (seriously) 【免费下载链接】json-server 项目地址: https://gitcode.com/GitHub_Trending/js/json-server

你是否还在为前端演示需要搭建复杂的文件上传后端而烦恼?是否遇到过API开发到一半,前端却因缺少文件存储服务而停滞的困境?本文将带你通过5个步骤,为json-server添加完整的文件上传功能,无需编写任何后端代码,即可实现生产级别的多媒体文件管理。

读完本文你将获得:

  • 完整的json-server文件上传实现方案
  • 支持多文件上传、类型验证、大小限制的中间件
  • 带进度显示的前端上传组件
  • 文件元数据管理与RESTful API集成
  • 生产环境部署的性能优化策略

为什么需要为json-server添加文件上传?

json-server作为一款零代码REST API工具,已经成为前端开发的必备工具。但原生json-server存在明显短板:

功能痛点传统解决方案json-server扩展方案
文件存储购买云存储服务本地文件系统+API封装
上传接口编写Node.js后端中间件扩展+几行配置
进度反馈复杂的XMLHttpRequest处理标准化FormData接口
权限控制集成OAuth/Token验证简单的API密钥验证
元数据管理设计数据库表结构自动生成JSON数据文件

实现原理与架构设计

文件上传功能的核心是在json-server的请求处理流程中插入文件处理中间件。整体架构如下:

mermaid

关键技术点包括:

  • 使用multer处理multipart/form-data请求
  • 文件存储路径与URL映射
  • 自动生成文件元数据记录
  • 与json-server现有REST API无缝集成

步骤1:环境准备与依赖安装

首先确保你的开发环境满足以下要求:

  • Node.js 16.x或更高版本
  • npm或yarn包管理器
  • 已安装json-server(v0.17.0+)

创建项目并安装必要依赖:

mkdir json-server-file-upload && cd json-server-file-upload
npm init -y
npm install json-server multer cors express --save
npm install nodemon --save-dev

创建基础项目结构:

mkdir -p src/middlewares src/routes public/uploads
touch server.js db.json .gitignore

步骤2:实现文件上传核心中间件

创建文件上传中间件src/middlewares/upload.js

const multer = require('multer');
const path = require('path');
const fs = require('fs');

// 确保上传目录存在
const uploadDir = path.join(__dirname, '../../public/uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

// 配置存储引擎
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    // 按日期创建子目录,避免单目录文件过多
    const date = new Date();
    const year = date.getFullYear().toString();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const dayDir = path.join(uploadDir, year, month);
    
    if (!fs.existsSync(dayDir)) {
      fs.mkdirSync(dayDir, { recursive: true });
    }
    
    cb(null, dayDir);
  },
  filename: (req, file, cb) => {
    // 生成唯一文件名:时间戳+随机数+原扩展名
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const ext = path.extname(file.originalname);
    const baseName = path.basename(file.originalname, ext);
    // 保留原文件名并添加唯一标识
    const fileName = `${baseName}-${uniqueSuffix}${ext}`;
    cb(null, fileName);
  }
});

// 文件过滤函数
const fileFilter = (req, file, cb) => {
  // 允许的MIME类型
  const allowedTypes = [
    'image/jpeg', 'image/png', 'image/gif', 
    'application/pdf', 'video/mp4', 'audio/mpeg'
  ];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    // 拒绝上传并返回错误信息
    cb(new Error(`不支持的文件类型: ${file.mimetype}`), false);
  }
};

// 配置上传限制
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5 // 最多5个文件
  },
  fileFilter: fileFilter
});

module.exports = upload;

步骤3:扩展json-server实现文件API

创建主服务器文件server.js

const jsonServer = require('json-server');
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const upload = require('./src/middlewares/upload');

// 初始化Express应用
const app = express();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults({
  static: './public',
  logger: true
});

// 扩展默认中间件
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 自定义文件上传路由
app.post('/upload', upload.array('files', 5), (req, res) => {
  try {
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({
        error: '没有文件被上传'
      });
    }
    
    // 读取现有数据库
    const dbPath = path.join(__dirname, 'db.json');
    const db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
    
    // 确保files集合存在
    if (!db.files) {
      db.files = [];
    }
    
    // 处理每个上传的文件
    const fileRecords = req.files.map(file => {
      const fileUrl = `/uploads/${path.basename(path.dirname(file.path))}/${file.filename}`;
      const fileRecord = {
        id: Date.now().toString(),
        filename: file.originalname,
        path: file.path.replace(path.join(__dirname, 'public'), ''),
        url: fileUrl,
        mimetype: file.mimetype,
        size: file.size,
        uploadedAt: new Date().toISOString(),
        uploader: req.body.uploader || 'anonymous'
      };
      
      // 添加到数据库
      db.files.push(fileRecord);
      return fileRecord;
    });
    
    // 保存数据库更改
    fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
    
    // 返回上传结果
    res.status(201).json({
      message: '文件上传成功',
      files: fileRecords
    });
  } catch (error) {
    res.status(500).json({
      error: '文件上传失败',
      details: error.message
    });
  }
});

// 删除文件端点
app.delete('/files/:id', (req, res) => {
  try {
    const dbPath = path.join(__dirname, 'db.json');
    const db = JSON.parse(fs.readFileSync(dbPath, 'utf8'));
    
    if (!db.files) {
      return res.status(404).json({ error: '文件记录不存在' });
    }
    
    const fileIndex = db.files.findIndex(file => file.id === req.params.id);
    if (fileIndex === -1) {
      return res.status(404).json({ error: '文件不存在' });
    }
    
    // 获取文件路径并删除
    const file = db.files[fileIndex];
    const filePath = path.join(__dirname, 'public', file.path);
    
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath);
    }
    
    // 从数据库中删除记录
    db.files.splice(fileIndex, 1);
    fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
    
    res.json({ message: '文件删除成功' });
  } catch (error) {
    res.status(500).json({
      error: '删除文件失败',
      details: error.message
    });
  }
});

// 使用json-server中间件和路由
app.use(middlewares);
app.use(router);

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`JSON Server with file upload running on http://localhost:${PORT}`);
});

初始化db.json文件:

{
  "posts": [],
  "comments": [],
  "profile": {},
  "files": []
}

步骤4:创建前端上传演示页面

创建public/index.html文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>json-server 文件上传演示</title>
  <style>
    * {
      box-sizing: border-box;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }
    
    body {
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    
    .upload-container {
      border: 2px dashed #ccc;
      border-radius: 8px;
      padding: 30px;
      text-align: center;
      margin-bottom: 30px;
      transition: all 0.3s;
    }
    
    .upload-container.dragover {
      border-color: #3498db;
      background-color: #f0f8ff;
    }
    
    #file-input {
      display: none;
    }
    
    .upload-btn {
      background-color: #3498db;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 16px;
      margin-top: 10px;
    }
    
    .upload-btn:hover {
      background-color: #2980b9;
    }
    
    .file-list {
      margin-top: 20px;
    }
    
    .file-item {
      display: flex;
      align-items: center;
      padding: 10px;
      border-bottom: 1px solid #eee;
    }
    
    .file-icon {
      margin-right: 10px;
      font-size: 24px;
    }
    
    .file-info {
      flex-grow: 1;
    }
    
    .file-name {
      font-weight: bold;
    }
    
    .file-size {
      color: #666;
      font-size: 0.9em;
    }
    
    .progress-container {
      width: 100%;
      height: 8px;
      background-color: #eee;
      border-radius: 4px;
      margin-top: 5px;
      overflow: hidden;
    }
    
    .progress-bar {
      height: 100%;
      background-color: #3498db;
      width: 0%;
      transition: width 0.3s;
    }
    
    .error-message {
      color: #e74c3c;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>json-server 文件上传演示</h1>
  
  <div class="upload-container" id="drop-area">
    <p>拖放文件到此处,或</p>
    <button class="upload-btn" onclick="document.getElementById('file-input').click()">
      选择文件
    </button>
    <input type="file" id="file-input" multiple accept="*" />
    <div class="error-message" id="error-message"></div>
    
    <div class="file-list" id="file-list"></div>
  </div>

  <script>
    const dropArea = document.getElementById('drop-area');
    const fileInput = document.getElementById('file-input');
    const fileList = document.getElementById('file-list');
    const errorMessage = document.getElementById('error-message');
    
    // 拖放事件处理
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
      dropArea.addEventListener(eventName, preventDefaults, false);
    });
    
    function preventDefaults(e) {
      e.preventDefault();
      e.stopPropagation();
    }
    
    ['dragenter', 'dragover'].forEach(eventName => {
      dropArea.addEventListener(eventName, highlight, false);
    });
    
    ['dragleave', 'drop'].forEach(eventName => {
      dropArea.addEventListener(eventName, unhighlight, false);
    });
    
    function highlight() {
      dropArea.classList.add('dragover');
    }
    
    function unhighlight() {
      dropArea.classList.remove('dragover');
    }
    
    // 处理放置的文件
    dropArea.addEventListener('drop', handleDrop, false);
    
    function handleDrop(e) {
      const dt = e.dataTransfer;
      const files = dt.files;
      handleFiles(files);
    }
    
    // 处理选择的文件
    fileInput.addEventListener('change', function() {
      handleFiles(this.files);
    });
    
    // 处理文件上传
    function handleFiles(files) {
      if (files.length === 0) return;
      
      errorMessage.textContent = '';
      fileList.innerHTML = '';
      
      const formData = new FormData();
      Array.from(files).forEach(file => {
        formData.append('files', file);
        addFileToDOM(file);
      });
      
      // 添加额外表单数据
      formData.append('uploader', 'demo-user');
      
      uploadFiles(formData);
    }
    
    // 添加文件到DOM显示
    function addFileToDOM(file) {
      const fileItem = document.createElement('div');
      fileItem.className = 'file-item';
      fileItem.dataset.filename = file.name;
      
      const fileIcon = document.createElement('div');
      fileIcon.className = 'file-icon';
      fileIcon.textContent = getFileIcon(file.type);
      
      const fileInfo = document.createElement('div');
      fileInfo.className = 'file-info';
      
      const fileName = document.createElement('div');
      fileName.className = 'file-name';
      fileName.textContent = file.name;
      
      const fileSize = document.createElement('div');
      fileSize.className = 'file-size';
      fileSize.textContent = formatFileSize(file.size);
      
      const progressContainer = document.createElement('div');
      progressContainer.className = 'progress-container';
      
      const progressBar = document.createElement('div');
      progressBar.className = 'progress-bar';
      progressBar.style.width = '0%';
      
      progressContainer.appendChild(progressBar);
      fileInfo.appendChild(fileName);
      fileInfo.appendChild(fileSize);
      fileInfo.appendChild(progressContainer);
      
      fileItem.appendChild(fileIcon);
      fileItem.appendChild(fileInfo);
      
      fileList.appendChild(fileItem);
    }
    
    // 获取文件图标
    function getFileIcon(mimeType) {
      if (mimeType.startsWith('image/')) return '🖼️';
      if (mimeType.startsWith('video/')) return '🎬';
      if (mimeType.startsWith('audio/')) return '🎵';
      if (mimeType === 'application/pdf') return '📄';
      return '📎';
    }
    
    // 格式化文件大小
    function formatFileSize(bytes) {
      if (bytes < 1024) return bytes + ' B';
      else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
      else return (bytes / 1048576).toFixed(1) + ' MB';
    }
    
    // 上传文件
    function uploadFiles(formData) {
      const xhr = new XMLHttpRequest();
      xhr.open('POST', '/upload', true);
      
      xhr.upload.addEventListener('progress', function(e) {
        if (e.lengthComputable) {
          const percent = (e.loaded / e.total) * 100;
          updateProgress(percent);
        }
      });
      
      xhr.addEventListener('load', function() {
        if (xhr.status === 201) {
          const response = JSON.parse(xhr.responseText);
          console.log('上传成功:', response);
          // 可以在这里更新UI显示文件URL等信息
        } else {
          const error = JSON.parse(xhr.responseText) || { error: '上传失败' };
          errorMessage.textContent = error.error;
        }
      });
      
      xhr.addEventListener('error', function() {
        errorMessage.textContent = '网络错误,上传失败';
      });
      
      xhr.send(formData);
    }
    
    // 更新上传进度
    function updateProgress(percent) {
      const progressBars = document.querySelectorAll('.progress-bar');
      progressBars.forEach(bar => {
        bar.style.width = percent + '%';
      });
    }
  </script>
</body>
</html>

步骤5:配置与启动服务

修改package.json添加启动脚本:

{
  "name": "json-server-file-upload",
  "version": "1.0.0",
  "description": "json-server with file upload capability",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "json-server": "^0.17.4",
    "multer": "^1.4.5-lts.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

启动服务:

npm run dev

访问http://localhost:3000即可看到文件上传界面。上传文件后,可以通过以下API访问文件信息:

  • GET /files - 获取所有文件记录
  • GET /files/:id - 获取单个文件详情
  • DELETE /files/:id - 删除文件

高级功能扩展

添加文件类型与大小验证

修改src/middlewares/upload.js增强验证功能:

// 在fileFilter函数中添加更严格的验证
const allowedTypes = {
  'image/jpeg': { maxSize: 5 * 1024 * 1024 }, // 5MB
  'image/png': { maxSize: 5 * 1024 * 1024 },
  'image/gif': { maxSize: 10 * 1024 * 1024 },
  'application/pdf': { maxSize: 20 * 1024 * 1024 },
  'video/mp4': { maxSize: 100 * 1024 * 1024 },
  'audio/mpeg': { maxSize: 30 * 1024 * 1024 }
};

const fileFilter = (req, file, cb) => {
  if (!allowedTypes[file.mimetype]) {
    return cb(new Error(`不支持的文件类型: ${file.mimetype}`), false);
  }
  
  const typeConfig = allowedTypes[file.mimetype];
  if (file.size > typeConfig.maxSize) {
    return cb(new Error(`文件超过大小限制: ${formatFileSize(typeConfig.maxSize)}`), false);
  }
  
  cb(null, true);
};

// 添加辅助函数格式化文件大小
function formatFileSize(bytes) {
  if (bytes < 1024) return bytes + ' B';
  else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
  else return (bytes / 1048576).toFixed(1) + ' MB';
}

实现文件访问权限控制

添加简单的API密钥验证中间件src/middlewares/auth.js

const apiKeys = {
  'demo-key': 'read-only',
  'admin-key': 'full-access'
};

function authenticate(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey || !apiKeys[apiKey]) {
    return res.status(401).json({ error: '未授权访问' });
  }
  
  // 将权限添加到请求对象
  req.userRole = apiKeys[apiKey];
  next();
}

function authorize(requiredRole) {
  return (req, res, next) => {
    if (!req.userRole) {
      return res.status(401).json({ error: '未授权访问' });
    }
    
    if (requiredRole === 'full-access' && req.userRole !== 'full-access') {
      return res.status(403).json({ error: '权限不足' });
    }
    
    next();
  };
}

module.exports = { authenticate, authorize };

server.js中应用权限控制:

const { authenticate, authorize } = require('./src/middlewares/auth');

// 保护文件删除接口
app.delete('/files/:id', authenticate, authorize('full-access'), (req, res) => {
  // 原有的删除逻辑...
});

// 保护上传接口
app.post('/upload', authenticate, authorize('full-access'), upload.array('files', 5), (req, res) => {
  // 原有的上传逻辑...
});

部署与性能优化

生产环境配置

创建生产环境启动脚本ecosystem.config.js(使用PM2):

module.exports = {
  apps: [{
    name: 'json-server-upload',
    script: 'server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    max_memory_restart: '1G',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
};

性能优化策略

  1. 文件存储优化

    • 对于大型部署,考虑使用AWS S3或阿里云OSS替代本地存储
    • 实现文件分片上传处理大文件
  2. 缓存策略

    // 添加缓存控制头
    app.use('/uploads', (req, res, next) => {
      res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存1天
      next();
    });
    
  3. 数据库性能

    • 对于大量文件记录,考虑使用lowdb的JSON5格式支持注释
    • 实现数据分页加载:GET /files?_page=1&_limit=20

总结与后续扩展

通过本文介绍的方法,我们成功为json-server添加了完整的文件上传功能,包括:

  • 多文件上传处理
  • 文件类型和大小验证
  • 文件元数据管理
  • RESTful API接口
  • 权限控制与安全验证

后续可以考虑的扩展方向:

  • 实现文件预览功能
  • 添加图片缩略图生成
  • 集成文件压缩与格式转换
  • 实现断点续传功能
  • 添加用户认证系统

希望这个方案能够帮助你解决前端开发中的文件上传痛点,让json-server成为更强大的全栈开发工具。如果觉得本文有用,请点赞收藏,并关注获取更多开发技巧!

下期预告:《使用json-server模拟GraphQL API》

【免费下载链接】json-server Get a full fake REST API with zero coding in less than 30 seconds (seriously) 【免费下载链接】json-server 项目地址: https://gitcode.com/GitHub_Trending/js/json-server

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值