NodeJS开发服务端实现文件上传下载和数据增删改查

本文主要讲解已NodeJS作为服务器完成文件的上传下载和数据增删改查,前端框架为Vue3,UI框架为element-plus,Node版本为V16.14.2. 

项目场景模拟是开发一个项目管理的系统,支持任务和项目的增删改查、以及上传、下载项目附件包。

其他依赖如下

"dependencies": {
    "body-parser": "^1.20.2",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "fs": "^0.0.1-security",
    "http": "^0.0.1-security",
    "moment": "^2.29.4",
    "multiparty": "^4.2.3",
    "mysql": "^2.18.1"
  }
Node依赖包
"dependencies": {
    "axios": "^1.3.4",
    "core-js": "^3.8.3",
    "dayjs": "^1.11.7",
    "element-plus": "^2.2.32",
    "js2uml": "^0.2.4",
    "mysql": "^2.18.1",
    "vue": "^3.2.13",
    "vue-router": "^4.1.6"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "less": "^4.1.3",
    "less-loader": "^11.1.0"
  }
Web依赖包

一、配置Node服务

首先npm i express 安装express组件作为Web服务,创建一个基础文件index.js代码如下:

const express = require('express');
const router = express();

router.listen(3000);
console.log('服务器已启动,请访问 localhost:3000');

然后cmd到当前目录,输入node index.js即可启动服务

二、前端搭建

1. 打开 cmd 窗口输入命令 npm i -g @vue/cli 安装vue脚手架

2. 输入 vue create 项目名称初始化项目

3. 输入 npm i axios 安装axios组件用于请求交互

4. 输入 npm i element-plus 安装Ele用户快速搭建UI

5. 输入 npm run serve 启动Web服务,打开浏览器访问localhost:端口号 即可验证是否搭建成功

三、Node服务搭建
1. 首先安装mysql数据库,mysql数据库为轻型数据库,所以在自己本地安装也不会太影响性能

 访问 https://dev.mysql.com/downloads/mysql/ 下载数据库,按照步骤安装完成后,在配置完环境变量后即可(注:初始化时会生成一个默认的初始密码,需要记录下,否则后面登陆需要重置,比较耗时)

2. 安装navicat 客户端,按照提示步骤安装即可

3. 开发Node服务代码

  ①. 在index.js 中增加如下代码,为了方便大家阅读,所以一次性罗列,可以增删不同的项来学习他们的用处

  

  ②. 新建一个db.js文件,主要创建数据库连接池和提供操作数据库接口

const mysql = require('mysql');

const pool = mysql.createPool({
  connectionlimit: 50, // 最大连接数
  host: 'localhost', 
  user: 'root',   // 数据库用户
  password: '123456', // 数据库密码
  database: 'task'   // 新建的数据库名
})

/**
 * 数据库查询
 * @param {*} sql 查询语句
 * @param {*} p 参数
 * @param {*} c 回调函数
 */
function query(sql, params = [], callback) {
  pool.getConnection((err, connection) => {
    connection.query(sql, params, (queryErr, res) => {
      connection.release();
      callback.apply(null, [queryErr ? false : true, queryErr ? queryErr : res]);
    })
  })
}

module.exports = { query }
View Code

 

  ③. 开始开发业务相关代码, 新建border.js文件,主要处理跟项目和任务相关的逻辑

const express = require('express');
// 处理日期格式
const moment = require('moment');
// 文件上传的流解析
const multiparty = require('multiparty');
const fs = require('fs');
const db = require('./db');

function guid() {
  function S4() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
  }
  return S4();
}

function creatRes(flag, data) {
  let res = {};
  if (flag) {
    res = {
      code: 1001,
      data: data
    }
  } else {
    res = {
      code: 4001,
      errMsg: data
    }
  }
  return res;
}

const router = express.Router();

/**
 * 获取文件列表
 */
router.get('/getTaskList', (req, res) => {
  const sql = 'select t.*, p.name as productName from task t join product p where t.productId = p.productId'
  db.query(sql, null, (flag, data) => {
    res.send(creatRes(flag, data));
  })
})

/**
 * 添加任务
 */
router.post('/addTask', (req, res) => {
  const param = req.body;
  const id = guid();
  const values = [id, param.name, param.startTime, param.progress, param.productId, param.desc, param.endTime, moment().format('YYYY-MM-DD HH:mm:ss') ];
  const sql = 'INSERT INTO task VALUES(?, ?, ?, ?, ?, ?, ?, ?)';
  db.query(sql, values, (flag, data) => {
    res.send(creatRes(flag, data))
  })
})

/**
 * 删除任务
 */
router.post('/deleteTask', (req, res) => {
  const param = req.body;
  const sql = `DELETE FROM task where id='${param.id}'`;
  db.query(sql, null, (flag, data) => {
    res.send(creatRes(flag, data));
  })
})

/**
 * 获取项目列表
 */
router.get('/getProductList', (req, res) => {
  const sql = 'select * from product';
  db.query(sql,null, (flag, data) => {
    res.send(creatRes(flag, data));
  })
})

/**
 * 添加项目
 */
router.post('/addProduct', (req, res) => {
  const param = req.body;
  const id = guid();
  const sql = 'INSERT INTO product VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
  const sqlParam = [id, param.name, param.productId, param.startTime, param.endTime, param.progress, param.desc, moment().format('YYYY-MM-DD HH:mm:ss'), '', ''];
  db.query(sql, sqlParam, (flag, data) => {
    res.send(creatRes(flag, data));
  })
})

/**
 * 删除项目
 */
router.post('/deleteProduct', (req, res) => {
  const param = req.body;
  const querySql = `select packageId from product where id ='${param.id}'`;
  db.query(querySql, null, (flag, data) => {
    if (flag) {
      fs.unlink(__dirname + '/demo/' + data[0].packageId, () => {
        const sql = `DELETE t, p from product p left join task t on p.productId = t.productId where p.id= '${param.id}'`;
        db.query(sql, null, (flag, data) => {
          res.send(creatRes(flag, data));
        })
      })
    }
  })
})

/**
 * 给项目上传附件包并将包保存到本地
 */
router.post('/upload', (req, res) => {
  let form = new multiparty.Form();
  // 将上传的文件流解析并保存到本地,此时文件名是随机产生的
  form.uploadDir = __dirname + '/demo/';
  form.parse(req, (err, fields, files) => {
    const packageName = files.file[0].originalFilename;
    const randomName = Math.ceil(Math.random() * 10000)+ '_' + packageName;
    // 将随机产生的文件名修改为上传时的文件名
    fs.rename(files.file[0].path, __dirname + '\\demo\\' + randomName, () => {
      console.log('修改成功-----');
    });
    const sql = `update product set packageName="${packageName}",packageId="${randomName}" where id = "${fields.id[0]}"`;
    db.query(sql, null, (flag, data)=> {
      res.send(creatRes(flag, data));
    })
  })
})

/**
 * 将存储的文件下载到本地
 */
router.get('/download', (req, res) => {
  const sql = 'select packageId,packageName from product where id = "' + req.query.id + '"';
  db.query(sql, null, (flag, result) => {
    const data = result[0];
    const path = __dirname + '\\demo\\' + data.packageId;
    const f = fs.createReadStream(path);
    const userAgent = (req.headers['user-agent']||'').toLowerCase();
    const filename = data.packageName;
    
    // 这句话不能少,不然Web端获取不到响应头参数
    res.setHeader('Access-Control-Expose-Headers', 'Content-disposition');
    
    // 不同的浏览器上传文件流时将参数写入到响应头的方式
    if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
      res.writeHead(200, {
        'Content-type': 'application/octet-stream',
        'Content-disposition': 'attachment; filename=' + encodeURIComponent(filename),
      });
    } else if(userAgent.indexOf('firefox') >= 0) {
      res.writeHead(200, {
        'Content-type': 'application/octet-stream',
        'Content-disposition': 'attachment; filename*="utf8\'\'' + encodeURIComponent(filename)+'"',
      });
    } else {
      /* safari等其他非主流浏览器只能自求多福了 */
      res.writeHead(200, {
        'Content-type': 'application/octet-stream',
        'Content-disposition': 'attachment; filename=' + new Buffer(filename).toString('binary'),
      });
    }
    f.pipe(res);
  })
  
})

module.exports = router;
View Code

 

  ④. 新建package.js文件,无实质内容,主要为了展示分模块路由的使用方法

const express = require('express');
const db = require('./db');

const router = express.Router();

function creatRes(flag, data) {
  let res = {};
  if (flag) {
    res = {
      code: 1001,
      data: data
    }
  } else {
    res = {
      code: 4001,
      errMsg: data
    }
  }
  return res;
}

router.get('/getList', (req, res) => {
  db.query('select * from package', null, (flag, data) => {
    res.send(creatRes(flag, data));
  })
})

module.exports = router;
View Code

 四、搭建Web端

  到第三步,Node服务已经开发完毕,Web端搭建默认各位老总对Web端很熟,所以大部分东西就直接上代码了

  ①. 首先新建axios.js文件,提供get、post、download、upload接口

import axios from "axios";

const instance = axios.create({
  // Node服务地址和端口
  baseURL: 'http://localhost:3000/',
  timeout: 30000
})

instance.interceptors.response.use(function (response) {
  return response;
}, function (error) {
  return Promise.reject(error);
});

function get(url, param) {
  return new Promise((reslove, reject) => {
    instance.get(url, {
      params: param
    }).then(res => {
      if (res.data.code === 1001) {
        reslove(res.data.data);
      } else {
        reject(res.data.errMsg);
      }
    });
  }) 
}

function post(url, param) {
  return new Promise((reslove, reject) => {
    instance.post(url, param).then(res => {
      if (res.data.code === 1001) {
        reslove(res.data.data);
      } else {
        reject(res.data.errMsg);
      }
    })
  })
}

function upload(url, param) {
  // 请求头配置必不可少
  const config = {
    headers: {
      'Content-type': 'multipart/form-data'
    }
  };
  return new Promise((reslove, reject) => {
    instance.post(url, param, config).then(res => {
      if (res.data.code === 1001) {
        reslove(res.data.data);
      } else {
        reject(res.data.errMsg);
      }
    });
  })
}

function download(url, param) {
  return new Promise((reslove, reject) => {
    instance.get(url, {
      params: param,
      responseType: 'blob'
    }).then(res => {
      if (res.data instanceof Blob) {
        // 获取文件名,重新命名,否则下载后文件名会变
        const fileName = decodeURIComponent(res.headers['content-disposition'].split(';')[1].split('filename=')[1]);
        const blob = new Blob([res.data], { type: 'application/x-zip-compressed' });
        const a = document.createElement('a');
        const href = window.URL.createObjectURL(blob);
        a.href = href;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(href);
        document.body.removeChild(a);
        reslove(fileName);
      } else {
        reject('文件下载失败');
      }
    });
  })
}

export default { get, post, download, upload };
View Code

  ②. 新建Product.vue文件,主要包含增、删、查和上传下载文件功能

<template>
  <div class="package">
    <el-table :data="list" stripe style="width: 100%">
      <el-table-column prop="productName" label="产品名称" />
      <el-table-column prop="productId" label="产品ID" />
      <el-table-column prop="packageName" label="包名" />
      <el-table-column prop="createTime" label="创建时间" />
      <el-table-column prop="updateTime" label="更新时间时间" />
      <el-table-column fixed="right" label="操作" >
        <template #default="scope">
          <div class="upload-button">
            <input type="file" @change="upload(scope.row, $event)"/>
            <el-button link type="primary" size="small" >上传</el-button>
          </div>
          
          <el-button link type="primary" size="small" @click="download(scope.row)">下载</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import { onMounted, ref } from 'vue'
import axios from '../utils/axios.js';
import { ElMessage } from 'element-plus'

export default {
  name: 'PackageView',
  setup() {
      const list = ref([]);

      function getList() {
        axios.get('/package/getList').then(res => {
          list.value = res;
        })
      }
      

      function upload(row, e) {
        const file = e.target.files[0];
        if (file) {
          const form = new FormData();
          form.append('file', file);
          form.append('id', row.id);
          axios.upload('/package/upload', form).then(() => {
            ElMessage({ showClose: false, message: '上传成功.', type: 'success' });
            getList();
          });
        }
      }

      function download(row) {
        const param = {
          id: row.id
        };
        axios.download('/package/download', param).then(() => {
          ElMessage({ showClose: false, message: '下载成功.', type: 'success' })
        });
      }

      onMounted(() => {
        getList();
      }) 

      return {
        list,
        upload,
        download
      }
  }
}
</script>

<style lang="less" scoped>
.package {
  .upload-button {
    width: 30px;
    display: inline;
    position: relative;
    input {
      width: 30px;
      opacity: 0;
      z-index: 1;
      position: absolute;
      cursor: pointer;
    }
  }
}
</style>
View Code

 五、效果图展示

 

posted @ 2023-03-13 20:04  火星写程序  阅读(710)  评论(0编辑  收藏  举报