Editor.md 是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建
Editor.md 主页
1. 整合Editor.md
1. 下载 Editor.md 插件代码并放进项目(我放在 plugins 目录下),再在html代码引入Edit.md相关文件
2. 添加编辑框 DOM 元素
<div class="card-body">
<form id="blogForm" onsubmit="return false;">
<div class="form-group" id="blog-editormd">
<textarea style="display:none;"></textarea>
</div>
<div class="form-group">
<!-- 按钮 -->
<button class="btn btn-info float-right" style="margin-left: 5px;"
id="confirmButton">保存文章
</button>
</div>
</form>
</div>
3. 初始化 Editor.md 对象
通过调用 editormd() 方法并传入前文中定义的 DOM id,之后再次重启项目就能够看到编辑器的效果了,如下图所示:
<script type="text/javascript">
var blogEditor;
$(function () {
blogEditor = editormd("blog-editormd", {
width: "100%",
height: 640,
syncScrolling: "single",
path: "/admin/plugins/editormd/lib/",
toolbarModes: 'full'
});
});
</script>
4. Editor.md 编辑器图片上传功能完善
在整合之后,默认是不可以上传图片的,我们需要略作配置修改,在 Editor.md 编辑器初始化时新增如下配置项:
/**图片上传配置*/
imageUpload: true,//开启图片上传
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"], //图片上传格式
imageUploadURL: "/admin/blogs/md/uploadfile",//图片上传的后端路径
onload: function (obj) { //上传成功之后的回调
}
其中imageFormats就是图片的上传地址
文件上传后端代码
@PostMapping("/blogs/md/uploadfile")
public void uploadFileByEditormd(HttpServletRequest request,
HttpServletResponse response,
@RequestParam(name = "editormd-image-file", required = true)
MultipartFile file) throws IOException, URISyntaxException {
String fileName = file.getOriginalFilename();
String suffixName = fileName.substring(fileName.lastIndexOf("."));
//生成文件名称通用方法
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
Random r = new Random();
StringBuilder tempName = new StringBuilder();
tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName);
String newFileName = tempName.toString();
//创建文件
//我自定义的文件保存路径 E:/upload
File destFile = new File("E:/upload" + newFileName);
//这一步应该是为了数据的回显
String fileUrl = MyBlogUtils.getHost(new URI(request.getRequestURL() + "")) + "/upload/" + newFileName;
File fileDirectory = new File("E:/upload");
try {
if (!fileDirectory.exists()) {
if (!fileDirectory.mkdir()) {
throw new IOException("文件夹创建失败,路径为:" + fileDirectory);
}
}
file.transferTo(destFile);
request.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "text/html");
response.getWriter().write("{\"success\": 1, \"message\":\"success\",\"url\":\"" + fileUrl + "\"}");
} catch (UnsupportedEncodingException e) {
response.getWriter().write("{\"success\":0}");
} catch (IOException e) {
response.getWriter().write("{\"success\":0}");
}
}
可以看到,当我们上传一个图片的时候,调用了这一方法进行了图片的上传
其中有一步
String fileUrl = MyBlogUtils.getHost(new URI(request.getRequestURL() + "")) + "/upload/" + newFileName;
......
response.getWriter().write("{\"success\": 1, \"message\":\"success\",\"url\":\"" + fileUrl + "\"}");
将存储的图片地址返回给前端进行图片的回显拿到相应的图片
MyBlogUtils.getHost(new URI(request.getRequestURL() + “”))获取主机名
MyBlogUtils:
public class MyBlogUtils {
public static URI getHost(URI uri) {
URI effectiveURI = null;
try {
effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
} catch (Throwable var4) {
effectiveURI = null;
}
return effectiveURI;
}
}
因为我设置的图片保存地址是在E:/upload,所以为了能直接访问这个外部目录,需要自定义静态资源映射目录,重写addResourceHandlers方法
@Configuration
public class MyBlogWebMvcConfigurer implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//addResourceLocations指的是文件放置的目录,addResoureHandler指的是对外暴露的访问路径("file:"是规定写法)
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + "E:/upload");
}
}
之后访问/upload/filename实际上就是去"E:/upload"下找相应的文件
5. Editor.md 编辑器编辑器粘贴图片上传
上面只是点击编辑器自带的上传图片按钮进行图片上传的实现,但是在该富文本编辑器中是可以直接粘贴图片到富文本编辑器中进行上传的,具体实现是首先在前端需要监听粘贴事件,并对粘贴文件的类型进行判断,如果是图片则上传,再通过返回值进行回显
// 编辑器粘贴上传
document.getElementById("blog-editormd").addEventListener("paste", function (e) {
var clipboardData = e.clipboardData;
if (clipboardData) {
var items = clipboardData.items;
if (items && items.length > 0) {
for (var item of items) {
if (item.type.startsWith("image/")) {
var file = item.getAsFile();
if (!file) {
alert("请上传有效文件");
return;
}
var formData = new FormData();
formData.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/admin/upload/file");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var json=JSON.parse(xhr.responseText);
if (json.resultCode == 200) {
blogEditor.insertValue("![](" + json.data + ")");
} else {
alert("上传失败");
}
}
}
xhr.send(formData);
}
}
}
}
});
后端
@PostMapping({"/upload/file"})
@ResponseBody
public Result upload(HttpServletRequest httpServletRequest, @RequestParam("file") MultipartFile file) throws URISyntaxException {
String fileName = file.getOriginalFilename();
String suffixName = fileName.substring(fileName.lastIndexOf("."));
//生成文件名称通用方法
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
Random r = new Random();
StringBuilder tempName = new StringBuilder();
tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName);
String newFileName = tempName.toString();
File fileDirectory = new File(Constants.FILE_UPLOAD_DIC);
//创建文件
File destFile = new File(Constants.FILE_UPLOAD_DIC + newFileName);
try {
if (!fileDirectory.exists()) {
if (!fileDirectory.mkdir()) {
throw new IOException("文件夹创建失败,路径为:" + fileDirectory);
}
}
file.transferTo(destFile);
Result resultSuccess = ResultGenerator.genSuccessResult();
resultSuccess.setData(MyBlogUtils.getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/upload/" + newFileName);
return resultSuccess;
} catch (IOException e) {
e.printStackTrace();
return ResultGenerator.genFailResult("文件上传失败");
}
}
这里对前端返回的数据统一封装成了Result对象数据的封装
2. 文章上传
通过==getMarkdown()==方法可以获取编辑器中的内容,并通过Ajax进行上传
$('#saveButton').click(function () {
var blogId = $('#blogId').val();
var blogTitle = $('#blogName').val();
var blogSubUrl = $('#blogSubUrl').val();
var blogCategoryId = $('#blogCategoryId').val();
var blogTags = $('#blogTags').val();
var blogContent = blogEditor.getMarkdown();
var blogCoverImage = $('#blogCoverImage')[0].src;
var blogStatus = $("input[name='blogStatus']:checked").val();
var enableComment = $("input[name='enableComment']:checked").val();
if (isNull(blogCoverImage) || blogCoverImage.indexOf('img-upload') != -1) {
swal("封面图片不能为空", {
icon: "error",
});
return;
}
var url = '/admin/blogs/save';
var swlMessage = '保存成功';
var data = {
"blogTitle": blogTitle, "blogSubUrl": blogSubUrl, "blogCategoryId": blogCategoryId,
"blogTags": blogTags, "blogContent": blogContent, "blogCoverImage": blogCoverImage, "blogStatus": blogStatus,
"enableComment": enableComment
};
if (blogId > 0) {
url = '/admin/blogs/update';
swlMessage = '修改成功';
data = {
"blogId": blogId,
"blogTitle": blogTitle,
"blogSubUrl": blogSubUrl,
"blogCategoryId": blogCategoryId,
"blogTags": blogTags,
"blogContent": blogContent,
"blogCoverImage": blogCoverImage,
"blogStatus": blogStatus,
"enableComment": enableComment
};
}
console.log(data);
$.ajax({
type: 'POST',//方法类型
url: url,
data: data,
success: function (result) {
if (result.resultCode == 200) {
$('#articleModal').modal('hide');
swal({
title: swlMessage,
type: 'success',
showCancelButton: false,
confirmButtonColor: '#3085d6',
confirmButtonText: '返回博客列表',
confirmButtonClass: 'btn btn-success',
buttonsStyling: false
}).then(function () {
window.location.href = "/admin/blogs";
})
}
else {
$('#articleModal').modal('hide');
swal(result.message, {
icon: "error",
});
}
;
},
error: function () {
swal("操作失败", {
icon: "error",
});
}
});
});
3. 发布后文章的展示
我们文章发布后数据库存储的是markdown格式的文档,但是在前端展示出相应的样式需要把markdown格式的文档转换为符合 html 标签语法的代码片段
想要达到这个目的,我们需要借助第三方工具类,本系统中我选择的 markdown 解析器是 CommonMark 解析器,首先需要将相关依赖引入到 pom.xml 文件中,依赖文件增加如下设置
<!-- commonmark core -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.8.0</version>
</dependency>
<!-- commonmark table -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>0.8.0</version>
</dependency>
MarkDownUtil 工具类,代码如下:
package com.site.blog.my.core.util;
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.util.StringUtils;
import java.util.Arrays;
public class MarkDownUtil {
/**
* 转换md格式为html
*
* @param markdownString
* @return
*/
public static String mdToHtml(String markdownString) {
if (StringUtils.isEmpty(markdownString)) {
return "";
}
java.util.List<Extension> extensions = Arrays.asList(TablesExtension.create());
Parser parser = Parser.builder().extensions(extensions).build();
Node document = parser.parse(markdownString);
HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build();
String content = renderer.render(document);
return content;
}
}
这样我们就可以借助 CommonMark 解析器我们将文章的 content 字段由 markdown 格式的字符串转换为页面显示所需的 html 片段,再返回给前端
小工具 :代码高亮
使用highlight 插件让代码变得五颜六色,可以让代码看起来更直观,也更美观
下载相关文件后引入,之后指定对应 DOM 中的元素进行高亮操作,在 markdown 格式的内容转换为 html 标签格式的内容后,代码块通常是在
中,所以可以进行如下设置
<script type="text/javascript">
$(function () {
// 代码高亮
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
});
</script>