大文件上传实现-基础上传功能(Vue + Express)

这篇文章讲解实现基础的文件上传的前后端逻辑,是大文件上传实现专栏的第一篇文章。

下面将分为后端部分实现的讲解和前端部分实现的讲解,先讲后端后讲前端。

  • 后端部分

后端部分,用 Express.js 来做。先捋一下需求:接口要能够接收文件并保存到特定文件夹下面,能够指定文件名。

sh 复制代码
# 创建项目
mkdir server-by-express
cd server-by-express
npm init -y
# 安装将用到的依赖
npm i express fs-extra cors

新建项目之后,在vscode中打开,新建一个index.js文件,键入如下代码:

js 复制代码
const express = require("express");
const cors = require("cors");
const fs = require("fs-extra");

const app = express();
app.use(cors());

app.get("/", (req, res) => {
  res.json({ message: "Hello World" });
});

app.listen(8080, () => {
  console.log("Server is running on port 3000");
});

在项目根目录下执行 node index.js 跑起来看下

测试一下没问题

先确保文件上传的目录存在

js 复制代码
// 临时文件夹,文件将会被上传到这个文件夹中
const TEMP_DIR = path.resolve(__dirname, "temp");
// 确保临时文件夹存在
fs.ensureDir(TEMP_DIR);

然后定义一个辅助函数,用来将可读流流入到可写流:

js 复制代码
/**
 * 数据从可读流流向可写流
 * @param {ReadableStream} rs 可读流
 * @param {WritableStream} ws 可写流
 * @returns 返回一个Promise,当流结束时,Promise会被resolve
 */
function pipeStream(rs, ws) {
  return new Promise((resolve, reject) => {
    rs.pipe(ws).on("finish", resolve).on("error", reject);
  });
}

接下来实现上传文件接口:

js 复制代码
app.post("/upload/:filename", async (req, res) => {
  // 获取文件名
  const { filename } = req.params;
  // 拼接文件路径
  const filePath = path.resolve(TEMP_DIR, filename);
  // 创建可读流
  const ws = fs.createWriteStream(filePath);
  // 将可读流中的数据写入到可写流中,完成文件上传
  await pipeStream(req, ws);
  res.json({ success: true });
});

注意这里以流的形式接收文件,而不是FormData

为了方便测试,全局安装下 npm i nodemon -g, 然后在package.json添加一条script

然后执行 npm run start重启下,后面修改了就会自动重启了。

下面用postman来测试下

用 post 请求,body 里面选择二进制文件,测试没问题,后端基础上传文件部分就完成了

  • 前端部分

将要做的基础前端部分功能如下:

vite初始化项目,选择 vue, 用 javascript 就好, 项目名为 client-by-vue

sh 复制代码
# 创建和启动项目
npm create vite@latest
cd client-by-vue
npm i
npm run dev

先把不用的文件删除掉,保持简单

打开网址看看,没问题。

然后把后面会用到依赖安装了先,分别是element-plusaxioselement-plus安装按照官网指南全量安装,包括图标

sh 复制代码
npm i element-plus @element-plus/icons-vue axios

我们试下:

好了,以上就是前端需要准备的部分,下面开始开发文件上传功能

先把样式写出来:画一个文件选择框和一个按钮,鼠标悬浮和边框变色

html 复制代码
<template>
  <div id="upload-container">
    <el-icon :size="30"><Files /></el-icon>
  </div>

  <el-button>开始上传</el-button>
</template>

<script setup></script>

<style scoped>
#upload-container {
  height: 200px;
  line-height: 200px;
  text-align: center;
  border: 1px dashed #ccc;
  margin-bottom: 20px;
  cursor: pointer;
}
#upload-container:hover,
#upload-container.is-dragover {
  border: 1px dashed #0037ff;
  color: #0037ff;
}
</style>

实现拖拽选择文件功能

拖拽获取到文件,需要用到drog事件,在 event.dataTransfer读取到,同时要阻止浏览器默认事件; isDragover用于控制拖拽的高亮

html 复制代码
  <div
    id="upload-container"
    :class="{ 'is-dragover': isDragover }"
    @drop="handleDrop"
    @dragover.prevent
    @dragenter.prevent="isDragover = true"
    @dragleave.prevent="isDragover = false"
  >
    <el-icon :size="30"><Files /></el-icon>
  </div>
js 复制代码
const { isDragover } = toRefs(
  reactive({
    isDragover: false,
  })
);

const handleDrop = (e) => {
  e.preventDefault();
  const { files } = e.dataTransfer;
  console.log("files: ", files);
};

效果是这样的

这样就拿到了拖拽的File文件对象了,可以用来上传

实现点击选择文件功能

除了拖拽上传,常用的方式还有点击选择文件的,也来实现一下:利用input:type=file的点击事件,可以打开选择文件的弹框。

html 复制代码
  <div
    id="upload-container"
    ...
    @click="handleClick"
    ...
  >
    <el-icon :size="30"><Files /></el-icon>
  </div>
js 复制代码
const handleClick = () => {
  const input = document.createElement("input");
  input.type = "file";
  input.addEventListener("change", (e) => {
    const { files } = e.target;
    console.log("files: ", files);
  });
  input.click();
};

效果同样可以拿到文件对象

例子以视频和图片讲解,来展示一下拿到视频文件和图片文件吧

js 复制代码
// 新增一个 selectedFile 用来存放文件信息
const { isDragover, selectedFile } = toRefs(
  reactive({
    isDragover: false,
    selectedFile: { url: "", file: null },
  })
);

const handleDrop = (e) => {
  e.preventDefault();
  const { files } = e.dataTransfer;
  if (files.length === 0) return;
  // 限制只能上传图片或视频
  const isMedia =
    files[0].type.indexOf("image") > -1 || files[0].type.indexOf("video") > -1;
  if (!isMedia) {
    return ElMessage.warning("只能上传图片或视频");
  }
  // 拿到文件之后,存放到 selectedFile 中, url 为文件的本地路径,file 为文件对象
  selectedFile.value = {
    url: URL.createObjectURL(files[0]),
    file: files[0],
  };
};

const handleClick = () => {
  const input = document.createElement("input");
  input.type = "file";
  input.addEventListener("change", (e) => {
    const { files } = e.target;
    if (files.length === 0) return;
    // 限制只能上传图片或视频
    const isMedia =
      files[0].type.indexOf("image") > -1 ||
      files[0].type.indexOf("video") > -1;
    if (!isMedia) {
      return ElMessage.warning("只能上传图片或视频");
    }
    // 拿到文件之后,存放到 selectedFile 中, url 为文件的本地路径,file 为文件对象
    selectedFile.value = {
      url: URL.createObjectURL(files[0]),
      file: files[0],
    };
  });
  input.click();
};
html 复制代码
  <div
    id="upload-container"
    ...
  >
    <template v-if="selectedFile.url">
      <img
        v-if="selectedFile.file.type.indexOf('image') > -1"
        :src="selectedFile.url"
      />
      <video
        controls
        v-if="selectedFile.file.type.indexOf('video') > -1"
        :src="selectedFile.url"
      ></video>
    </template>
    <el-icon v-else :size="30"><Files /></el-icon>
  </div>

效果是这样的

好了,选择文件功能搞定,接下来我们简单封装一下axios, 用来发送上传文件请求

src 目录下面新建一个 axiosInstance.js 文件,内容如下

js 复制代码
// axios 设置baseURL 响应拦截器
import axios from "axios";

const axiosInstance = axios.create({
  baseURL: "http://localhost:8080",
});

axiosInstance.interceptors.response.use(
  (response) => {
    if (response.data && response.data.success) {
      return response.data;
    }
  },
  (error) => {
    console.log("error: ", error);
    return Promise.reject(error);
  }
);

export default axiosInstance;

在 App.js 文件中引入 axiosInstance.js 来发送请求

js 复制代码
import axiosInstance from "./axiosInstance";
...
const handleUpload = () => {
  if (!selectedFile.value.file) {
    return ElMessage.warning("请先选择文件");
  }
  const file = selectedFile.value.file;
  axiosInstance
    .post(`/upload/${file.name}`, file, {
      headers: {
        "Content-Type": "application/octet-stream",
      },
    })
    .then((res) => {
      ElMessage.success("上传成功");
    })
    .catch((err) => {
      ElMessage.error("上传失败");
    });
}

开始发送请求,注意 请求头设置 application/octet-stream

我们试一下

上面演示的都是小文件,本地上传速度很快,接下来我们试一下大一点的文件,我找了一个1.5G的视频,我们来试一下

在本地没有网络延迟的情况下,虽然上传成功了,但是有明显的等待感,网络请求一直pending,体验不是很好,我们加个显示上传进度来优化一下

js 复制代码
// 添加一个对象,用来保存进度信息
const { 
    ...
    progressInfo
  } = toRefs(
  reactive({
    ...
    progressInfo: {
      name: "",
      percent: 0,
    },
  })
);

const handleUpload = () => {
  ...
  const file = selectedFile.value.file;
  axiosInstance
    .post(`/upload/${file.name}`, file, {
      headers: {
        "Content-Type": "application/octet-stream",
      },
      // 这里更新进度信息
      onUploadProgress(progressEvent) {
        progressInfo.value = {
          name: file.name,
          percent: Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          ),
        };
      },
    })
    ...
};
html 复制代码
  <div class="progress" v-if="progressInfo.percent > 0">
    <span>{{ progressInfo.name }}</span>
    <el-progress :percentage="progressInfo.percent"></el-progress>
  </div>

我们试一下效果

到这里,前端基础上传文件的部分也完成了。代码可以跟着上面的讲解敲一遍加深印象,同时我放到gitee上面,有需要自取。

大文件上传实现 的下一篇文章主题是"分片上传",将在这篇文章的实现基础上进行改造升级,敬请期待。

相关推荐
知识分享小能手3 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
我命由我123456 小时前
前端开发问题:SyntaxError: “undefined“ is not valid JSON
开发语言·前端·javascript·vue.js·json·ecmascript·js
海天胜景6 小时前
vue3 当前页面方法暴露
前端·javascript·vue.js
天天向上10248 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y8 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁8 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
写不出来就跑路9 小时前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui
前端小盆友10 小时前
从零实现一个GPT 【React + Express】--- 【4】实现文生图的功能
react.js·chatgpt·express
1undefined210 小时前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js
paopaokaka_luck10 小时前
基于SpringBoot+Vue的非遗文化传承管理系统(websocket即时通讯、协同过滤算法、支付宝沙盒支付、可分享链接、功能量非常大)
java·数据库·vue.js·spring boot·后端·spring·小程序