目录
概述
Vue3整合wagnEditor
图片的上传
图片的删除
文章存储
文章渲染
概述
实现功能:管理端使用富文本编辑器编写文章内容,将编辑好的文章存入数据库或服务器中,前端应用读取存储的文章内容作展示。
本文章能提供
①Vue3整合wangEditor过程。
②整合后实现图片上传并提供服务端代码参考。
③提供监测编辑器删除图片事件捕获,同步删除服务器上图片。
Vue3整合wagnEditor
Vue3起步文档:用于 Vue | wangEditor
进入地址,可以点击此处看demo蛮有用:
1.安装
npm install @wangeditor/editor --save
npm install @wangeditor/editor-for-vue@next --save
2.添加html结构
<template><div style="border: 1px solid #ccc;"><Toolbar:editor="editorRef":defaultConfig="toolbarConfig":mode="mode"style="border-bottom: 1px solid #ccc"/><Editor:defaultConfig="editorConfig":mode="mode"v-model="valueHtml"style="height: 500px; overflow-y: hidden;"@onCreated="handleCreated"@onDestroyed="handleDestroyed"/></div>
</template>
3.编写js部分
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();let mode = "defualt";// 内容 HTML
const valueHtml = ref('');// 编辑器配置
const toolbarConfig = {};
const editorConfig = { placeholder: '请输入内容...',MENU_CONF: {uploadImage: {fieldName: 'file',server: `${import.meta.env.VITE_BASE_URL}/commons/upload` // 注意 ${import.meta.env.VITE_BASE_URL} 写你自己的后端服务地址}}
};// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {const editor = editorRef.valueif (editor == null) returneditor.destroy()
})const handleCreated = (editor) => {editorRef.value = editor // 记录 editor 实例,重要!
}
</script>
ok,弄到这里,基本样式就出来了,基本的文字编辑内容是可以用了的。
ps:如果你的需求无需加图片,其实功能已经实现,直接将html内容存储即可,可以直接跳到目录"存储文章"看看细节。
这时候图片的上传和粘贴图片并不奏效,需要往配置编写点代码:
let's go 让我们往下。
图片的上传
图片的上传也很简单,两步走:
①前端代码配置后端的图片上传接口路径。
②后端把图片上传接口的返回值设置固定格式。
①前端添加配置(在上方的js代码中你会发现下面代码包含在其中):
const editorConfig = { placeholder: '请输入内容...',MENU_CONF: {uploadImage: {fieldName: 'file',server: `${import.meta.env.VITE_BASE_URL}/commons/upload`}}
};
其中的${import.meta.env.VITE_BASE_URL}/commons/upload 请修改为你后端的图片上传接口地址,如果你担心接口代码兼容性问题,不用担心,下面我会提供我后端的图片上传接口给你作参考适配。
②后端固定返回值格式
这是必要且固定的格式设置,官网描述:
这是因为当我们将图片上传之后,以固定的数据结构返回,那么前端就能拦截并获取到url等信息用以构造<img>标签然后放到页面中展示。
我的文件上传接口(springboot中代码)
主要看到try{}块中代码:
/*** 文件上传接口* @param file 前端传入的文件对象* @return 返回存在服务器的文件名称*/@PostMapping("/upload")public ResponseEntity<Map<String, Object>> fileUpload(@RequestParam("file") MultipartFile file) {Map<String, Object> response = new HashMap<>();// 获取上传的图片文件后缀名String originalFilename = file.getOriginalFilename();String fileExtension = originalFilename.substring(originalFilename.lastIndexOf('.'));// 检查文件是否为空if (file.isEmpty()) {response.put("errno", 1);response.put("message", "File is empty!");return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}// 检查文件名是否合法,避免目录遍历攻击String fileName = StringUtils.cleanPath(originalFilename);if (fileName.contains("..")) {response.put("errno", 1);response.put("message", "Illegal name!");return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}try {// 给上传的图片随机生成一个名称,将之返回,// 用户就可以根据此名称下载图片,防止图片名称冲突。UUID uuid = UUID.randomUUID();String randomUUIDString = uuid.toString();// 将文件保存到指定目录文件File targetFile = new File(this.picturePath + randomUUIDString + fileExtension);// 将传入的图片转存到指定目录文件file.transferTo(targetFile);// 构建成功响应Map<String, Object> data = new HashMap<>();// myEnv 服务端的前缀例如本地测试时 http://localhost:8080String imageUrl = myEnv +"/commons/download?picName=" + randomUUIDString + fileExtension;data.put("url", imageUrl); // 使用拼接的URL路径data.put("alt", "Image description"); // 可以根据需要从文件或其他地方获取data.put("href", imageUrl); // 使用同一个URL作为hrefdata.put("pictureName", randomUUIDString + fileExtension);response.put("errno", 0);response.put("data", data);return ResponseEntity.ok(response);} catch (IOException e) {e.printStackTrace();response.put("errno", 1);response.put("message", "Server error!");return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}}
实现上传的方式千千万,只要返回指定的格式即可。上方代码供参考。
当你做完这两步你会发现,编辑器就可以实现图片上传和图片粘贴功能了。
图片的删除
当然,这里说明的并不是用手指点击一下"backspace"的操作把图片删除的新手电脑人教程。
而是当我们将图片删除之后,我们其实是需要获取删除图片对象,将服务器中对应的图片删除的,因为这个编辑器框架上传图片的方式是粘贴图片后直接上传到服务器,没有先存在缓存。
这里说明一下,我在开发的时候没有注意到(眼瞎)官方已经提供了解决方案,甚至还吐槽了一下这个功能都没有,结果就在刚刚我再次访问时看到了...:
非常贴心,但是现在才看到的我已经裂开,因为我已经自己用vue的watch和diff差异对比库实现了这个功能。当然,如果看到这的小伙伴,建议直接使用官网提供的方法做就行,更成熟。我没有考虑到图片撤回的操作。
我的实现方法(大伙基本可以跳过,直接去使用官方提供的方法即可)
// 监听valueHtml的变化,做差异化对比,查看是否是删除图片操作,是的话获取src中的文件名
watch(valueHtml, (newValue, oldValue) => {if(newValue.length > oldValue.length){ // 如果是插入不做任何操作return 0;}// 删除操作,调用自定义对比函数,获取差异内容const diff = getHtmlDifference(oldValue, newValue);if (!diff.includes('img')){ // 差异内容包含img,即可确定删除了图片return 0;}// 使用正则表达式匹配src属性中的文件名const regex = /src="http?:\/\/[^\/]+\/commons\/download\?picName=([^"]+)/;const match = diff.match(regex);if (match && match[1]) {const fileName = match[1]; // 提取的文件名// 删除服务器图片,并给出提示deletePictureFromServer(fileName);}
});// 调用diff库与html作差异化对比
function getHtmlDifference(previousHtml, currentHtml){let changes = diffWords(previousHtml, currentHtml);if(changes.length <=1){return "";}return changes[1].value
}// 删除服务器冗余图片
async function deletePictureFromServer(fileName){let res = await axios.delete(`/commons/deleteFile/${fileName}`);if(res.data == "删除图片成功!"){ElNotification({title: '服务器提示',message: '图片删除成功',type: 'success',})}
}
其中,用到的文本差异化对比库为diff,github地址:GitHub - kpdecker/jsdiff: A javascript text differencing implementation.
服务端删除功能接口代码:
/*** 删除文件图片* @param imageName 图片名称* @return ·*/@DeleteMapping("/deleteFile/{imageName}")public String deleteImage(@PathVariable("imageName") String imageName) {if (StringUtils.isEmpty(imageName)) {return "请传入图片名称";}String imagePath = this.picturePath + imageName;try {File file = new File(imagePath);if (!file.exists()) {return "图片没有找到!";}if (!file.delete()) {return "删除图片失败!";}return "删除图片成功!";} catch (Exception e) {return "发生错误!";}}
ok,就这样。
文章存储
当我们做完如上操作之后,基本的文本编辑,图片处理就没问题了,可以开始考虑存储问题了。
可以注意到一开始给的代码中有一个变量:valueHtml
当然,应该都能看出它就是存储我们的html结构内容的。
我们编写好内容之后仅需要将这个变量中存储的内容持久化存储起来就好。
一般有两种存储方案:
① 将html结构内容转换为.html文件或md文件,然后存储到服务器中。
② 直接将html结构内容存到数据库中(数据类型选longtext比较合适)。
因为我的文章内容不是非常非常长那种,所以就直接使用第二种方案了。
代码就不贴了,将valueHtml变量作为参数传入你的接口存入数据库就行(废话了)
但是有一点需要注意,没错,就是我踩的坑了 :(
如果,你遇到如下错误,请报警...开玩笑
### Error updating database. Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x99\x81</...' for column 'content' at row 1
### The error may exist in com/mh/dao/TabArticleDao.java (best guess)
### The error may involve com.mh.dao.TabArticleDao.insert-Inline
### The error occurred while setting parameter
这个错误通常是当你在编辑器中使用了emoji表情,他是四个字节的 UTF-8 编码的字符,在MYSQL默认使用的字符集中,并不支持四字节的 UTF-8 字符,因此你需要修改数据库,数据表,数据字段的字符集为 utf8mb4
ps: 这就体现出创建数据库时显示的指定数据库字符集的重要性了。
# 修改数据库字符集
ALTER DATABASE your_database_name CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;# 修改表的字符集
ALTER TABLE your_table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;# 修改指定列的字符集
ALTER TABLE your_table_name CHANGE column_name column_name TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
如果你的目标表是被参考表/主表,也就是存在外键约束,那么你需要先将外键删除掉后才能通过上方语句修改字符集。
# 查看指定数据库的指定表的所有键名SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_NAME = 'tab_article_tag' AND TABLE_SCHEMA = 'your_database_name';# 找到外键名之后,删除外键ALTER TABLE 表名 DROP FOREIGN KEY 外键名;
当然,改完记得恢复你的外键。
ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (字段名) REFERENCES 被参考表名(被参考字段);
文章渲染
通过接口获取存储在数据库或服务器的html结构内容应当不是重点,当我们获取到html结构内容的之后,我们只要将他们丢到html结构中即可,非常之简单,这里提及主要是想给大家复习一下vue中的 v-html 的用法。
<template>中直接声明:
<template><div id='container'><div v-html="rawHtml"></div></div>
</template>
<javascript setup>中编写:
<script setup>
import { ref, onMounted } from 'vue';
import axios from "../config/axios.js"// 文章内容
const rawHtml = ref('');onMounted(()=>{// 获取文章内容渲染getArticleContent('文字频闭一下')
})// 获取html结构赋予rawHtml 展示即可
async function getArticleContent(articleId){let res = await axios.get(`/tabArticles/${articleId}`);rawHtml.value = res.data.content
}
</script>
OK,就这样,完毕!