springboot中使用kindeditor富文本编辑器实现博客功能&vue-elementui使用vue-kindeditor
kindeditor官网:http://kindeditor.net/demo.php
kindeditor在之前已经用过,现在在springboot项目中使用。并且也在里面使用了图片上传以及回显等功能。
其实主要的功能是图片的处理:kindeditor对输入的内容会作为html标签处理,对于image的做法是先将图片传到后台服务器,然后上传成功之后回传图片的URL,之后内容中增加<img src='url'>进行回显,当然保存到数据库也是img标签进行保存的。对于文件上传是以a标签的形式进行回显,数据库也以a标签进行保存。
下面的代码涉及到了:Restful风格的请求、SpringMVC文件的上传、不配置虚拟路径的前提下请求图片资源、kindeditor、thymeleaf模板的使用。
1.首先编写接收kindeditor图片上传和图片请求的类:
package cn.qs.controller.common; import java.io.File; import java.io.FileInputStream; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import cn.qs.bean.common.Document; import cn.qs.service.common.DocumentService; import cn.qs.utils.UUIDUtils; import cn.qs.utils.file.FileHandleUtil; import cn.qs.utils.file.TikaUtils; import cn.qs.utils.system.MySystemUtils; @Controller @RequestMapping("document") public class DocumentController { private static final Logger logger = LoggerFactory.getLogger(DocumentController.class); @Autowired private DocumentService documentService; /** * Restful风格获取文档 * * @param request * @param response * @param documentId */ @RequestMapping("/getDocument/{documentId}") public void getPicture(HttpServletRequest request, HttpServletResponse response, @PathVariable() String documentId) { FileInputStream in = null; ServletOutputStream outputStream = null; try { Document document = documentService.getById(documentId); String path = document.getPath(); String originName = document.getOriginName(); File fileByName = FileHandleUtil.getFileByName(path); // 判断文件类型,image、pdf返回阅读,其他下载 String fileType = TikaUtils.getFileType(fileByName); if (!TikaUtils.TYPE_IMAGE.equals(fileType) && !TikaUtils.TYPE_PDF.equals(fileType)) { response.setContentType("application/force-download"); response.setHeader("Content-Disposition", "attachment;fileName=" + originName); } in = new FileInputStream(fileByName); outputStream = response.getOutputStream(); IOUtils.copyLarge(in, outputStream); } catch (Exception e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(outputStream); } } /** * 文档上传 * * @param imgFile * @return */ @RequestMapping("/upload") @ResponseBody public Map<String, Object> uploadPicture(MultipartFile imgFile) { Map<String, Object> result = new HashMap<String, Object>(); result.put("error", 1); if (imgFile == null) { result.put("message", "文件没接到"); return result; } logger.debug("file -> {},viewId ->{}", imgFile.getOriginalFilename()); String fileOriName = imgFile.getOriginalFilename();// 获取原名称 String fileNowName = UUIDUtils.getUUID2() + "." + FilenameUtils.getExtension(fileOriName);// 生成唯一的名字 try { FileHandleUtil.uploadSpringMVCFile(imgFile, fileNowName); } catch (Exception e) { logger.error("uploadPicture error", e); return result; } String id = UUIDUtils.getUUID(); Document document = new Document(); document.setCreatetime(new Date()); document.setPath(fileNowName); document.setId(id); document.setOriginName(fileOriName); document.setUploaderUsername(MySystemUtils.getLoginUsername()); documentService.insert(document); // 回传JSON结果 result.put("error", 0); result.put("url", "/document/getDocument/" + id); return result; } }
图片上传:参数接受名字必须是imgFile,否则接收不到文件。收到文件之后先生成一个全局唯一的名称然后保存到本地,并保存到数据库之后返回一个图片的URL例如: /document/getDocument/94995b51-901c-44e9-87ec-12c109098f5e
图片获取:通过restful风格的请求将图片的ID传到后台,根据ID查询到图片的路径,然后调用IOUtils将文件流回传回去实现图片的src请求显示。如果是其他类型的文件以下载的方式进行回传流。
Document实体类如下:
package cn.qs.bean.common; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; //系统文档表 @Entity public class Document { @Id private String id; /** * 原名字 */ private String originName; /** * 上传者 */ private String uploaderUsername; private String name; private String path; private Date createtime; private String remark1; private String remark2; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name == null ? null : name.trim(); } public String getPath() { return path; } public void setPath(String path) { this.path = path == null ? null : path.trim(); } public Date getCreatetime() { return createtime; } public void setCreatetime(Date createtime) { this.createtime = createtime; } public String getRemark1() { return remark1; } public void setRemark1(String remark1) { this.remark1 = remark1; } public String getRemark2() { return remark2; } public void setRemark2(String remark2) { this.remark2 = remark2; } public String getOriginName() { return originName; } public void setOriginName(String originName) { this.originName = originName; } public String getUploaderUsername() { return uploaderUsername; } public void setUploaderUsername(String uploaderUsername) { this.uploaderUsername = uploaderUsername; } }
FileHandleUtil类是图片保存以及获取,保存到本地的固定文件夹下面。
package cn.qs.utils; import java.io.File; import java.util.Locale; import java.util.ResourceBundle; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.web.multipart.MultipartFile; public class FileHandleUtil { public static final String LANGUAGE = "zh"; public static final String COUNTRY = "CN"; private static String getProperties(String baseName, String section) { String retValue = ""; try { Locale locale = new Locale(LANGUAGE, COUNTRY); ResourceBundle rb = ResourceBundle.getBundle(baseName, locale); retValue = (String) rb.getObject(section); } catch (Exception e) { } return retValue; } public static String getValue(String fileName, String key) { String value = getProperties(fileName, key); return value; } public static boolean deletePlainFile(String propertiesFileName, String fileName) { if (fileName == null) { return false; } String fileDir = StringUtils.defaultIfBlank(FileHandleUtil.getValue("path", "picture"), "E:/picture/"); try { FileUtils.deleteQuietly(new File(fileDir + fileName)); } catch (Exception e) { return false; } return true; } public static boolean uploadSpringMVCFile(MultipartFile multipartFile, String fileName) throws Exception { String fileDir = StringUtils.defaultIfBlank(FileHandleUtil.getValue("path", "picture"), "E:/picture/"); if (!new File(fileDir).exists()) { new File(fileDir).mkdirs(); } multipartFile.transferTo(new File(fileDir + fileName));// 保存文件 return true; } public static File getFileByName(String path) { String fileDir = StringUtils.defaultIfBlank(FileHandleUtil.getValue("path", "picture"), "E:/picture/"); return new File(fileDir+path); } }
TikaUtils是提取文件类型的工具类,依赖的jar包如下:
<!--tika解析文本内容 --> <dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-parsers</artifactId> <version>1.17</version> </dependency>
工具类源码如下:
package cn.qs.utils.file; import java.io.File; import java.io.IOException; import org.apache.commons.lang3.StringUtils; import org.apache.tika.Tika; public class TikaUtils { // 总的文件类型分为下面几类 public static final String TYPE_OFFICE = "OFFICE"; public static final String TYPE_PDF = "PDF"; public static final String TYPE_IMAGE = "IMAGE"; public static final String TYPE_VIDEO = "VIDEO"; public static final String TYPE_OTHER = "OTHER"; // tika解析的文件信息 private static final String WORD_DOC = "application/msword"; private static final String WORD_PPT = "application/vnd.ms-powerpoint"; private static final String WORD_EXCEL = "application/vnd.ms-excel"; private static final String WORD_DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; private static final String WORD_PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; private static final String WORD_EXCELX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; private static final String VIDEO = "video/"; private static final String IMAGE = "image/"; private static final String TEXT = "text/plain"; private static final String CSS = "text/css"; private static final String HTML = "text/html"; private static final String PDF = "application/pdf"; private static final String ZIP = "application/zip"; private static final String RAR = "application/x-rar-compressed"; public static String getFileType(File file) { if (file == null || !file.exists()) { return ""; } Tika tika = new Tika(); String filetype = null; try { filetype = tika.detect(file); } catch (IOException ignore) { // ignore return "error"; } if (StringUtils.isBlank(filetype)) { return "error"; } if (WORD_DOC.equals(filetype) || WORD_PPT.equals(filetype) || WORD_EXCEL.equals(filetype) || WORD_DOCX.equals(filetype) || WORD_PPTX.equals(filetype) || WORD_EXCELX.equals(filetype)) { return TYPE_OFFICE; } if (filetype.startsWith(VIDEO)) { return TYPE_VIDEO; } if (filetype.startsWith(IMAGE)) { return TYPE_IMAGE; } if (filetype.equals(PDF)) { return TYPE_PDF; } return TYPE_OTHER; } }
2.前台界面准备富文本编辑器并且保存输入的信息到数据库
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>欢迎页面-X-admin2.0</title> <meta name="renderer" content="webkit"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta name="viewport" content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" /> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/x-admin/css/font.css'}"/> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/x-admin/css/xadmin.css'}"/> <script type="text/javascript" th:src="${#httpServletRequest.getContextPath()+'/static/js/jquery.min.js'}" charset="utf-8"></script> <script type="text/javascript" th:src="${#httpServletRequest.getContextPath()+'/static/x-admin/lib/layui/layui.js'}" charset="utf-8"></script> <script type="text/javascript" th:src="${#httpServletRequest.getContextPath()+'/static/x-admin/js/xadmin.js'}"></script> <!-- kindeditor相关 --> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/kindeditor/themes/default/default.css'}" /> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/kindeditor/plugins/code/prettify.css'}" /> <script charset="utf-8" th:src="${#httpServletRequest.getContextPath()+'/static/kindeditor/kindeditor-all.js'}"></script> <script charset="utf-8" th:src="${#httpServletRequest.getContextPath()+'/static/kindeditor/lang/zh-CN.js'}"></script> <script> var editor; KindEditor.ready(function(K) { editor = K.create('textarea[name="contentEditor"]', { resizeType : 1, allowPreviewEmoticons : false, uploadJson : '/document/upload.html', allowImageUpload : true, pasteType : 0, //设置能否粘贴 items : [ 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', 'italic', 'underline', 'removeformat', '|', 'justifyleft', 'justifycenter', 'justifyright', 'insertorderedlist', 'insertunorderedlist', '|', 'emoticons', 'image', 'link','fullscreen'] }); }); </script> <!-- 让IE8/9支持媒体查询,从而兼容栅格 --> <!--[if lt IE 9]> <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script> <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script> <![endif]--> </head> <body> <div class="x-body layui-anim layui-anim-up"> <form class="layui-form"> <div class="layui-form-item"> <label for="L_email" class="layui-form-label"> <span class="x-red">*</span>博客标题 </label> <div class="layui-input-inline"> <input type="text" id="L_blogtitle" name="blogtitle" lay-verify="required" autocomplete="off" class="layui-input"/> </div> </div> <div class="layui-form-item"> <label for="L_email" class="layui-form-label"> <span class="x-red">*</span>内容 </label> <div class="layui-input-inline"> <textarea name="contentEditor" id="test" cols="100" rows="8" style="width:700px;height:200px;visibility:hidden;"></textarea> </div> </div> <div class="layui-form-item"> <label for="L_repass" class="layui-form-label"> </label> <button class="layui-btn" lay-filter="add" lay-submit=""> 增加 </button> </div> </form> </div> </body> <script> /*<![CDATA[*/ layui.use(['form','layer'], function(){ $ = layui.jquery; var form = layui.form ,layer = layui.layer; //监听提交 form.on('submit(add)', function(data){ var data = { "blogtitle":$('[name="blogtitle"]').val(), "content":editor.html() } //异步提交数据 $.post("/blog/doAddBlog.html",data,function(response){ if(response.success == true){ layer.msg("增加成功", {icon: 6},function () { // 获得frame索引 var index = parent.layer.getFrameIndex(window.name); //关闭当前frame parent.layer.close(index); // 父页面刷新 parent.location.reload(); }); }else{ layer.alert(response.msg); } }); return false; }); }); /*]]>*/ </script> </html>
界面如下:
最终生成的内容保存到数据库之后如下:
<p> 测试 </p> <p> <img src="http://localhost:8088/static/kindeditor/plugins/emoticons/images/20.gif" border="0" alt="" /> </p> <p> <img src="/document/getDocument/efe5134f-8a41-4e75-9108-6ab045979db6" alt="" /> </p>
最终经过代码处理后在界面显示如下:
@RequestMapping("/getBlogdetail/{blogId}") public String getBlogdetail(ModelMap map, @PathVariable() Integer blogId, HttpServletRequest request) { Blog blog = blogService.getBlogdetail(blogId); // 获取当前用户 HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); String username = user.getUsername(); String blogUsername = StringUtils.defaultIfBlank(blog.getBlogblank(), "admin"); if (blogUsername.equals(username)) { map.put("blog", blog); } else { map.put("blog", new Blog()); } return "blogDetail"; }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title th:text="${blog.blogtitle}"></title> <meta name="renderer" content="webkit"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta name="viewport" content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" /> <link rel="shortcut icon" href="/static/x-admin/favicon.ico" type="image/x-icon" /> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/x-admin/css/font.css'}"/> <link rel="stylesheet" th:href="${#httpServletRequest.getContextPath()+'/static/x-admin/css/xadmin.css'}"/> <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script> <script type="text/javascript" th:src="${#httpServletRequest.getContextPath()+'/static/x-admin/lib/layui/layui.all.js'}" charset="utf-8"></script> <script type="text/javascript" th:src="${#httpServletRequest.getContextPath()+'/static/x-admin/js/xadmin.js'}"></script> <span th:if="${session.user.username} eq 'admin'"> <script> var admin = true; </script> </span> <style type="text/css"> body{ text-align:center; background-color: #dcdcdc; } #container{ text-align:left; margin:0 auto; width: 80%; background-color: #fff; } </style> </head> <body class="layui-anim layui-anim-up"> <div id="container"> <center> <h1 th:text="${'标题:'+blog.blogtitle}"></h1> <h2 th:text="${'所属人'+blog.blogblank}"></h2> </center> <hr/> <span th:utext="${blog.content}"></span> </div> </body> </html>
补充:kindeditor可选的显示插件有好多,如下需要的时候items里面增加对应的选项即可
items : [
'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
'anchor', 'link', 'unlink', '|', 'about'
]
源码git地址:https://github.com/qiao-zhi/bs-tourism2.git
3.前后端分离项目vue-elementui中使用kindeditor
git地址: https://github.com/ff755/vue-kindeditor
大致安装步骤如下:
0.设置淘宝镜像:
npm config set registry https://registry.npm.taobao.org npm config get registry
(如果上面配置正确,会显示https://registry.npm.taobao.org )
1. 添加vue-kindedtior
在终端项目目录下执行
npm install vue-kindeditor --save-dev
2.使用vue-kindedtior
第一步,编辑 demo/src/main.js 文件
import Vue from 'vue' import App from './App' // 引入 vue-kikindeditor 需要的文件 import VueKindEditor from 'vue-kindeditor' import 'kindeditor/kindeditor-all-min.js' import 'kindeditor/themes/default/default.css' Vue.config.productionTip = false // 注册 vue-kikindeditor plugin Vue.use(VueKindEditor) /* eslint-disable no-new */ new Vue({ el: '#app', template: '<App/>', components: { App } })
编辑完配置文件后,就可以在组件中使用了。
第二步,编辑 demo/src/components/Hello.vue 文件
在 Hello.vue 中添加如下代码
<template> <div class="hello"> <editor id="editor_id" height="500px" width="700px" :content="editorText" pluginsPath="/static/kindeditor/plugins/" :loadStyleMode="false" @on-content-change="onContentChange"></editor> </div> </template> <script> export default { name: 'hello', data () { return { editorText: '' } }, methods: { onContentChange (val) { this.editorText = val } } } </script>
返回浏览器就可以看到编辑器了。
使用 editor 标签使用kindeditor。<editor ...></editor> 参数: id: 设置编辑器的id,kindeditor创建使用需要用到,所以必填。例子:editor_id content: 用来获取编辑器内容,使用双向绑定获取编辑器内容数据。例子:editorText loadStyleMode: 是否自动加载kindeditor需要的css文件,默认是从 / 查找,所以设置为false,搭配pluginsPath使用 pluginsPath: 设置编辑器的plugins目录。例子中为了方便把kindeditor全部目录复制到了demo/static中了,实际使用中地址自行设置。
3.关于图片上传:(使用该编辑器我主要是使用图片上传)
vue.config.js使用的是proxy,代理如下:
proxy: { '/api': { target: 'http://localhost:8088', ws: true, changeOrigin: true, pathRewrite: { '^/api': '' } } }
后台代码只需要修改返回的地址加上/api 以供前端代理请求,如下:
/** * 文档上传 * * @param imgFile * @return */ @RequestMapping("/upload") @ResponseBody public Map<String, Object> uploadPicture(MultipartFile imgFile) { Map<String, Object> result = new HashMap<String, Object>(); result.put("error", 1); if (imgFile == null) { result.put("message", "文件没接到"); return result; } logger.debug("file -> {},viewId ->{}", imgFile.getOriginalFilename()); String fileOriName = imgFile.getOriginalFilename();// 获取原名称 String fileNowName = UUIDUtils.getUUID2() + "." + FilenameUtils.getExtension(fileOriName);// 生成唯一的名字 try { FileHandleUtil.uploadSpringMVCFile(imgFile, fileNowName); } catch (Exception e) { logger.error("uploadPicture error", e); return result; } String id = UUIDUtils.getUUID(); Document document = new Document(); document.setCreatetime(new Date()); document.setPath(fileNowName); document.setId(id); document.setOriginName(fileOriName); document.setUploaderUsername(MySystemUtils.getLoginUsername()); documentService.insert(document); // 回传JSON结果 result.put("error", 0); result.put("url", "/api/document/getDocument/" + id); return result; }
editor修改为如下:
<editor id="editor_id" height="200px" width="200px" :content="addForm.content" pluginsPath="/static/kindeditor/plugins/" :loadStyleMode="false" uploadJson="/api/blogPicture/uploadPicture.html" filePostName="imgFile" @on-content-change="onContentChange"></editor> </el-form-item>
4.实现文件上传
kindeditor实现更多的插件,文件上传和图片上传也可以单独的使用,不在textarea里面使用。
有时候需要实现文件上传,文件上传和图片上传的思路是一样的。
我们用到的controller还是上面的controller,只是在items增加文件上传选项。 allowFileUpload: true 表示允许上传本地文件、items中增加'insertfile' 表示显示菜单。
<script> var editor; KindEditor.ready(function(K) { editor = K.create('textarea[name="contentEditor"]', { resizeType : 1, allowPreviewEmoticons : false, uploadJson : '/document/upload.html', allowImageUpload : true, allowFileUpload: true, pasteType : 0, //设置能否粘贴 items : [ 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', 'italic', 'underline', 'removeformat', '|', 'justifyleft', 'justifycenter', 'justifyright', 'insertorderedlist', 'insertunorderedlist', '|', 'emoticons', 'image', "insertfile", 'link','fullscreen'] }); }); </script>
显示:
文件上传之后相当于editor给我们增加了一个a标签,href属性为我们返回的url,a标签的text为说明,如下:
我们点击的时候会用a标签访问我们的URL,当浏览器可以解析的文件时会显示,不解析的会以下载的形式进行下载。
这个文件上传成功之后会alert(上传成功),有时候体验不是很好。通过查看源码发现在kindeditor\plugins\insertfile目录的insertfile.js文件的87行左右,如果不想 打印我们去掉打印效果即可,当然也可以自己修改上传成功的源码,如下:
如果我们希望文件在线预览,可以修改后台代码,在调用URL获取文件信息的时候转为PDF返回到流中。当然可以结合kkFileView(git上一个在线预览,也是基于libreoffice转换pdf进行预览,只不过封装了界面与更多的文件类型判断)
补充:vue-kindeditor中修改文件上传后的代码
这里涉及到了修改node_modules文件夹下面的文件,比如我们需要修改:node_modules\kindeditor\plugins\insertfile\insertfile.js
(1)安装patch-package
npm i patch-package --save-dev
(2)修改模块中文件:
(3)执行如下命令(kindeditor是在node_modules下面的文件夹名称)
npx patch-package kindeditor
(4)执行完成会在项目的根路径生成一个patches文件夹,下面的文件记录了差异
文件内容如下:
diff --git a/node_modules/kindeditor/plugins/insertfile/insertfile.js b/node_modules/kindeditor/plugins/insertfile/insertfile.js
index 0a4ce9e..6725451 100644
--- a/node_modules/kindeditor/plugins/insertfile/insertfile.js
+++ b/node_modules/kindeditor/plugins/insertfile/insertfile.js
@@ -84,7 +84,7 @@ KindEditor.plugin('insertfile', function(K) {
if (self.afterUpload) {
self.afterUpload.call(self, url, data, name);
}
- alert(self.lang('uploadSuccess'));
+ // alert(self.lang('uploadSuccess'));
} else {
alert(data.message);
}
(5)修改package.json,scripts加入:
"postinstall": "patch-package"