Chris's Lab

If you obey all the rules,you miss all the fun.

0%

[教學] Vue element UI + Spring Boot 表單加圖片一起傳至後端

前言


在 Web 專案中多多少少都會遇到要把表單資料傳至後端儲存的功能,可是外加圖片檔案後就變得不那麼單純,這邊想分享是把表單資料加上圖片檔案,放在同一個物件內傳送到後端做保存。

問題敘述


把網頁中的彈跳視窗內的表單與圖片檔案,以 base64 格式一起傳至後端,前端以虛擬靜態路徑顯示圖片。

效果展示


upload-photos

邏輯思路


  1. 前端取得表單和圖片的 base64 格式,傳至後端。
  2. 後端取得表單內容和解析 base64 格式,轉換成圖片jpg 格式。
  3. 把圖片保存至本機,製作圖片的虛擬靜態路徑,保存在資料庫裡。

Vue

HTML


這裡是使用 element UI 的上傳檔案模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--
httpRequest:圖片覆蓋默認的上傳行為
beforeUpload:上傳前判斷圖片大小、類型
handleExceed:上傳超出數量提醒
handleChange:改變檔案樣子
:limit:上傳最大數量
-->
<el-form-item label="上傳照片" prop="img" label-width="100px">
<el-input v-model="editForm.avatar" v-if="false"></el-input>
<el-upload
class="upload-demo"
ref="upload"
action
accept="image/jpeg, image/png"
:http-request="httpRequest"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-change="handleChange"
:limit="1"
:data="editForm">
<el-button slot="trigger" size="small" type="primary">選取照片</el-button>
<div slot="tip" class="el-upload__tip">只能上傳 jepg、png 檔案,且不超過 5 MB</div>
</el-upload>
</el-form-item>

JavaScript


這裡是前端的邏輯處理,把圖片檔案轉成 Base64 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

data() {
return {
fileList: [], // 存放圖片檔案
};
},

methods: {

// 圖片覆蓋默認的上傳的行為,可以自定義上傳的實現,將上傳的檔案依次添加到 fileList 陣列中,支持多個檔案
httpRequest(option) {
this.fileList.push(option)
},

// 圖片上傳前處理
beforeUpload(file) {

let fileSize = file.size; // 用 size 屬性,判斷檔案大小不能超過 5MB
let fileName = file.name; // 用檔案名 name 後綴,判斷檔案類型
const FIVE_M = 5 * 1024 * 1024; // 5MB

// 大於 5MB 不允許上傳
if (fileSize > FIVE_M) {
this.$message.error("最大上傳 5 MB")
return false
}

// 允許的圖片類型 (正規表達式)
if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(fileName)) {
this.$message.error("只能上傳 jpg、png 格式!")
return false
}

return true
},

// 檔案數量過多時提醒
handleExceed() {
this.$message({
type: 'error',
message: '最多支持 1 個照片上傳'
})
},

// 把圖片轉換成 Base64
handleChange(file) {
// raw 是前端放置原始資料的類檔案物件 Blob (用於圖片、檔案)
// 有這個物件裡的東西, Js 才能讀寫二進位資料的檔案
this.getBase64(file.raw).then(res => {
this.editForm.avatar = res; // 轉好的資料放進物件裡
});
},

// 轉換邏輯
getBase64(file) {
return new Promise(function (resolve, reject) {

let imgResult = ""; // 裝 Base64 字串的容器
let reader = new FileReader(); // 讀取檔案方法
reader.readAsDataURL(file); // 讀取圖片後,編碼成 Base64 格式

// 將讀取的結果放到變數
reader.onload = function () {
imgResult = reader.result;
};

// 失敗傳錯誤訊息
reader.onerror = function (error) {
reject(error);
};

// 成功傳出去
reader.onloadend = function() {
resolve(imgResult);
};
});
},
},

Spring Boot

Entity


這裡繼承了一個我專案中的父類實體,與這個教學毫無關係,這裡想表達我的實體屬性是 avatar,其他屬性略。

1
2
3
4
@Data
public class ManageUser extends BaseEntity {
private String avatar; // 使用者圖示
}

Config


這邊很重要,瀏覽器是看不懂你本機電腦裡的圖片路徑的,要瀏覽器顯示你的圖片的話,一定要配置這個在 Spring 裡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration // 配置在 Spring 裡
public class MyWebConfigurer extends WebMvcConfigurationSupport {

@Override // 覆蓋重寫
public void addResourceHandlers(ResourceHandlerRegistry registry) {

/**
* 對檔案的路徑進行配置,建立一個靜態虛擬路徑 /api/file/**
* 即前端只要在<img src="/Path/picName.jpg" /> 便可以直接引用圖片
* 這是圖片的物理路徑 "file:/ + 本地圖片的實體地址"
* 這邊要注意,最後放圖片的資料夾後要加一個斜線,斜線後面才是你的圖片名稱
*/
registry.addResourceHandler("/api/file/**").addResourceLocations("file:" + "D:/xxx/src/main/resources/static/img/");

super.addResourceHandlers(registry);
}
}

Utils


工具類來處理前端傳來的物件,將 base64 格式轉換成 jpg 圖片格式,並保存圖片在本機,把虛擬靜態路徑回傳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@Slf4j // log
@Component // 注入到 spring 容器
public class ImgUtils {

// 檔案隨機取名(取得隨機 uuid)
public static String getUUID() {
// 把 uuid 裡的 "-" 換成 "" 空
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
return uuid;
}

// 取得 ManageUser 中的 Avatar 的 base64
public static String getAvatarBase64(String base64){

String imgUrl = ImgUtils.handleUploadPicture(base64); // 轉換邏輯後取得虛擬靜態 url
log.info("圖片url:" + imgUrl);

return imgUrl;
}

// 轉換邏輯
public static String handleUploadPicture(String base64){

// 虛擬靜態位置
String fileUrl = null;
// 最後重組的實體路徑
String lastFilePath;
// 實體資料夾目錄 (最後放圖片的資料夾後要加一個斜線,斜線後面才是你的圖片名稱)
String uploadFolder = "D:/xxx/src/main/resources/static/img/";
// 虛擬路徑位址
String apiUrl = "http://localhost:8081/api/file/";
// 副檔名
String suffix = null;

// 判斷前端來的附檔名為何
// .startsWith() 方法判斷字符串,是否從字符串 data:image/jpg 開始
if (base64.startsWith("data:image/jpg")) {
suffix = ".jpg";
} else if (base64.startsWith("data:image/jpeg")) {
suffix = ".jpeg";
} else if (base64.startsWith("data:image/png")) {
suffix = ".png";
}

// 新的圖片檔名
String newFileName = ImgUtils.getUUID() + suffix; // uuid 編碼 + 後戳 .jpg

// 判斷有無實體資料夾
File folder = new File(uploadFolder);
if (!folder.exists()) {
folder.mkdirs(); // 創建資料夾
}

// 檔案 IO 流:該類用來創建一個檔案並向檔案中寫資料
FileOutputStream out = null;
final BASE64Decoder decoder = new BASE64Decoder(); // base64 解碼方法

try {
lastFilePath = uploadFolder + File.separator + newFileName; // 實體位置 + 系統目錄中的間隔符 + 新的圖片檔名
out = new FileOutputStream(lastFilePath); // 寫到檔案 IO 流裡
byte[] decoderBytes = decoder.decodeBuffer(base64.split(",")[1]); // 前端 base64 資料解碼
out.write(decoderBytes); // 寫進檔案 IO 流裡
fileUrl = apiUrl + newFileName; // 虛擬靜態路徑 + 新的圖片檔名
log.info("虛擬靜態路徑:" + fileUrl);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.flush(); // 將位元組強制寫入
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close(); // 關掉流
} catch (IOException e) {
e.printStackTrace();
}
}
}

return fileUrl; // 回傳虛擬靜態位置
}
}

Service


Spring Boot 業務的介面類 (由 Controller 來引用)

1
2
3
4
5
6
public interface ManageUserService extends IService<ManageUser> {

// 新增使用者圖示
ManageUser UserAvatarInfoSave(ManageUser manageUser);

}

ServiceImpl


Spring Boot 業務的實現類 (由業務的介面類來管理)
這裡主要的工作是 Controller 收到前端的 manageUser 物件了,現在要把 manageUser 物件裡的 Avatar (Base64格式),拿出來給上面寫好的工具類處理,解析轉換完後把他丟回 manageUser 物件裡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j // log
@Service // 注入到 spring 容器 (標記為 Service 層的 class)
public class ManageUserServiceImpl extends ServiceImpl<ManageUserMapper, ManageUser> implements ManageUserService {

// 處理 Avatar 工具類
@Autowired
ImgUtils imgUtils;

// 處理 Avatar
@Override // 覆蓋重寫
public ManageUser UserAvatarInfoSave(ManageUser manageUser) {

// 取得前端傳來的 Avatar 的 base64
String avatar = manageUser.getAvatar();

// 取得工具類處理好的 url
String imgUrl = imgUtils.getAvatarBase64(avatar);

// 添加到 manageUser 物件裡
manageUser.setAvatar(imgUrl);

return manageUser;
}
}

Controller


Controller 接收到前端傳來的資料物件後,使用業務的介面類,來引用業務的實現類,去調用工具類處理 Avatar,處理完在把整個 manageUser 物件存進資料庫裡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController // 是 @ResponseBody 和 @Controller 的組合註解,處理 http 請求
@RequestMapping("/manage/user")
public class ManageUserController extends BaseController {

// 新增(創建新的用戶)
// @Validated 驗證實體參數、@RequestBody 因放在 body 裡,是使用 json 格式,但只能用 POST 傳遞物件
@PostMapping("/save")
public Result save(@Validated @RequestBody ManageUser manageUser){

// 新增使用者圖示
manageUserService.UserAvatarInfoSave(manageUser);

// 使用 MyBatis Plus 的 .save() 函數新增至資料庫(insert)
manageUserService.save(manageUser);

return Result.succ(manageUser); // 回傳給前端 (統一結果類 Result)
}
}

結語


這邊盡可能提供詳細的方法給各位參考,筆者也還在努力學習中,寫的並不是非常好,可能有哪些地方邏輯可以再加強或者再優化,也不吝嗇被指教,感謝各位的觀看。