桥山之巅,姬水之畔

Vue SpringBoot实现Html和Markdown格式内容(含图片上传)保存到MySQL

2019.10.24

实现功能

  1. 本文代码实现了前端Markdown格式的博文保存到MySQL的功能。
  2. 包括文章中图片的上传,在用户选择图片后就将其传到后端并将图片的链接返回给前端,填入到指定的位置。

遇到的问题

  1. 由于Markdown编辑器原因,返回的图片路径不能有\与空格
  2. 如果遇到第二次进入编辑页面不能显示文章内容,那么在下方getArticle()方法中,处理响应的最后一行加入
    	// 解决第二次进入不能显示内容bug
    	this.$refs.md.d_value = response.data.markdownContent
    
  3. Html格式内容中的部分特殊符号会被JAVA替换掉,导致回显的页面样式有出入。建议不使用Html格式,使用Markdown格式

前端

安装依赖

  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