Vue通过file控件上传文件到Node服务器
功能: 1.多文件同时上传、2.拖动上传、3.实时上传进度条、4.中断上传和删除文件、5.原生file控件的美化
搁置的功能: 上传文件夹、大文件切片上传、以及其他限制条件未处理
Node服务器的前置准备:
新建文件夹: file_upload_serve初始化npm: npm init -y安装工具: npm add express multernodemon工具: npm install nodemon -gaxios: npm install axios -sNode运行版本: 18.17.1修改package.json文件"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},
改为:监听app.js"scripts": {"dev": "nodemon ./app.js"},
启动: npm run dev
Node > file_upload_serve > app.js
按前置准备完成,其他无需更改,请求部分全在app.js

/** @Description: * @Last Date: Do not edit*/
const express = require('express')
// post请求解析body
const bodyParser = require('body-parser')
// 上传工具库
const multer = require('multer')
const { writeFileSync } = require('fs')
const { resolve } = require('path')
const path = require('path')
const fs = require('fs')const app = express()
app.use(bodyParser.json({limit: '10mb', extended: true}))
// 静态资源共享(下载需要)
app.use(express.static(path.join(__dirname, 'public')))
// const storage = multer.diskStorage({
// destination: function (req, file, callback) {
// // 第一个参数: errorMessage; 参数2: 目标,即下载到哪个文件夹下
// callback(null, 'uploads/')
// },
// filename: function (req, file, callback) {
// // 获取上传文件的后缀名
// const ext = file.originalname.split('.')[1]
// callback(null, Date.now() + '.' + ext)
// }
// })
const storage = multer.diskStorage({destination: (req, file, cb) => {cb(null, 'uploads/') // 分片存储目录},filename: (req, file, cb) => {const ext = file.originalname.split('.')[1]if(req.body.rename) {cb(null, Date.now() + '.' + ext) // 单文件名} else {cb(null, `${req.body.index}-${req.body.fileName}`) // 分片文件名}}
})// 生成upload对象
const upload = multer({storage,
})// 设置请求头
app.all('*', (req, res, next) => {// 允许所有不同源的地址访问res.header('Access-Control-Allow-Origin', '*');// 跨域允许的请求方式res.header('Access-Control-Allow-Methods', 'GET, POST');// x-ext: 获取文件的后缀名// res.header('Access-Control-Allow-Headers', 'Content-Type, x-ext');// res.header('Access-Control-Allow-Headers', 'Content-Type');res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-ext");if (req.method.toLowerCase() == 'options'){res.send(200); //让options尝试请求快速结束} else {next()}})/* 上传方式1: multipart/form-data** upload.single 单文件上传*/
app.post('/file', upload.single('file'), (req, res) => {if(req.file){res.send('formData上传成功')} else {res.send('form-data上传失败')}
})/* 上传方式2: base64** upload.single 单文件上传*/
app.post('/base64', (req, res) => {const { file, ext, fileName } = req.bodyconst binaryData = Buffer.from(file, 'base64')if(!fileName) {writeFileSync(resolve(__dirname, 'uploads/' + Date.now() + '.' + ext), binaryData, 'binary')} else {writeFileSync(resolve(__dirname, 'uploads/' + fileName), binaryData, 'binary')}res.send('base64文件流上传成功')
})/* 上传方式3: binary 二进制** upload.single 单文件上传*/
app.post('/binary', (req, res) => {const ext = req.headers['x-ext']const buffers = []req.on('data', chunk => {buffers.push(chunk)}).on('end', () => {const binaryData = Buffer.concat(buffers)writeFileSync(resolve(__dirname, 'uploads/' + Date.now() + '.' + ext), binaryData, 'binary')res.send('二进制流上传成功')})
})
/* 多文件上传: formData** upload.array('formData中的字段名', 最大上传数量): */
app.post('/files', upload.array('files', 4), (req, res) => {console.log(req.files)if(req.files){res.send('多文件formData上传成功')} else {res.send('多文件formData上传失败')}
})
/* 文件下载* __dirname: 代表当前文件<app.js>所在的文件路径*/
app.get('/download', (req, res) => {try{// 下载路径: __dirname 拼接 第二个参数的路径const filePath = path.join(__dirname, '/public/download/1731726859151.txt')res.download(filePath)}catch(e){console.log(e)}})app.post('/merge', async (req, res) => {const uploadPath = '/uploads'let files = fs.readdirSync(path.join(process.cwd(), uploadPath)) // 获取所有的分片数据console.log(files)console.log(req.body.fileName)files = files.sort((a, b) => a.split('-')[0] - b.split('-')[0]) // 将分片按照文件名进行排序const writePath = path.join(process.cwd(), uploadPath, `${req.body.fileName}`) // 生成新的文件路径files.forEach((item) => {fs.appendFileSync(writePath, fs.readFileSync(path.join(process.cwd(), uploadPath, item))) // 读取分片信息,追加到新文件路径尾部fs.unlinkSync(path.join(process.cwd(), uploadPath, item)) // 将读取过的分片进行删除})res.send('ok')
})app.listen(8888, () => {console.log("链接成功")})
客户端
<!--* @Description: 功能: 1.多文件同时上传、2.拖动上传、3.实时上传进度条、* 4.中断上传和删除文件、5.原生file控件的美化* 搁置的功能: 上传文件夹、大文件切片上传、以及其他限制条件未处理* @Last Date: Do not edit
-->
<template><div class="container"><header><divclass="box"@drop="handleClick"@dragenter="handleClick"@dragover="handleClick"@dragleave="handleClick"><div class="box-font"><div><span style="display: flex; align-items: center"><i class="el-icon-upload"> </i><p>将目录或多个文件拖拽到此进行扫描</p></span></div><div><span>支持的文件类型: .JPG、.JPEG、.BMP、.PNG、.GIF、.ZIP、</span></div><div><span>每个文件允许的最大尺寸: 1M</span></div></div></div></header><main><div class="main-choose-files-btn"><div class="file-box"><input type="button" class="btn" value="选择文件" /><inputtype="file"class="file"@change="previewMoreFilesByFormData"multiple/></div><div class="file-box"><input type="button" class="btn" value="选择文件夹" /><inputtype="file"class="file"@change="previewMoreFilesByFormData"multiple/></div></div><div><el-table :data="tableData" stripe style="width: 85%"><!-- <el-table-columnv-for="item in tableColumn":key="item.prop":prop="item.prop":label="item.label"></el-table-column> --><el-table-columnprop="name"label="文件名"width="240"fixed></el-table-column><el-table-column prop="type" label="类型"></el-table-column><el-table-column prop="size" label="大小"></el-table-column><el-table-column prop="state" label="状态"><!-- 当template中有多个元素需要切换时,需要在最外层使用div将所有元素包裹住 --><!-- slot-scope="scope" 必须加,否则数据不是响应式的 --><template slot-scope="scope"><div><divv-show="scope.row.progressPercent > 0 &&scope.row.progressPercent < 100"><el-progress:text-inside="true":stroke-width="15":percentage="scope.row.progressPercent"/></div><divv-show="scope.row.progressPercent < 1"slot="reference"class="name-wrapper"><el-tag size="medium"> 待上传 </el-tag></div><divv-show="scope.row.progressPercent === 100"slot="reference"class="name-wrapper"><el-tag size="medium"> 已上传 </el-tag></div></div></template></el-table-column><el-table-column label="操作"><template slot-scope="scope"><i class="el-icon-delete" @click="deleteFile(scope.row)"></i></template></el-table-column></el-table></div></main><footer><el-row><el-button class="foot-btn" size="mini"><span>文件数量: {{ tableData.length }}</span></el-button><el-button class="foot-btn" type="success" plain size="mini">成功数量: {{ successCount }}</el-button><el-button class="foot-btn" size="mini"><span>总大小: {{ countSize }} MB</span></el-button></el-row><el-row class="upload-btn"><el-buttontype="primary":disabled="uploadDisabled"@click="handelUploadMoreFile">开始上传</el-button></el-row></footer></div>
</template><script>
import axios from "axios"
export default {data() {return {ext: undefined, // 文件后缀名tableData: [],tableColumn: [{ prop: "name", label: "文件名" },{ prop: "type", label: "类型" },{ prop: "size", label: "大小" },{ prop: "progressPercent", label: "状态" },{ prop: "option", label: "操作" },],filesNumber: 1, // 列表文件总条数successCount: 0, // 上传成功条数cancelTokens: [], // 存储每个请求的取消方法}},mounted() {// 阻止事件冒泡,防止在拖拽后意外打开新标签页document.body.ondrop = function (event) {event.preventDefault()event.stopPropagation()}},computed: {getByte() {return this.tableData.reduce((a, b) => {return a + b.size}, 0)},// 文件总大小countSize() {return (this.getByte / 1048576).toFixed(2)},// 是否禁用"上传按钮"uploadDisabled() {return this.tableData.length > 0 ? false : true},},methods: {// 读取多个文件previewMoreFilesByFormData(e, drop) {let filesif (!drop) {files = e.target.files} else {files = e}// 获取文件后缀名this.ext = files[0].name.split(".")[1]if (!files) returnvar i = 0var _this = thisvar funcs = function () {if (files[i]) {var reader = new FileReader()reader.onload = function (e) {const uint8Array = new Uint8Array(e.target.result)const str = uint8Array.reduce((prev, byte) => {prev += String.fromCharCode(byte)return prev}, "")let now = new Date()// 由于JS执行速度很快,极大可能会得到一样的时间戳,故将timestamp加上下标// timestamp的作用是在将来删除文件时,作为唯一id对比删除let timestamp = now.getTime()// 将预览的文件中数据转换到table中_this.tableData.push({timestamp: timestamp + i,name: files[i].name,type: files[i].type,size: files[i].size,progressPercent: 0,dataBase64: btoa(str),})// progressPercent 上传进度条i++funcs() // onload为异步调用}reader.readAsArrayBuffer(files[i])}}funcs()},/** 删除上传文件* 不能通过数组下标去删。删除再添加新文件时,下标会重复* @param row(行数据)*/deleteFile(row) {// 删除文件this.tableData = this.tableData.filter((item) => item.timestamp !== row.timestamp)// 中断请求const requestToCancel = this.cancelTokens.find((token) => token.requestId === row.requestId)if (requestToCancel && requestToCancel.cancel) {requestToCancel.cancel("Request was canceled by the user.")}},// 上传文件handelUploadMoreFile() {let _this = thisconst CancelToken = axios.CancelTokenconst List = []for (let i = 0; i < this.tableData.length; i++) {// console.log(source.cancel)const ext = this.ext // 文件后缀名// 为每个请求创建一个新的取消令牌源const cancelTokenSource = CancelToken.source()this.cancelTokens.push({requestId: i,cancel: cancelTokenSource.cancel,})// 给tableData设置"requestId",将来取消请求通过比对id对应到具体的请求this.tableData[i].requestId = ivar a = axios({url: "http://localhost:8888/base64",method: "post",cancelToken: cancelTokenSource.token,data: {ext,fileName: this.tableData[i].name,file: this.tableData[i].dataBase64,},onUploadProgress: (progressEvent) => {/** 上传进度条* progressEvent.loaded: 已上传文件大小* progressEvent.total: 被上传文件的总大小*/_this.tableData[i].progressPercent =(progressEvent.loaded / progressEvent.total) * 100},}).then((res) => {// this.$message({// message: '文件上传成功',// type: 'success'// })// console.log(res)}).catch((error) => {if (axios.isCancel(error)) {console.log(error.message)} else {console.log(error.message)}})List.push(a)}// 合并异步上传Promise.all(List).then((res) => {}).catch((err) => {})},// 处理鼠标拖放事件handleClick(e) {if (e.type == "dragenter") {// this.className = "drag_hover"}if (e.type == "dragleave") {// this.className = ""}if (e.type == "drop") {var files = e.dataTransfer.filesthis.className = ""if (files.length != 0) {this.previewMoreFilesByFormData(files, "drop")}}if (e.type == "dragover") {// e.dataTransfer.dragEffect = "copy"}},},
}
</script><style lang="scss">
body,
html {list-style: none;padding: 0;margin: 0;
}
.container {width: 85%;margin: 25px auto;.box {width: 85%;height: 300px;border-style: dashed; // border虚线border-width: 1px;margin-bottom: 20px;display: flex; /* 启用 Flexbox */justify-content: center; /* 水平居中 */align-items: center; /* 垂直居中 */.box-font {display: flex;flex-direction: column;align-items: center;gap: 30px;span {display: block;}}}main {.main-choose-files-btn {display: flex;gap: 100px;height: 44px;}}footer {margin-top: 20px;.upload-btn {margin-top: 10px;}}
}
// 对原生file控件优化
.btn,
.file {@extend .merge-input;
}
.merge-input {// display: block;position: absolute;width: 75px;height: 35px;color: #fff;border-radius: 4px;border-color: #409eff;
}
.btn {z-index: 2;background: #409eff; // #66b1ff 409effpointer-events: none; /* 让事件传递到下一层,即: btn的层级比file高,但btn能触发file的事件 */
}
.file {z-index: 1;
}
// el-table表头样式修改
.el-table th {font-size: 13px;font-weight: 700;
}.el-table .el-table__header th,
.el-table .el-table__header tr,
.el-table .el-table__header td {background: #f5f8fd;
}.el-icon-upload {font-size: 35px;
}
</style>相关文章:
Vue通过file控件上传文件到Node服务器
功能: 1.多文件同时上传、2.拖动上传、3.实时上传进度条、4.中断上传和删除文件、5.原生file控件的美化 搁置的功能: 上传文件夹、大文件切片上传、以及其他限制条件未处理 Node服务器的前置准备: 新建文件夹: file_upload_serve初始化npm: npm …...
如何在 SQL Server 中新增账户并指定数据库权限
在日常的数据库维护与开发中,管理用户的权限是必不可少的一环。本文将指导你如何在 SQL Server 中为新用户创建账户,并为其指定相应的数据库权限,使其只能查看有权访问的数据。我们将以步骤和代码示例的方式展示整个流程。用户权限分配包括:读取权限、写入权限、执行权限。…...
c#编码技巧(十九):各种集合特点汇总
.NET 常用集合对比: .NET 常见的线程安全集合 .NET 只读集合...
汽车软件DevOps解决方案
汽车软件DevOps解决方案是专为现代汽车行业设计的一套集成化需求、开发、测试、部署、OTA与监控,旨在加速软件开发流程,提高软件质量和安全性,同时确保整个生命周期的高效性和灵活性。以下是经纬恒润汽车软件DevOps解决方案的关键组成部分和优…...
同步的意义以及机制
一、同步的意义 同步(Synchronization)的意义在于确保在多线程环境中,多个线程对共享资源的访问是安全的,避免竞争条件(race conditions)和数据不一致的情况。 具体来说,同步的核心目标是&…...
leetcode 面试150之 156.LUR 缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -…...
启发式搜索算法复现
🏡作者主页:点击! 🤖编程探索专栏:点击! ⏰️创作时间:2024年11月21日19点05分 神秘男子影, 秘而不宣藏。 泣意深不见, 男子自持重, 子夜独自沉。 论文链接 点击开启你的论文编程之旅…...
【IDE】使用指南
定期更新实用技能,建议关注收藏点赞。 友情链接: 点击跳转常见代码编辑器的报错解决方案 目录 常用快捷键pycharm右下角边栏脚本头安装IDE的插件git配置TODO 代码编辑器里有许多小技巧,便于办公。本篇主要以pycharm,vscode等主流常用IDE为…...
设计编程网站集:简述可扩展性系统设计(笔记)
视频连接:简述可扩展性系统设计 三个关键原则 无状态 松散耦合 异步处理 扩展 负载均衡 缓存 分片...
「Mac玩转仓颉内测版25」基础篇5 - 布尔类型详解
本篇将介绍 Cangjie 中的布尔类型,包括布尔值的定义、运算操作符、逻辑运算、布尔类型的常见应用场景及其在条件判断中的应用,帮助开发者理解和使用布尔类型。 关键词 布尔类型定义布尔运算逻辑运算符条件判断常见应用场景 一、布尔类型概述 布尔类型&…...
Fashion-VDM:引领视频虚拟试穿技术的新篇章
引言 随着虚拟现实和增强现实技术的飞速发展,视频虚拟试穿(VVT)已成为时尚产业的一大创新领域。然而,现有的VVT方法在服装细节和时间一致性方面仍存在诸多不足。为了解决这些问题,Johanna Karras等人提出了Fashion-VDM,一种基于视频扩散模型(VDM)的新型视频虚拟试穿技…...
Scala中的集合复习(1)
Map、Set、Array、List 一、集合的三大类 1.序列Seq表示有先后顺序的集合。(Array、List) 2.集Set:表示无序且不重复的集合。 3.映射Map:表示键值对。 Stack:栈,特点是:后进先出。 packag…...
Java依赖包漏洞检测命令
1、漏洞扫描工具 maven插件方式:Dependency-Check 2、命令 检查单个 Maven 工程的安全漏洞 mvn dependency-check:check 这个命令会在 target 目录下生成一个 dependency-check-report.html 文件,其中包含了依赖项的安全漏洞分析报告。 检查多个 M…...
【Java】强制类型转换
int a23; short b(short) a; 小的接受大的接受不了,强制类型转换. 带有Buffer的,带有流的,都是数组。 网络流,文件流都是数组. 这种就是流。 操作系统底层就是C. 没有直系关系的,不让转换 语法不报错,运行…...
RabbitMQ消息可靠性保证机制4--消费端限流
7.7 消费端限流 在类似如秒杀活动中,一开始会有大量并发写请求到达服务端,城机对消息进行削峰处理,如何做? 当消息投递的速度远快于消费的速度时,随着时间积累就会出现“消息积压”。消息中间件本身是具备一定的缓冲…...
查找萤石云IOS Sdk中的编解码接口
2021/1/20 以前的时候,碰到的问题,想把萤石云视频介入到TRTC,但是... 萤石云的IOS接口中没有相应的解码播放库,也就是找不到PlayerSDK对应部分,怎么做呢? 一个是坐等萤石云开放这部分接口,可能…...
erchas
#include <iostream> #include <vector> https://gitee.com/tongchaowei/front-native-page-template/tree/main/image-display/template-01 using namespace std; class BinaryTree { private: vector<char> tree; // 存储二叉树的数组 int size;…...
【网络安全】SSL(一):为什么需要 Keyless SSL?
未经许可,不得转载。 文章目录 背景正文背景 随着网站和应用程序向云端迁移,使用 HTTPS(SSL/TLS)加密流量已成为行业标准。然而,传统的 HTTPS 配置要求服务器持有网站的私钥,这在云计算环境中引发了一系列安全性和合规性问题。一旦云服务器遭到攻击,私钥泄露可能带来不…...
ggplot2 分面图等添加注释文字,相加哪里加哪里: 自定义函数 AddText()
如果分面图上还想再添加文字,只能使用底层的grid包了。 函数定义 # Add text to ggplot2 figures # # param label text you want to put on figure # param x position x, left is 0, right 1 # param y position y, bottom is 0, up 1 # param color text color…...
解读缓存问题的技术旅程
目录 前言1. 问题的突发与初步猜测2. 缓存的“隐身术”3. 缓存策略的深层优化4. 反思与感悟结语 前言 那是一个普通的工作日,团队例行的早会刚刚结束,我正准备继续优化手头的模块时,突然收到了用户反馈。反馈的内容是部分数据显示异常&#…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
MySQL:分区的基本使用
目录 一、什么是分区二、有什么作用三、分类四、创建分区五、删除分区 一、什么是分区 MySQL 分区(Partitioning)是一种将单张表的数据逻辑上拆分成多个物理部分的技术。这些物理部分(分区)可以独立存储、管理和优化,…...
【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道
文/法律实务观察组 在债务重组领域,专业机构的核心价值不仅在于减轻债务数字,更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明,合法债务优化需同步实现三重平衡: 法律刚性(债…...
