Vue3 + Nodejs 实战 ,文件上传项目--实现图片上传

news2025/6/26 11:39:33

目录

技术栈

1. 项目搭建前期工作(不算太详细)

前端

 后端

2.配置基本的路由和静态页面

 3.完成图片上传的页面(imageUp)

静态页面搭建

 上传图片的接口

 js逻辑

4.编写上传图片的接口

5.测试效果

 结语


博客主页:専心_前端,javascript,mysql-CSDN博客

 系列专栏:vue3+nodejs 实战--文件上传

 前端代码仓库:jiangjunjie666/my-upload: vue3+nodejs 上传文件的项目,用于学习 (github.com)

 后端代码仓库:jiangjunjie666/my-upload-server: nodejs上传文件的后端 (github.com)

 欢迎关注

本系列记录vue3(前端)+nodejs(后端) 实现一个文件上传项目,目前只完成了图片的上传,后续会陆续完成:单文件上传,多文件上传,大文件分片上传,拖拽上传等功能,欢迎关注。

技术栈

前端:Vue3 Vue-router axios element-plus...

后端:nodejs express...

1. 项目搭建前期工作(不算太详细)

前端

我使用的是vite创建的vue项目,包管理器工具为:pnpm

pnpm create vite

创建好项目后安装依赖就可启动项目了

配置+安装需要用到的库

配置文件路径别名(在vite.config.js文件中)

安装需要用到的库

pnpm i vue-router
pnpm i element-plus
pnpm install @element-plus/icons-vue
pnpm i axios

进行基础配置(建好对应的文件夹)

导入Element-Plus (在main.js文件中)

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
//引入样式
import 'element-plus/dist/index.css'
import router from '@/router/index.js'

const app = createApp(App)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.use(ElementPlus, {
  locale: zhCn
})
app.use(router)
app.mount('#app')

 后端

使用express框架快速搭建出node项目

npx express-generator

需要用到的依赖

npm i cors
npm i formidanle@2.1.2

在app.js文件中配置跨域

//配置跨域
var cors = require('cors')

app.use(cors())

启动项目

npm start

2.配置基本的路由和静态页面

目前路由文件是这样的

//vue-router
// import Vue from 'vue'
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      //重定向至主页
      redirect: '/home'
    },
    {
      path: '/home',
      component: () => import('../views/home/index.vue'),
      name: 'home',
      redirect: '/home/imageUp',
      meta: {
        title: '首页'
      },
      children: [
        {
          path: '/home/imageUp',
          component: () => import('../views//home/imageUp/index.vue'),
          name: 'imageUp',
          meta: {
            title: '图片上传'
          }
        },
        {
          path: '/home/videoUp',
          component: () => import('../views/home/videoUp/index.vue'),
          name: 'videoUp',
          meta: {
            title: '视频上传'
          }
        },
        {
          path: '/home/fileUp',
          component: () => import('@/views/home/fileUp/index.vue'),
          name: 'fileUp',
          meta: {
            title: '文件上传'
          }
        }
      ]
    }
  ]
})

export default router

目前就这几个页面

在App.vue中使用路由占位

home主页中的index.vue文件

这其中除了静态页面的搭建外,我使用了编程式路由跳转方式实现路由跳转,后续可能会添加更多功能(也可以使用其他的方式实现跳转,不唯一)。

<template>
  <div class="container">
    <div class="top">My upload</div>
    <div class="heart">
      <div class="left">
        <ul>
          <li :class="{ active: activeIndex == 1 }" @click="changeUp('/home/imageUp', 1)">
            <el-icon size="20"><PictureFilled /></el-icon>
            <p>图片上传</p>
          </li>
          <li :class="{ active: activeIndex == 2 }" @click="changeUp('/home/fileUp', 2)">
            <el-icon size="20"><FolderAdd /></el-icon>
            <p>文件上传</p>
          </li>
          <li :class="{ active: activeIndex == 3 }" @click="changeUp('/home/videoUp', 3)">
            <el-icon size="20"><VideoCamera /></el-icon>
            <p>视频上传</p>
          </li>
        </ul>
      </div>
      <div class="layout">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
//引入路由
import { useRouter } from 'vue-router'

const $router = useRouter()
let activeIndex = ref(1)

//二级路由跳转函数
const changeUp = (path, index) => {
  //路由跳转
  $router.push(path)
  activeIndex.value = index
}
</script>

<style lang="scss" scoped>
.container {
  .top {
    width: 100vw;
    height: 100px;
    background-color: rgb(61, 221, 154);
    text-align: center;
    font-size: 30px;
    color: #fff;
    line-height: 100px;
  }
  .heart {
    width: 100vw;
    //高度减去100px
    height: calc(100vh - 100px);
    display: flex;
    .left {
      width: 350px;
      height: 100%;
      background-color: #fffcfc;
      border-right: 1px solid #ccc;
      // padding-left: 20px;
      ul {
        li {
          width: 100%;
          height: 40px;
          display: flex;
          align-items: center;
          padding-left: 20px;
          p {
            margin-left: 10px;
            font-size: 16px;
            line-height: 40px;
          }
        }
        //给li加个active
        .active {
          color: rgb(61, 221, 154);
        }
        //加hover
        li:hover {
          cursor: pointer;
          background-color: rgb(146, 236, 199);
          opacity: 0.8;
          color: black;
        }
      }
    }
    .layout {
      width: calc(100% - 350px);
      height: 100%;
      padding: 20px;
    }
  }
}
</style>

目前项目长这样:

 3.完成图片上传的页面(imageUp)

静态页面搭建

act用于控制上传图片时的不同状态:选择图片->上传中->上传成功

上传成功的图片会展示在下方的照片墙中

<template>
  <div class="box">
    <div class="add">
      <input type="file" ref="fileInputRef" style="display: none" @change="handleFileChange" />
      <el-icon size="100" color="#ccc" v-if="act == 1" @click="openFileInput"><Plus /></el-icon>
      <!-- loading效果 -->
      <div class="loading" v-if="act == 2"></div>
      <img :src="base64Img" alt="" v-if="act == 2" />
    </div>
  </div>
  <div class="imgList">
    <ul>
      <li v-for="item in imgList"><img :src="item" alt="" /></li>
    </ul>
  </div>
</template>

<style lang="scss" scoped>
.box {
  width: 350px;
  height: 350px;
  border: 2px dashed rgb(175, 171, 171);
  border-radius: 2em;
  display: flex;
  justify-content: center;
  align-items: center;
  .add {
    position: relative;
    .loading {
      width: 100px;
      height: 100px;
      position: absolute;
      top: 35%;
      left: 35%;
      border: 3px solid #302b2b;
      border-top-color: transparent;
      border-radius: 50%;
      animation: circle infinite 0.75s linear;
    }
    // 转转转动画
    @keyframes circle {
      0% {
        transform: rotate(0);
      }
      100% {
        transform: rotate(360deg);
      }
    }
    img {
      width: 350px;
      height: 350px;
      z-index: -1;
      // 增加点模糊透明度
      opacity: 0.5;
    }
  }
  .add:hover {
    cursor: pointer;
  }
}
.imgList {
  width: 60%;
  // background-color: pink;
  margin-top: 30px;
  ul {
    border: 1px solid #ccc;
    border-radius: 20px;
    display: flex;
    flex-wrap: wrap;
    padding-left: 20px;
    li {
      width: 200px;
      height: 200px;
      margin: 5px 6px;
      // border: 1px solid pink;
      img {
        width: 100%;
        border-radius: 20px;
        height: 100%;
      }
    }
  }
}
</style>

 上传图片的接口

 js逻辑

我想实现的是有加载中的一个效果,但是单图片上传的速度,所以我使用了定时器来看到这个上传的效果,其中还没完成上传的图片,能显示加载出来主要是将上传的图片转为了base64格式,临时显示在页面加载上(因为base64加载要比服务器上传快),不懂图片转base64的可以看这个:前端图片转base64,并使用canvas对图片进行压缩_图片base64压缩-CSDN博客,其他的就很简单了,代码基本能看懂。

 

<script setup>
import { ref } from 'vue'
import { reqUploadImg } from '@/api/data.js'
import { ElMessage } from 'element-plus'
let act = ref(1)
const fileInputRef = ref(null)
let selectedFile = ref(null)
let base64Img = ref('')
let imgList = ref([])
//上传函数
const openFileInput = () => {
  // 点击图标时触发文件选择框
  fileInputRef.value.click()
}

const handleFileChange = async (event) => {
  // 处理文件选择事件
  act.value = 2
  selectedFile.value = event.target.files[0]
  if (!selectedFile.value) {
    return
  }

  // 将图片转为base64显示loading状态
  const reader = new FileReader()
  reader.onload = (e) => {
    const img = new Image()
    img.src = e.target.result
    img.onload = () => {
      // 图片加载完成
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      const maxWidth = 300 // 设置最大宽度
      const maxHeight = 300 // 设置最大高度
      let width = img.width
      let height = img.height

      // 如果图片尺寸大于最大宽度或最大高度,则按比例缩放图片
      if (width > maxWidth || height > maxHeight) {
        const ratio = Math.min(maxWidth / width, maxHeight / height)
        width *= ratio
        height *= ratio
      }

      canvas.width = width
      canvas.height = height

      ctx.drawImage(img, 0, 0, width, height)

      const compressedDataUrl = canvas.toDataURL('image/jpeg') // 压缩图片质量为0.8
      console.log(compressedDataUrl)
      base64Img.value = compressedDataUrl
    }
  }
  reader.readAsDataURL(selectedFile.value) //调用生成base64

  // 创建一个FormData对象来包装文件
  const formData = new FormData()
  formData.append('file', selectedFile.value)

  // 使用上传图片的接口函数发送请求
  let res = await reqUploadImg(formData)
  if (res.code !== 200) {
    ElMessage({
      type: 'error',
      message: '上传失败'
    })
    act.value = 1
    return
  }
  //成功
  //开个定时器(为了看到上传的加载效果)
  else {
    let timer = setInterval(() => {
      act.value = 1
      clearInterval(timer)
      imgList.value.push(res.imgUrl)
      ElMessage({
        type: 'success',
        message: '上传成功'
      })
    }, 1000)
  }
}
</script>

到这里前端的功能基本完成了

4.编写上传图片的接口

先建好文件夹

路由images.js

var express = require('express')
var router = express.Router()

const handler = require('./image_handler')
//挂载路由
router.post('/imageUpload', handler.imageUp)

module.exports = router

接口函数

这里使用的包是formidable,下载的版本是2.1.2 ,如果版本不同可能代码会有所差异

这里限制了上传的图片类型和图片大小,并且将图片上传至了public/images文件夹中。

//放置上传图片的处理函数
//导入处理文件上传的包
const formidable = require('formidable')
const path = require('path')
exports.imageUp = (req, res, next) => {
  const form = formidable({
    multiples: true,
    uploadDir: path.join(__dirname, '../../public/images'),
    keepExtensions: true
  })
  form.parse(req, (err, fields, files) => {
    if (err) {
      next(err)
      return
    }
    console.log(files)
    //切割出上传的文件的后缀名
    let ext = files.file.mimetype.split('/')[1]
    //计算出图片文件大小
    let size = (files.file.size / 1024 / 1024).toFixed(2)
    if ((ext == 'png' || ext == 'jpg' || ext == 'jpeg') && size < 2) {
      let url = 'http://127.0.0.1:3000/images/' + files.file.newFilename
      res.send({
        code: 200,
        msg: '上传成功',
        imgUrl: url
      })
    } else {
      res.send({
        code: 400,
        msg: '只能上传png、jpg、jpeg格式的图片或图片过大'
      })
      return
    }
  })
}

5.测试效果

上传中

上传成功

 

 后端文件夹中

 结语

如果没有接触过文件上传或者想尝试开发一下文件上传项目的可以交流学习一下,后续会陆续更新更多的功能,如有想法可在评论区交流或私信,感谢关注!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1085816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记一次惊险的CDH6.3.2集群断电后重启的过程

重启服务 systemctl restart cloudera-scm-server.service systemctl restart cloudera-scm-agent.service查看服务是否启动&#xff0c;显然结果是failed systemctl status cloudera-scm-server.service查看异常 journalctl -xe去看服务日志 发现是这个位置错误 SqlExcep…

018-第三代软件开发-整体介绍

第三代软件开发-整体介绍 文章目录 第三代软件开发-整体介绍项目介绍整体介绍Qt 属性系统QML 最新软件技术框架 关键字&#xff1a; Qt、 Qml、 属性、 Qml 软件架构 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QML&#xff08;Qt Meta-Object …

Java Day1

day01 一、Markdown 基础语法1.标题2. 字体3. 引用 >4. 分隔线 --- ***5. 图片 ![]()6.超链接7.列表8.表格9.代码 代码名称 二、计算机三、常用快捷键1. Win 系列2. Ctrl 系列3. ALt 系列 四、 基本的DOS命令1. 打开方式&#xff1a;2. 常用DOS命令 五、计算机语言发展史第一…

黑马JVM总结(三十二)

&#xff08;1&#xff09;类加载器-线程上下文1 使用的应用程序类加载器来完成类的加载&#xff0c;不是用的启动类加载器&#xff0c;jdk在某些情况下要打破&#xff0c;双亲委派的模式&#xff0c;有时候需要调用应用程序类加载器来完成类的加载&#xff0c;否则有些类它是找…

Linux C select 的学习

一. select 系统调用 1. 函数说明 #include <sys/select.h> #include <sys/time.h>int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);nfds: 是一个整数值&#xff0c;是指集合中所有文件描述符的范围&#…

JavaScript使用类-模态窗口

**上节课我们为这个项目获取了一些DOM元素&#xff0c;现在我们可以继续&#xff1b;**这个模态窗口有一个hidden类&#xff0c;这个类上文我们讲了&#xff0c;他的display为none&#xff1b;如果我们去除这个hidden的话&#xff0c;就可以让这个模态窗口展现出来。如下 cons…

【机器学习】sklearn对数据预处理

文章目录 数据处理步骤观察数据数据无量纲化缺失值处理处理分类型特征处理连续型特征 数据处理步骤 数据无量纲化缺失值处理处理分类型特征&#xff1a;编码与哑变量处理连续型特征&#xff1a;二值化与分段 观察数据 通过pandas读取数据&#xff0c;通过head和info方法大致查…

TCP/IP(十五)拥塞控制

一 拥塞控制 ① 拥塞控制必要性 思考&#xff1a; 为什么要有拥塞控制呀,不是有流量控制了吗&#xff1f; ② 拥赛窗口 cwnd 什么是拥塞窗口? 和发送窗口有什么关系呢?明白&#xff1a; cwnd、swnd、rwnd 缩写 含义 ③ 如何知道当前网络是否出现了拥塞呢&#xff1f;…

抖音小程序没人做了吗?

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 咱说的严谨点&#xff0c;不是没人做了&#xff0c;而是做的人少了。利益驱使&#xff0c;越来越多的人开始思考新方向了&#xff0c;开发小程序的人少了&#xff0c;排名也没多少人做了&#xff…

RustDay03——记录刷完Rust100题

刷了两三天Rust&#xff0c;终于把Rust100题刷完了&#xff0c;小小记录一下 明天白天的时候重开账户开题写答案

不定积分(原函数)存在性定理、定积分存在性定理、变限积分存在性定理

1.不定积分(原函数)存在性定理、定积分存在性定理、变限积分存在性定理 笔记来源&#xff1a; 1.10个命题搞懂可积和原函数存在 2.考研变限积分概念超详细&#xff0c;超通俗讲解&#xff08;变限积分和原函数关系&#xff09; 声明&#xff1a;本文截图主要来自bili心一学长、…

大数据Doris(九):配置BE步骤

文章目录 配置BE步骤 一、配置be节点

自建数据集,基于YOLOv7开发构建农田场景下杂草检测识别系统

在我们前面的一些文章中也做过不少跟农业相关的检测项目&#xff0c;感兴趣的话可以自行移步阅读即可&#xff0c;这里仅给出来最近的两个&#xff1a; 《激光除草距离我们实际的农业生活还有多远&#xff0c;结合近期所见所感基于yolov8开发构建田间作物杂草检测识别系统》 …

github Release 下载加速,绿色合法,遥遥领先

你有没有这样一个困惑&#xff0c;当你寻找了很久终于找到一个解决问题的方案&#xff0c;发现这个工具在 GitHub 上&#xff0c;接下来等待我们的就是遥遥无期的龟速下载。 文章目录 前言下载测试加速下载操作 视频讲解 遥遥领先 前言 GitHub 作为程序员的知识宝库&#xff…

stm32学习笔记:中断的应用:对射式红外传感器计次旋转编码器计次

相关API介绍 EXT配置API(stm32f10x exti.h&#xff09; NVIC 配置API (misc.h) 初始化的中断的步骤 第一步&#xff1a;配置RCC时钟&#xff0c;把涉及外设的时钟都打开 第二步&#xff1a;配置GPIO&#xff0c;设置为输入模式 第三步&#xff1a;配置AFIO&#xff0…

JFLASH基本使用总结

注意&#xff0c;不同版本的操作略有不同&#xff0c;本教程以J-Flash V5.12f为例。 烧录文件 如果是刚打开J-Flash&#xff0c;会弹出这样的一个工程选择界面&#xff0c;可以选择已有工程&#xff0c;或者创建新的工程&#xff0c;我们这里选择创建新工程。 注意&#xff0…

软件设计师学习笔记12-数据库的基本概念+数据库的设计过程+概念设计+逻辑设计

1.数据库的基本概念 1.1数据库的体系结构 1.1.1常见数据库 ①集中式数据库 数据是集中的&#xff1b;数据管理是集中的 ②C/S结构 客户端负责数据表服务&#xff1b;服务器负责数据库服务&#xff1b;系统分前后端&#xff1b;ODBC、JDBC ③分布式数据库 物理上分布、逻…

Windows-C盘清理

//1.常用软件&#xff1a; Chrome&#xff1a; C:\Users\Administrator\AppData\Local\Google\Chrome\User Data\Default\Code Cache\js C:\Program Files\Google\Chrome\Application\ //2.系统缓存 远程桌面缓存 C:\Users\Administrator\AppData\Local\Microsoft\Termin…

2023年电工(初级)证考试题库及电工(初级)试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年电工&#xff08;初级&#xff09;证考试题库及电工&#xff08;初级&#xff09;试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#…

C-Pack: Packaged Resources To Advance General Chinese Embedding

简介 论文提出了一个C-pack资源集合&#xff0c;其中包括三个主要的部分&#xff1a; C-MTEB一个中文综合基准集合&#xff0c;包括6个任务和35个数据集合。C-MTP一个中文embedding数据集合&#xff0c;包括unlabeled和labeled两种数据。C-TEM一个embedding模型家族&#xff0…