超简单!教你手把手整一个富文本编辑器
基于Quill的富文本编辑器上传图片,提交内容,后端处理全流程,纯js版本
这几天在做个人博客毕设的时侯苦苦寻找了网上主流的富文本编辑器,发现只有Quill的比较适合自己,但是能找到的文章又不多(主要是有的看不懂),这边给大家介绍一下我的摸索过程。
1.导入quill
前去官网->DOWNLOAD->找到Direct Download->点击release
Quill - Your powerful rich text editor
之后得到解压包,解压好后,导入到你的项目里去
2.生成一个Quill编辑器
新建一个Quill.html页面
html代码为:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文本编辑器</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no" />
<link href="../script/quill/quill.snow.css" rel="stylesheet">
<script src="../script/quill/quill.js"></script>
<style>
/*编辑器样式修改*/
#editor {
height: 400px;
}
#btn {
background-color: #4CAF50;
width: 150px;
color: #fff;
border: none;
padding: 10px;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="toolbar">
<span class="ql-formats">
<button class="ql-bold" title="文字加粗">Bold</button><!--控制粗细-->
<button class="ql-italic" title="文字倾斜">Italic</button> <!--控制切斜-->
<button class="ql-underline" title="添加下划线">下划线</button> <!--下划线-->
<button class="ql-link" title="添加链接">link</button> <!--链接-->
<button class="ql-strike" title="添加划线"></button>
<button class="ql-script" title="添加为下标" value="sub"></button>
<button class="ql-script" title="添加为上标"value="super"></button>
</span>
<span class="ql-formats">
<button class="ql-list" title="添加数字序号" value="ordered"></button><!--序号-->
<button class="ql-list" title="添加点序号" value="bullet"></button> <!--点-->
<button class="ql-blockquote" title="添加引用" ></button> <!--引用-->
<button class="ql-code-block" title="添加代码" ></button> <!--代码-->
<button class="ql-image" title="上传图片" ></button> <!--图片-->
</span>
<span class="ql-formats" title="文字颜色">
<select class="ql-color">
<option selected></option>
<option value="red"></option>
<option value="orange"></option>
<option value="yellow"></option>
<option value="green"></option>
<option value="blue"></option>
<option value="purple"></option>
<option value="grey"></option>
</select>
<select class="ql-background" title="文字背景颜色">
<option selected></option>
<option value="black"></option>
<option value="red"></option>
<option value="orange"></option>
<option value="yellow"></option>
<option value="green"></option>
<option value="blue"></option>
<option value="purple"></option>
</select>
</span>
<!--控制文字位置大小-->
<span class="ql-formats" title="调整文字位置">
<select class="ql-align">
<option selected="selected"></option>
<option value="center"></option>
<option value="right"></option>
<option value="justify"></option>
</select>
</span>
<!--控制文字大小-->
<span class="ql-formats" title="调整文字大小">
<select class="ql-size">
<option value="10px">小字体</option>
<option selected>中字体</option>
<option value="18px">大字体</option>
<option value="32px">超大字</option>
</select>
</span>
<span class="ql-formats" title="切换文字">
<select class="ql-font" style="width:150px">
<option selected="selected"></option>
<option value="SimHei"></option>
<option value="Microsoft-YaHei"></option>
<option value="SimSun"></option>
<option value="KaiTi"></option>
<option value="FangSong"></option>
<option value="Arial"></option>
<option value="Times-New-Roman"></option>
<option value="sans-serif"></option>
</select>
</span>
</div>
<!-- 创建文本编辑器 -->
<div id="editor">
<p>站长建议,按一下空格再开始写比较好,我用的这个编译器有点本身的bug</p>
</div>
<div style="width: 100%;text-align: center;margin-top: 20px;">
<button id="btn">提交</button>
</div>
</body>
</html>
js代码为:
window.onload = function() {
// 文字背景导入
var BackgroundClass = Quill.import('attributors/class/background');
// 文字颜色导入
var ColorClass = Quill.import('attributors/class/color');
// 文字大小导入
var SizeStyle = Quill.import('attributors/style/size');
Quill.register(BackgroundClass, true);
Quill.register(ColorClass, true);
Quill.register(SizeStyle, true);
var editor = new Quill('#editor', {
modules: {
toolbar: '#toolbar'
},
theme: 'snow' // 使用 snow 主题(白色主题)
});
}
最后运行的效果为:
这样你就拥有了一个属于自己的quill编辑器了。
注意,其中id为toolbar的div是Quill编辑器的工具栏即,其中每一个span表示了工具栏的一个工具
id为editor的div是Quill编辑器的输入栏,即
此时基本功能已经完成了
3.绑定提交事件
为提交Button绑定点击事件,javascript代码为:
/* 获取编辑器的html */
document.getElementById('btn').addEventListener('click', function() {
const content = document.querySelector('#editor').children[0];
let data = {
"content": content.innerHTML,
};
var xhr = new XMLHttpRequest();
xhr.open('POST', "http://localhost:9954/test/submitBlog", true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
var response = JSON.parse(xhr.responseText);
if (response['code'] === "yes") {
console.log("提交文章成功!");
} else {
console.log("提交文章失败");
}
} else {
console.log("请求失败");
console.log(xhr.statusText);
}
};
xhr.send(JSON.stringify(data));
});
记得替换自己的后端访问路径
现在开始写后端的内容,我采用的是springBoot框架处理的(鄙人学的是java)
首先在pom里面引入依赖
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>
这是一个可以在java中处理html语言的包
配置跨域请求
package com.example.springboot_ch_1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许的前端域名
config.addAllowedOrigin("*");
// 允许的HTTP方法
config.addAllowedMethod("*");
// 允许的HTTP头部
config.addAllowedHeader("*");
// 允许Content-Type头部
config.addAllowedHeader("Content-Type");
// 暴露Content-Type头部
config.addExposedHeader("Content-Type");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
Controller端为:
@RestController
@RequestMapping("/myblog")
public class BlogController {
@Autowired
private ContentService contentService;
// 得到quill编译器发送来的文章数据
@PostMapping("/test")
public Status submitContent(@RequestBody String data){
Status status;
boolean result2 = contentService.submitContent(data);
if (result2) {
status = new Status("yes", "提交成功");
} else {
status = new Status("no", "网络异常,请重新提交");
}
return status;
}
Service端为:
import com.example.springboot_ch_1.entity.Content;
import com.example.springboot_ch_1.repository.ContentRepository;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Service
@Transactional
public class ContentServiceImpl implements ContentService {
@Autowired
private ContentRepository contentRepository;
public static String user_textContent_href="C:/Users/12431/Desktop/img/textContent/";
public static String user_textContent_href_url="C:/Users/12431/Desktop/img/textContent/";
@Override
public boolean submitContent(String data) {
JSONObject jsonObject = new JSONObject(data);
String content = jsonObject.getString("content");
try {
// 第一个参数是html内容的图片Base64Data数据转化成图片格式
String finalContent = convertBase64Images(content);
System.out.println("最终得到的content是: "+finalContent);
if (finalContent!=null && !finalContent.equals("")){
Content result = new Content();
result.setContentId(generateShortId());
result.setContent(finalContent);
result.setCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
result.setHref(generateShortId());
Content answer = contentRepository.save(result);
if (answer!=null){
return true;
}else{
return false;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
private static String convertBase64Images(String html) throws IOException {
Document doc = Jsoup.parse(html);
Elements imgElements = doc.select("img");
for (Element imgElement : imgElements) {
String base64Data = imgElement.attr("src").split(",")[1];
String extension = getExtensionFromBase64Data(base64Data);
// 生成随机文件名
String randomFileName = generateShortId() + "." + extension;
System.out.println(randomFileName);
// 这是保存路径
String imagePath = user_textContent_href + randomFileName;
// 这是返回图片路径
String imagePath_url = user_textContent_href_url + randomFileName;
// 将base64数据解码为二进制数据
byte[] imageBytes = Base64.getDecoder().decode(base64Data);
// 将二进制数据保存为图片文件
try (FileOutputStream fos = new FileOutputStream(imagePath)) {
fos.write(imageBytes);
}
// 替换img标签的src属性为新的文件地址
imgElement.attr("src", imagePath_url);
}
// 返回修改后的HTML字符串
return doc.outerHtml();
}
private static String getExtensionFromBase64Data(String base64Data) {
if (base64Data.startsWith("/9j/")) {
return "jpeg";
} else {
return "png";
}
}
// 用于生成时间戳+6为随机字符的字符串
public static String generateShortId() {
long timestamp = System.currentTimeMillis();
UUID randomUUID = UUID.randomUUID();
String randomComponent = randomUUID.toString().substring(0, 6);
String shortId = timestamp + randomComponent;
return shortId;
}
}
这边要解释的地方比较多,听我细细道来
首先,我们在使用Quill编辑器插入图片的时候,图片是以转化为Base64数据插入进去的
就是这种类型
现在你有两种选择,
第一个就是不用管它,直接把整个内容插入到数据库,那么本篇文章的第三部分观众姥爷可以不用继续看了。
第二个就是把src提取出来,通过转换base64数据把这一部分作为图片保存起来,并生成一个新的图片的url路径替换进去。
上面的代码就是第二个选择的内容,
convertBase64Images方法是先把发送来的文本转化为html格式,通过Jsoup处理得到文本中所有的<img>标签,获取其中的src数据,然后使用getExtensionFromBase64Data方法,这个方法是判断base64数据转换成哪种格式的图片,generateShortId方法是随机生成一段时间戳+6位字符的字符串,然后可以得到文件名称,此时再使用Base64方法处理从src中得到的数据,并使用FileOutputStream写入即可得到相应文件。
问题来了,我题目里给的imagePath和imagePath_url是啥?
我跟大家解释一下,我们在服务器做项目,上传文件的过程通常需要一个存储路径和一个访问路径,比如在服务器中的后端得到了前端发送来的一个"llbj.png"图片,处理好存储时,就需要存储路径,我现在把图片存储在"/path/img/"文件夹下,
代入到咱们代码里,那么存储路径user_textContent_href就是"/path/img",imagePath就是user_textContent_href+"llbj.png"
但是你也想别人访问你的项目时,能够看到这张图片,那么你就需要一个访问路径,比如你的服务器端通过tomcat配置"/path/img/"文件夹,此时你文件夹的访问路径是:"http://llbj/source/img/",
代入到咱们代码里,那么访问路径user_textContent_href_url就是"http://llbj/source/img/",imagePath_url就是user_textContent_href_url+"llbj.png"
(挂个大佬的链接,大家可以看一下Tomcat服务的配置_tomcat配置文件路径-CSDN博客)
此时就大功告成了,补充一下我其它类的代码
Content类
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;
@Entity
@Data
public class Content {
@Id
public String contentId;
public String content;
public String createTime;
public String href;
}
Status类
import lombok.Data;
@Data
public class Status {
public String code;
public String reason;
public String returnData;
public Status(String code,String reason){
this.code = code;
this.reason = reason;
}
}
pom配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springBoot_ch_1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springBoot_ch_1</name>
<description>springBoot_ch_1</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.14</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>
</dependencies>
<build>
<finalName>myblog_idea</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
application.yml配置
server:
servlet:
context-path: /
port: 9954
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
这边大家最好配置好max-file-size,不然容易上传文件过大导致上传失败
4.展示文本
html页面为
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文本编辑器</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no" />
<link href="../script/quill/quill.snow.css" rel="stylesheet">
<script src="../script/quill/quill.js"></script>
<style>
#editor {
height: 600px;
}
</style>
</head>
<body>
<!-- 这一部分是必须要有的 -->
<div id="editor" class="qi-container ql-snow">
<div class="ql-editor" id="quillContainer" style="white-space:normal;height: 700px;width: 60%;margin: auto;">
</div>
</div>
<script>
window.onload = function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', "http://localhost:9954/test/getBlog", true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
document.getElementById("quillContainer").innerHTML = xhr.responseText;
} else {
console.log("请求失败");
console.log(xhr.statusText);
}
};
xhr.send();
}
</script>
</body>
</html>
后端为
controller端
@GetMapping("/getBlog")
public String getBlog(){
String answer = contentService.getContent();
return answer;
}
service端
@Override
public String getContent() {
// 这边我就简化了一下过程,随便拿到了一个数据展示用
Content content = contentRepository.findAll().get(0);
return content.getContent();
}
5.结果演示
最后的效果图给大家看一下
点击提交
控制台显示“提交成功!”字样
在我指定的位置保存了文本中的图片
内容保存在了我的数据库中
然后运行展示页面,展示结果为
并且可以看到我们的照片拥有了自己的访问路径
6.内容补充
如果你想再多补充一点编辑器的字体,只需要在css样式中添加一下内容
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimSun]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimSun]::before {
content: "宋体";
font-family: "SimSun";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimHei]::before {
content: "黑体";
font-family: "SimHei";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Microsoft-YaHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Microsoft-YaHei]::before {
content: "微软雅黑";
font-family: "Microsoft YaHei";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=KaiTi]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=KaiTi]::before {
content: "楷体";
font-family: "KaiTi";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=FangSong]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=FangSong]::before {
content: "仿宋";
font-family: "FangSong";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Arial]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Arial]::before {
content: "Arial";
font-family: "Arial";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Times-New-Roman]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Times-New-Roman]::before {
content: "Times New Roman";
font-family: "Times New Roman";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=sans-serif]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=sans-serif]::before {
content: "sans-serif";
font-family: "sans-serif";
}
.ql-font-SimSun {
font-family: "SimSun";
}
.ql-font-SimHei {
font-family: "SimHei";
}
.ql-font-Microsoft-YaHei {
font-family: "Microsoft YaHei";
}
.ql-font-KaiTi {
font-family: "KaiTi";
}
.ql-font-FangSong {
font-family: "FangSong";
}
.ql-font-Arial {
font-family: "Arial";
}
.ql-font-Times-New-Roman {
font-family: "Times New Roman";
}
.ql-font-sans-serif {
font-family: "sans-serif";
}
/* 添加自定义滚动条样式 */
.ql-editor {
overflow-y: auto;
/* 允许垂直滚动 */
scrollbar-width: thin;
/* 定义滚动条的宽度,可以是thin、auto、none等值 */
scrollbar-color: #808080 #f0f0f0;
/* 定义滚动条的颜色,两个颜色分别表示滑块和轨道的颜色 */
}
.ql-editor::-webkit-scrollbar {
width: 5px;
/* 定义滚动条的宽度 */
}
.ql-editor::-webkit-scrollbar-thumb {
background-color: #808080;
/* 定义滑块的颜色 */
}
.ql-editor::-webkit-scrollbar-track {
background-color: #f0f0f0;
/* 定义轨道的颜色 */
}
然后再在script中加入即可
window.onload = function() {
//quill编辑器的字体
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif'];
var Font = Quill.import('formats/font');
//将字体加入到白名单
Font.whitelist = fonts;
Quill.register(Font, true);
}
7.最后
这是咱第一次写blog,哪里不好请各位观众老爷指正,如果文章有问题欢迎大家指出,谢谢大家~
最后附上阿能
更多推荐
所有评论(0)