Vue SpringBoot實現Html和Markdown格式內容(含圖片上傳)儲存到MySQL

松林羊發表於2019-10-24

實現功能

  1. 本文程式碼實現了前端Html、Markdown格式的博文儲存到MySQL的功能。
  2. 包括文章中圖片的上傳,在使用者選擇圖片後就將其傳到後端並將圖片的連結返回給前端,填入到指定的位置。

遇到的問題

  1. 由於Markdown編輯器原因,返回的圖片路徑不能有\與空格
  2. 如果遇到第二次進入編輯頁面不能顯示文章內容,那麼在下方getArticle()方法中,處理響應的最後一行加入
    // 解決第二次進入不能顯示內容bug
    this.$refs.md.d_value = response.data.markdownContent
    
  3. Html格式內容中的部分特殊符號會被JAVA替換掉,導致回顯的頁面樣式有出入。建議不使用Html格式,使用Markdown格式

前端

前端是用Vue-cli搭建的專案工程,執行在WebStorm中。

安裝依賴

  1. 資料請求相關

    npm install axios --save
    npm install qs --save
    
  2. 安裝Markdown編輯器

      npm install mavon-editor --save
    

    使用mavon-editor,請自行參考如何使用。目前不支援流程圖、序列圖、甘特圖

  3. 其他依賴

    npm install style-loader
    npm install css-loader
    npm install sass-loader
    npm install babel-loader --save
    

在main.js中引入mavonEditor

import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
Vue.use(mavonEditor)

ArticleMarkdown.vue元件,實現Markdown博文的存取

程式碼導讀

getArticle():通過文章id獲取內容
saveArticle():提交博文內容到後端。同時提交了html、markdown格式的內容,見

var htmlCode = this.$refs.md.d_render; 
var markdownCode = this.$refs.md.d_value;

imgAdd(pos, file):上傳單張圖片。file圖片物件,pos圖片下標,後端返回圖片連結地址時,用於定位
imgDel(pos):刪除圖片
mulUploadimg() :上傳多張圖片。圖片物件存放在data中img_file物件中
imgDelMul(pos):刪除多張圖片。

原始碼
<template>
  <div>
    <mavon-editor ref="md" class="md" v-model="sqlData.markdown" @imgAdd="imgAdd" @imgDel="imgDel" @save="saveArticle"/>
  </div>
</template>

<script>
  import axios from 'axios'
  import qs from 'qs'
  const area_axios = axios.create({
    headers: {'Content-Type': 'application/json;charset=utf-8',},// 設定傳輸內容的型別和編碼
    withCredentials: true,// 指定某個請求應該傳送憑據
  })
  const file_axios = axios.create({
    headers: {'Content-Type': 'multipart/form-data',},// 設定傳輸內容的型別和編碼
    withCredentials: true,// 指定某個請求應該傳送憑據
  })
  const area_form_axios = axios.create({
    headers: {'Content-Type': 'application/x-www-form-urlencoded',},// 設定傳輸內容的型別和編碼
    withCredentials: true,// 指定某個請求應該傳送憑據
  })
  export default {
    name: "Markdown",
    data() {
      return {
        sqlData:{
          markdown:'',
          html:''
        },
        img_file: {},// 一次上次多張圖片時使用
      };
    },
    mounted:function (){
        getArticle()
    },
    methods: {
    	// 獲取文章
		getArticle(){
			area_form_axios.get('/api/get',{
	            params:{id: 12 }
	          },)
	        .then(response => {
	          console.log(this.sqlData)
	          this.sqlData = response.data
	        })
	        .catch(err => {
	          alert("請求失敗")
	        })
		},
      // 儲存文章
      saveArticle(){
        var htmlCode = this.$refs.md.d_render;
        var markdownCode = this.$refs.md.d_value;
        if(htmlCode.length == 0 || markdownCode.length == 0){
          alert("請填寫")
          return;
        }
        area_axios({
          url: '/api/add',
          method: 'post',
          data: JSON.stringify({'markdown':markdownCode,'html':htmlCode}),
        }).then((response) => {
          if(response.data > 0){
            alert("成功")
          }else {
            alert("失敗")
          }
        })
      },
      // 新增圖片
      imgAdd(pos, file){
        console.log("pos:"+pos)
        // 第一步.將圖片上傳到伺服器.
        var formdata = new FormData();
        formdata.append('pic', file);
        file_axios({
          url: '/api/img_upload',
          method: 'post',
          data: formdata,
        }).then((response) => {
          // 第二步.將返回的url替換到文字原位置
          var url = response.data;
          //通過引入物件獲取: import {mavonEditor} from ... 等方式引入後,此時$vm即為mavonEditor
          //通過$refs獲取: html宣告ref : <mavon-editor ref=md ></mavon-editor>, 此時$vm為 this.$refs.md`
          this.$refs.md.$img2Url(pos, url);
        })
      },
      // 刪除圖片
      imgDel(pos){
        console.log("imgDel pos:"+pos)
      }, 
      // 多張圖片
      mulUploadimg(){
        // 第一步.將圖片上傳到伺服器.
        var formdata = new FormData();
        for(var _img in this.img_file){
          debugger
          // 後臺需要圖片的key一致
          formdata.append('pics', this.img_file[_img]);
        }
        file_axios({
          url: '/api/mul_img_upload',
          method: 'post',
          data: formdata,
        }).then((res) => {
          /**
           * 例如:返回資料為 res = [[pos, url], [pos, url]...]
           * pos 為原圖片標誌(0)
           * url 為上傳後圖片的url地址
           */
            // 第二步.將返回的url替換到文字原位置![...](0) -> ![...](url)
          var  idx_url = res.data;
          idx_url.forEach(item => {
            //通過引入物件獲取: import {mavonEditor} from ... 等方式引入後,此時$vm即為mavonEditor
            //通過$refs獲取: html宣告ref : <mavon-editor ref=md ></mavon-editor>, 此時$vm為 this.$refs.md`
            this.$refs.md.$img2Url(item[0], item[1]);
          });
        })
      },
      // 多張圖片
      imgDelMul(pos){
        console.log("imgDel pos:"+pos)
        delete this.img_file[pos];
      },
    }
  }
</script>

跨域配置

vue-axios 前後端分離 跨域訪問的實現

後端

檔案上傳相關配置

application.properties檔案中

spring.servlet.multipart.enabled=true
# 最大支援檔案大小
spring.servlet.multipart.max-file-size=10MB
# 最大支援請求大小
spring.servlet.multipart.max-request-size=50MB

配置攔截器

@Component
public class CrossDomainInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 允許客戶端攜帶跨域cookie,此時origin值不能為“*”,只能為指定單一域名。!!開發時不要使用localhost訪問
        response.setHeader("Access-Control-Allow-Credentials", "true");
        // 允許指定域訪問跨域資源
        //response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:9006, http://127.0.0.1:8080");
        response.setHeader("Access-Control-Allow-Origin", origin);// *
        // 允許瀏覽器傳送的請求訊息頭
        //response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
        // 允許瀏覽器在預檢請求成功之後傳送的實際請求方法名
        //response.setHeader("Access-Control-Allow-Methods", "DEFAULT,POST,PATCH,PUT,OPTIONS,DELETE,HEAD");
        response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
        // 瀏覽器快取預檢請求結果時間,單位:秒
        response.setHeader("Access-Control-Max-Age", "86400");
        return true;
    }
}

資料介面

@Controller
@RequestMapping("")
public class MarkdownController {
	/**
	     * 獲取文章
	     * id: 文章id
	     * @author YSL
	     * 2019-03-04 15:38
	     */
	    @GetMapping("/get")
	    @ResponseBody
	    public Bean test(@RequestParam("id")Integer id){
	        // 獲取資料庫中的資料,請自行實現。
	        return vueMarkdownMapper.query(id); 
	    }
	
	    /**
	     * 儲存文章到資料庫。
	     * bean:前端傳回JSON.stringify({'markdown':markdownCode,'html':htmlCode})格式的資料即可
	     * @author YSL
	     * 2019-03-04 15:26
	     */
	    @PostMapping("/add")
	    @ResponseBody
	    public int test(@RequestBody Bean bean){
	        return vueMarkdownMapper.add(bean);// 儲存資料到資料庫,請自行實現
	    }
    }

圖片上傳

程式碼導讀

@RequestMapping("/img_upload"):單張圖片上傳,上傳到blog_files/pictures目錄下,返回圖片url。
@RequestMapping("/mul_img_upload"):多張圖片上傳。上傳到blog_files/pictures目錄下,返回new String[]{圖片下標, 圖片url}格式的list。
fileUpload():檔案上傳。上傳到blog_files/files目錄下。
upload():真正實現檔案上傳的方法,基於MultipartFile實現

說明

  1. 圖片與檔案都是上傳到tomcat/webapps/blog_files/目錄下,blog_files是我專門用來儲存圖片的一個web工程,方便通過http訪問到圖片,給前端返回的圖片地址也是http格式的。
  2. 在圖片和檔案上傳的同時會備份,案例總備份路徑:D:/webserver_bak/blog/
原始碼
public class FileController {
	/**
	     * 圖片上傳(一張)
	     * @param pic 需要上傳的圖片
	     * @return 圖片url
	     * @author YSL
	     * 2019-03-01 17:14
	     */
	    @RequestMapping("/img_upload")
	    @ResponseBody
	    public String imgUpload(@RequestParam(value = "pic", required = false) MultipartFile pic, HttpServletRequest request){
	
	        List<String> urlList = upload(new MultipartFile[]{pic}, "pictures", request);
	
	        return urlList != null ? urlList.get(0) : "";
	    }
	
	    /**
	     * 圖片上傳(多張)
	     * @param pics 需要上傳的圖片
	     * @return 圖片下標和url
	     * @author YSL
	     * 2019-03-01 17:14
	     */
	    @RequestMapping("/mul_img_upload")
	    @ResponseBody
	    public List<String[]> imgUpload(@RequestParam(value = "pics", required = false) MultipartFile[] pics, HttpServletRequest request){
	
	        List<String> urlList = upload(pics, "pictures", request);
	
	        List<String[]>  list = new ArrayList<>();
	        for (int i = 0; i < urlList.size() ; i++) {
	            String[] idx_url = new String[2];
	
	            // 圖片下標
	            idx_url[0]=i+"";
	            // 拼接url
	            idx_url[1] = urlList.get(i);
	
	            list.add(idx_url);
	        }
	
	        return list;
	    }
	
	    /**
	     * 檔案上傳
	     * @param files 需要上傳的檔案
	     * @return 檔案url
	     * @author YSL
	     * 2019-03-01 17:14
	     */
	    @RequestMapping("/file_upload")
	    @ResponseBody
	    public List<String> fileUpload(@RequestParam(value = "files", required = false) MultipartFile[] files, HttpServletRequest request){
	        List<String> urlList = upload(files, "pictures", request);
	        return urlList;
	    }
	
	    /**
	     * 檔案/圖片上傳。並做備份<br/>
	     * 路徑不能有反斜線和空格 <br/>
	     * 上傳路徑:.../webapps/blog_files/pictures/20190301/圖片 <br/>
	     * 上傳路徑:.../webapps/blog_files/files/20190301/檔案 <br/>
	     * 備份路徑:.../webserver_bak/blog/pictures/20190301/圖片 <br/>
	     * 備份路徑:.../webserver_bak/blog/files/20190301/檔案
	     * @param files 需要上傳的檔案
	     * @param categoryPath 類別路徑,pictures/files
	     * @return 上傳成功,返回檔案url
	     * @author YSL
	     * 2019-03-01 16:45
	     */
	    public List<String> upload(MultipartFile[] files, String categoryPath, HttpServletRequest request){
	
	        // 非空判定
	        if(files == null || files.length == 0){
	            return new ArrayList<>();
	        }
	
	        // 專門存放檔案工程名稱(是一個javaweb工程,方便圖片直接通過http訪問)
	        String fileProject = "blog_files";
	        // 備份路徑
	        String bakPath = "D:/webserver_bak/blog/";
	        //http://localhost:7989/
	        String ipPort = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort() + "/";
	
	        /**
	         * 獲取專案絕對路徑,格式,D:\tomcats\apache-tomcat-8.0.52\webapps\boot\。
	         * markdown編輯器圖片路徑不能有\,所以替換為/
	         * 注意:.replace("//", "/"); 與 replaceAll("\\\\", "/");
	         */
	        String rootPath = request.getSession().getServletContext().getRealPath("").replaceAll("\\\\", "/");
	
	        // 專案路徑。/boot
	        String contextPath = request.getContextPath();
	        rootPath = rootPath.substring(0, rootPath.lastIndexOf(contextPath.replace("/","")));
	
	        StringBuilder fileRoot = new StringBuilder("");
	        // 工程名稱
	        fileRoot.append(fileProject);
	        fileRoot.append("/");
	        // 類別目錄
	        fileRoot.append(categoryPath);
	        fileRoot.append("/");
	        // 檔案目錄,圖片上傳失敗時使用
	        String picRootPath = fileRoot.toString();
	        String day = new SimpleDateFormat("yyyyMMdd").format(new Date());
	        // 日期目錄
	        fileRoot.append(day);
	        fileRoot.append("/");
	
	        // 檔案最終儲存目錄
	        String fileDir = fileRoot.toString();
	
	        List<String>  list = new ArrayList<>();
	        for (MultipartFile  multipartFile : files) {
	
	            // 檔名稱。markdown編輯器圖片路徑不能有空格
	            String upFileName = multipartFile.getOriginalFilename().replaceAll("\\s+", "");
	            String filename = new SimpleDateFormat("HHmmss").format(new Date()) + "_" + UUID.randomUUID().toString() + "_" + upFileName;
	
	            String filePathName = rootPath + fileDir + filename;
	            File destFile = new File(filePathName);
	            try {
	                // 複製臨時檔案到指定目錄下, 會建立沒有的目錄
	                FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), destFile);
	
	                // 拼接url
	                list.add(ipPort + fileDir + filename);
	
	                // 備份
	                File bakFile = new File(bakPath + fileDir + filename);
	                FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), bakFile);
	            } catch (UnsupportedEncodingException e2) {
	                e2.printStackTrace();
	                if("pictures".equals(categoryPath)){
	                    // 預設圖片
	                    list.add(picRootPath+"default.jpg");
	                }else{
	                    list.add("");
	                }
	            } catch (IOException e) {
	                e.printStackTrace();
	                if("pictures".equals(categoryPath)){
	                    // 預設圖片
	                    list.add(picRootPath+"default.jpg");
	                }else{
	                    list.add("");
	                }
	            }
	        }
	
	        return list;
	    }
    }

資料庫

欄位名型別長度備註
idint預設文章id
markdowntextmarkdown格式內容
htmltexthtml格式內容

參考
https://blog.csdn.net/qq_32407233/article/details/84656914
https://blog.csdn.net/wangjun5159/article/details/48809427
https://segmentfault.com/q/1010000016563395

相關文章