项目介绍

        前端使用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>

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐