前后端分离项目中图片的上传和获取
前端使用VS code作为代码编辑器,技术栈涉及vue、axios和element-plus,后端使用IDEA作为代码编辑器,技术栈涉及springMVC、spring、mybatis。在上述图片中,点击“上传头像”选择一张图片,将其保存到浏览器的内存中,点击“保存”按钮,将其发送到后端进行保存,并将图片的地址写入数据库。
项目介绍
前端使用VS code作为代码编辑器,技术栈涉及vue、axios和element-plus,后端使用IDEA作为代码编辑器,技术栈涉及springMVC、spring、mybatis。
在上述图片中,点击“上传头像”选择一张图片,将其保存到浏览器的内存中,点击“保存”按钮,将其发送到后端进行保存,并将图片的地址写入数据库。
图片上传前端代码
<script lang="ts" setup>
import type { UploadProps } from 'element-plus'
import { error, successEasy, warning } from '../../../utils/notification'
import { customerInfo } from '../../../utils/pojo.js'
import { axiosInstance } from '../../../utils/request.js'
import { ref } from 'vue';
/*
头像上传过程:
1、用户选择文件后,检察文件的类型和大小。
2、文件符合要求后,以二进制数据的形式保存在浏览器的内存中。
3、点击“保存”按钮后,将二进制数据发送到后端服务器。
*/
let picturePath = ref(customerInfo.portraitPath);
let passMuster = ref(0);
let pictureFile = ref();
//上传头像之前,检察文件的类型和大小
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
if(rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/jpg' && rawFile.type !== 'image/png') {
warning('文件类型有误');
return false;
}else if(rawFile.size / 1024 / 1024 > 5) {
warning('头像大小超过5M');
return false;
}
passMuster.value = 1;
picturePath.value = URL.createObjectURL(rawFile);
pictureFile.value = rawFile;
return true;
}
//将图片发送到后端保存
let uploadPicture = () => {
if(passMuster.value == 1 && pictureFile.value){
let formData = new FormData();
formData.append('image',pictureFile.value,pictureFile.value.name);
axiosInstance.post('/customerInfo/uploadPicture',formData)
.then((response) => {
if(response.data.code != 200){
error('上传',response.data.message);
}else{
successEasy('上传','');
getHeadPicture();
}
})
.catch((errors) => {
console.log(errors);
});
}else{
warning('图片有误,无法上传')
}
}
//获取用户头像
let getHeadPicture = () => {
axiosInstance.get('/customerInfo/getHeadPicture',{responseType:'blob'})
.then((response) => {
customerInfo.portraitPath = URL.createObjectURL(new Blob([response.data]));
})
.catch((errors) => {
console.log(errors);
});
}
</script>
<template>
<div id="third-area">
<div id="upload-area">
<el-upload action=""
v-bind:show-file-list="false"
v-bind:before-upload="beforeUpload">
<el-button class="upload-button">上传头像</el-button>
<p id="upload-tip">支持大小不超过 5M 的 jpg、jpeg、png 图片</p>
</el-upload>
</div>
<div id="show-picture">
<img v-bind:src="picturePath">
</div>
<div id="picture-preview">
<p id="preview-title">效果预览</p>
<p id="preview-description">你上传的图片会自动生成以下尺寸, 请注意是否清晰。</p>
<img v-bind:src="picturePath" id="img-preview1">
<img v-bind:src="picturePath" id="img-preview2">
</div>
<button id="picture-submit" @click="uploadPicture()">保存</button>
</div>
</template>
<style scoped>
#third-area {
width: 830px;
height: 480px;
margin-left: 20px;
margin-top: 20px;
padding-top: 10px;
background-color: white;
border-radius: 10px;
}
#upload-area {
width: 500px;
height: 50px;
margin-left: 70px;
margin-top: 20px;
}
.upload-button {
width: 100px;
height: 35px;
background-color: rgb(250, 250, 250);
color: black;
border: 1px rgb(198, 194, 194) solid;
}
#upload-button:hover {
background-color: rgb(255, 255, 255);
}
#upload-tip {
font-size: 14px;
margin-left: 20px;
}
#show-picture {
width: 400px;
height: 300px;
background-color: rgb(238, 232, 232);
margin-left: 70px;
margin-top: 10px;
float: left;
}
#show-picture img {
width: 300px;
height: 225px;
margin-left: 50px;
margin-top: 35px;
}
#picture-preview {
width: 250px;
height: 300px;
background-color: white;
margin-left: 500px;
margin-top: 10px;
border-left: 1px rgb(226, 211, 211) solid;
}
#picture-submit {
width: 80px;
height: 35px;
margin-left: 200px;
margin-top: 40px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
color: white;
background-color: rgb(248, 83, 37);
}
#picture-submit:hover {
background-color: rgb(254, 103, 61);
}
#preview-title {
font-size: 18px;
margin-left: 28px;
}
#preview-description {
font-size: 14px;
margin-top: 5px;
margin-left: 28px;
}
#img-preview1 {
width: 110px;
height: 110px;
margin-top: 20px;
margin-left: 28px;
}
#img-preview2 {
width: 80px;
height: 80px;
margin-top: 20px;
margin-left: 28px;
}
</style>
代码解释
<el-upload action=""
v-bind:show-file-list="false"
v-bind:before-upload="beforeUpload">
<el-button class="upload-button">上传头像</el-button>
<p id="upload-tip">支持大小不超过 5M 的 jpg、jpeg、png 图片</p>
</el-upload>
前端项目使用了element-plus的<el-upload>组件,将action的值设为空,目的是将图片保存到浏览器的内存中而不发送出去,show-file-list表示是否展示上传的文件列表,before-upload表示上传前要执行的函数,这里只要写函数名,不要写大括号。
//上传头像之前,检察文件的类型和大小
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
if(rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/jpg' && rawFile.type !== 'image/png') {
warning('文件类型有误');
return false;
}else if(rawFile.size / 1024 / 1024 > 5) {
warning('头像大小超过5M');
return false;
}
passMuster.value = 1;
picturePath.value = URL.createObjectURL(rawFile);
pictureFile.value = rawFile;
return true;
}
这里使用的是TypeScript,因为element-plus是用TypeScript编写的,其他的组件有些可兼容JavaScript,但是文件上传涉及文件类型的导入,必须用TypeScript,只需要加个lang="ts"就好了(<script lang="ts" setup>)。TypeScript实在JavaScript的基础上开发的,相当于是对JavaScript的扩展,所以语法差不多。
这里的rawFile表示上传的原始文件,即选取的文件,passMuster的值表示文件的格式是否符合要求,picturePath表示展示图片的地址,pictureFile表示图片的值,将rawFile的值赋给pictureFile用于之后发送到后端。URL.createObjectURL(rawFile)表示将rawFile在浏览器的内存中生成一个地址,用于指向该图片。
我这里设置了文件类型只能是jpeg、jpg、png,其他格式的文件都会报错,报错通过warning()方法展示。这里我搞了一个工具文件,专门用于提示信息,使用的是element-plus的notification实现的。
import { ElNotification } from 'element-plus'
//弹出一个成功的提示框
const success = (message1,message2,fun) => {
ElNotification({
title: `${message1}成功`,
message: `即将跳转到${message2}`,
type: 'success',
duration: 2000,
showClose: false,
onClose: fun
})
}
//弹出一个警告的提示框
const warning = (message3) => {
ElNotification({
title: '警告',
message: message3,
type: 'warning',
duration: 3000,
showClose: false
})
}
//弹出一个错误的提示框
const error = (message4,message5) => {
ElNotification({
title: `${message4}失败`,
message: message5,
type: 'error',
duration: 3000,
showClose: false
})
}
//弹出一个简易的成功提示框
const successEasy = (message6,message7) => {
ElNotification({
title: `${message6}成功`,
message: message7,
type: 'success',
duration: 2000,
showClose: false,
})
}
//统一对外暴露
export {
success,
warning,
error,
successEasy
}
<el-upload>的v-bind:before-upload的值beforeUpload就是检查文件格式的方法名。
//将图片发送到后端保存
let uploadPicture = () => {
if(passMuster.value == 1 && pictureFile.value){
let formData = new FormData();
formData.append('image',pictureFile.value,pictureFile.value.name);
axiosInstance.post('/customerInfo/uploadPicture',formData)
.then((response) => {
if(response.data.code != 200){
error('上传',response.data.message);
}else{
successEasy('上传','');
getHeadPicture();
}
})
.catch((errors) => {
console.log(errors);
});
}else{
warning('图片有误,无法上传')
}
}
首先判断检查文件的结果,格式是否符合要求,用户是否上传了文件,通过后进行下一步。创建一个FormData对象,这个对象将包含上传图片的所有信息,使用append()方法,将图片值和图片名放入其中。'image'相当于是这张图片的id。发送的地址是 http://localhost:8080/fun-market/customerInfo/uploadPicture。
我这里的axiosInstance其实就是axios,只不过我在axios的拦截器中加入了一些操作,将token加入请求头中,用于用户身份的验证。
import axios from 'axios';
import { error } from './notification.js'
//公共的域名
const unificationURL = 'http://localhost:8080/fun-market';
//创建一个axios实例
const axiosInstance = axios.create({
baseURL:unificationURL,
timeout:10000
})
//请求拦截器
axiosInstance.interceptors.request.use(
config => {
let token = window.sessionStorage.getItem('token');
config.headers.token = token;
return config;
},
errors => {
console.log(errors);
error('请求','用户未登录');
}
)
export{
unificationURL,
axiosInstance
}
图片上传后端代码
package com.bxt.fm.controller;
import com.bxt.fm.pojo.CustomerInfo;
import com.bxt.fm.service.CustomerInfoService;
import com.bxt.fm.util.JwtHelper;
import com.bxt.fm.util.Result;
import com.bxt.fm.util.ResultCodeEnum;
import com.bxt.fm.util.UploadFile;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@CrossOrigin
@RestController
@RequestMapping("/customerInfo")
public class CustomerInfoController {
@Resource(name = "customerInfoService")
private CustomerInfoService customerInfoService;
//头像上传
@PostMapping("/uploadPicture")
public Result<String> uploadPicture(
@RequestHeader("token") String token,
@RequestParam("image") MultipartFile multipartFile
){
String signId = JwtHelper.getSignId(token);
if (signId != null) {
int customerId = Integer.parseInt(signId);
if (multipartFile == null){
return Result.build(null,ResultCodeEnum.UPLOAD_FAIL);
}
int checkFormat = UploadFile.checkFormat(multipartFile);
if (checkFormat == 207){
return Result.build(null,ResultCodeEnum.PICTURE_LARGE);
}else if (checkFormat == 208){
return Result.build(null,ResultCodeEnum.PICTURE_TYPE);
}
String picturePath = UploadFile.savePicture(multipartFile);
return customerInfoService.uploadPicture(picturePath,customerId);
}
return Result.build(null, ResultCodeEnum.LOGIN_AUTH);
}
}
代码解释
这里的@RequestHeader("token") String token表示获取请求头中的token,仅用于身份验证,对图片上传没关系。重点是@RequestParam("image") MultipartFile multipartFile,之前的文件是传输一个formData对象,在后端需要使用MultipartFile来进行接收。这里的Result是我写的一个统一返回结果类,便于前端进行解析响应,对图片上传没关系。
配置
仅仅这样写是无法实现业务的,需要进行配置,我的项目是使用SSM进行编写的,采取配置类的方式进行配置。我没有使用springboot所以需要配置较多的配置类,其中WebConfig是spring MVC的配置类,SpringConfig是spring的配置类,MybatisConfig是mybatis的配置类,DataSourceConfig是数据库连接池的配置类,InitializerConfig是初始化配置类。
在这个图片上传业务中,需要在WebConfig和InitializerConfig中进行配置。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ComponentScan("com.bxt.fm.controller")
@EnableWebMvc //启用SpringMVC框架的默认配置,包括消息转换器、格式化器以及验证器等,此外还启用了SpringMVC的注解驱动。
public class WebConfig implements WebMvcConfigurer {
//配置文件上传解析器
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
}
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
//初始化配置类
public class InitializerConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
//用于指定root容器的配置类,包括数据源(DataSource)、服务(Service)和映射器(Mapper)的配置
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {
SpringConfig.class,
MybatisConfig.class,
DataSourceConfig.class
};
}
//用于指定Web容器的配置类,通常包括SpringMVC的配置
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
//用于指定DispatcherServlet的处理路径
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
//配置文件上传的相关参数
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("D:\\大学\\大四\\毕业设计\\项目代码\\趣集后端代码\\fun-market\\src\\main\\webapp\\uploadTemp\\",
5*1024*1024,
7*1024*1024,
0
)
);
}
}
第一个参数表示存储临时文件的位置,前端上传文件时,Servlet 容器会先将文件存储在临时位置,然后再处理它。第二个参数表示最大文件大小(5*1024*1024)。第三个参数表示最大请求大小(7*1024*1024)。第四个参数表示何时应该开始将文件内容写入磁盘(0即立刻写入磁盘)。
图片上传工具类
package com.bxt.fm.util;
//文件上传的工具类
import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class UploadFile {
//校验所上传图片的大小及格式
public static int checkFormat(MultipartFile multipartFile){
//检查图片大小为5M
int MAX_SIZE = 5 * 1024 * 1024;
if (multipartFile.getSize() > MAX_SIZE){
return 207;
}
//检查上传的文件类型
Tika tika = new Tika();
//允许的文件类型列表
List<String> allowedMimeTypes = Arrays.asList("image/jpg", "image/jpeg", "image/png");
try {
InputStream inputStream = multipartFile.getInputStream();
//获取文件的MIME类型
String fileType = tika.detect(inputStream);
if (!allowedMimeTypes.contains(fileType)){
return 208;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return 200;
}
//将图片保存到服务器上,并返回url
public static String savePicture(MultipartFile multipartFile){
//获取文件的原始名称
String originalFilename = multipartFile.getOriginalFilename();
//获取文件的后缀
if (originalFilename != null) {
int index = originalFilename.lastIndexOf('.');
String suffix = originalFilename.substring(index);
//设置文件的新名字
String newPictureName = (UUID.randomUUID() + suffix).replaceAll("-","");
//设置文件保存的路径
String uploadDirection = "D:\\大学\\大四\\毕业设计\\项目代码\\趣集后端代码\\fun-market\\src\\main\\webapp\\uploadPicture\\";
//将文件保存到服务器中
try {
multipartFile.transferTo(new File(uploadDirection + newPictureName));
} catch (IOException e) {
throw new RuntimeException("文件保存失败: " + e);
}
return uploadDirection + newPictureName;
}
return null;
}
}
仅仅依靠依赖前端进行格式的校验是不够的,需要后端进行二次校验。MultipartFile可以获取文件的大小,但无法获取文件的类型,所以需要借助其他的工具。我这里使用的是Tika。
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
在savePicture()方法中,如果之间将图片保存到磁盘上,容易出现图片名重复,所以需要UUID对文件进行重命名。这个方法会返回完整的图片URL,包含整个路径和文件名。将这个String型数据存入数据库即可。
图片获取后端代码
package com.bxt.fm.controller;
import com.bxt.fm.pojo.CustomerInfo;
import com.bxt.fm.service.CustomerInfoService;
import com.bxt.fm.util.JwtHelper;
import com.bxt.fm.util.Result;
import com.bxt.fm.util.ResultCodeEnum;
import com.bxt.fm.util.UploadFile;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@CrossOrigin
@RestController
@RequestMapping("/customerInfo")
public class CustomerInfoController {
@Resource(name = "customerInfoService")
private CustomerInfoService customerInfoService;
//获取头像
@GetMapping("/getHeadPicture")
public Result<String> getHeadPicture(
@RequestHeader("token") String token,
HttpServletResponse response
) {
String signId = JwtHelper.getSignId(token);
if (signId != null) {
int customerId = Integer.parseInt(signId);
//获取头像的路径
String picturePath = customerInfoService.getHeadPicture(customerId);
if (picturePath == null || picturePath.equals("")) {
return Result.build(null, ResultCodeEnum.DEFAULT_PICTURE);
}
//将图片响应给客户端
try {
//根据路径获取图片
BufferedImage image = ImageIO.read(new File(picturePath));
ServletOutputStream outputStream = response.getOutputStream();
//获取图片的后缀
String fileType = picturePath.substring(picturePath.lastIndexOf(".") + 1);
ImageIO.write(image, fileType, outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Result.ok();
}
return Result.build(null, ResultCodeEnum.LOGIN_AUTH);
}
}
代码解释
首先从数据库中读取图片的地址picturePath,通过File(picturePath)可以获取图片文件,但是这种File文件是无法进行传输的,所以需要ImageIO.read(new File(picturePath))将其转化为BufferedImage,最后将其写入输出流中进行响应。
图片获取前端代码
<script lang="ts" setup>
import type { UploadProps } from 'element-plus'
import { error, successEasy, warning } from '../../../utils/notification'
import { customerInfo } from '../../../utils/pojo.js'
import { axiosInstance } from '../../../utils/request.js'
import { ref } from 'vue';
let picturePath = ref(customerInfo.portraitPath);
let passMuster = ref(0);
let pictureFile = ref();
//获取用户头像
let getHeadPicture = () => {
axiosInstance.get('/customerInfo/getHeadPicture',{responseType:'blob'})
.then((response) => {
customerInfo.portraitPath = URL.createObjectURL(new Blob([response.data]));
})
.catch((errors) => {
console.log(errors);
});
}
</script>
<template>
<div id="third-area">
<div id="upload-area">
<el-upload action=""
v-bind:show-file-list="false"
v-bind:before-upload="beforeUpload">
<el-button class="upload-button">上传头像</el-button>
<p id="upload-tip">支持大小不超过 5M 的 jpg、jpeg、png 图片</p>
</el-upload>
</div>
<div id="show-picture">
<img v-bind:src="picturePath">
</div>
<div id="picture-preview">
<p id="preview-title">效果预览</p>
<p id="preview-description">你上传的图片会自动生成以下尺寸, 请注意是否清晰。</p>
<img v-bind:src="picturePath" id="img-preview1">
<img v-bind:src="picturePath" id="img-preview2">
</div>
<button id="picture-submit" @click="uploadPicture()">保存</button>
</div>
</template>
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)