csdn本文链接:https://blog.csdn.net/u010004317/article/details/89513099
一. 前言
目前公司使用的富文本编辑器是百度的UEditor,目前其最新的版本停留在了16年的版本,基本算是无人维护的了,作为一个传统但是强劲的富文本编辑器,UEditor在初期的确是很多学生或者小型创业公司后台开发的首选富文本编辑器,但是随着业务的深入,UEditor的问题也暴露出来了。
-
代码结构复杂,无法很好地自定义和扩展(这个是很重要的原因,改起来真的很麻烦)
-
bug多,不够稳定
-
样式太过老旧
富文本编辑器对于我们来说,要简单,够用就好了,太多花里胡哨的功能我们都不需要用到,参考过公众号和知乎的富文本编辑器,都是很简单的,虽然对对于我们做电商来说太过简单了,但是基本功能和样式就是我们想要的,我们完全可以在其基础上进行扩展。
另外个原因,我们的图片服务器是oss,所以需要通过富文本编辑器直传图片到oss中,直接在百度UEditor中,花了很大的力气接入了oss,虽然功能基本实现了,但是效果很不理想,无法所见即所得。
基于以上种种,我在市面上找了好几个编辑器,但是效果都不是很理想,接下来我列一下。
1. Editor.js
-
简介:该编辑器是以块作为基本元素来的,每次编辑都是在一个块上进行编辑,主要的特点是,它的输出是json格式的,而不是传统的html格式,这是我非常喜欢的。可自定义插件,操作简单快捷。
-
建议:该编辑器适合业务不复杂的场景,以及只有pc端展示的场景,如写简单文章之类的,如果对于公司来说,需要从word编辑,需要提供移动端做html展示(对于ios和安卓来说,他们只能通过html来做渲染,无法引入该编辑器),该编辑器则无法实现
2. NKeditor
-
简介:基于 kindeditor 进行二次开发的项目,文档少,理解困难,这是产品找给我的
-
建议:不推荐用,没文档的开源就是坑,而且还是二次开发的。
3. wangEditor
-
简介:这款是我在网上发现很多人推崇的,github的star数也有7000多,算是比较活跃的一款开源编辑器,文档算是比较齐全,我刚开始也是觉得挺OK的,但是另外一个方面我觉得他的自定义插件功能不够强大,我的项目对自定义插件的要求很高,所以放弃
-
建议:对自定义插件功能要求不高的,我推荐这款编辑器
4. CKEditor
- 官网:https://ckeditor.com/ckeditor-4/ (CKEditor4地址)
https://ckeditor.com/ (CKEditor地址)
-
简介:CKEditor分CKEditor4和CKEditor5,因为CKEditor5要用到npm打包,并且其缺少了一些我觉得很重要的功能,所以我选择了CKEditor4,而且CKEditor5比较新,CKEditor4经历过了一段时间的考验。这也是在机缘巧合之下发现了这款编辑器,是国外开源的一款编辑器,拥有自己的详细的官网,而且官网做得还不错,github的社区也十分活跃,最近一次提交也是在一个月内,让我看到了他的可靠性,至少在未来两年内,该框架都会持续更新和优化,从长远来看,这都是其他编辑其所无法实现的。
-
他提供了多种编辑器样式供选择,而且文档很详细,老外写的文档,不得不说,真的很详细,虽然很长,像nginx,spring-boot的文档一样。而且文档中有各种demo,并且demo也提供了源码,给初学者提供了一个很好的入门机会。
-
建议:强烈推荐,只是是英文文档,读起来有点费力,不过技术英文文档估计是最简单的英文入门了,所用词汇相对简单,再结合词典就很容易理解了,不推荐把整个文档翻译成中文来看,先读英文文档,发现不懂的单词再去查词典,结合上下文理解他的语境。
二. 搭建基础的CKEditor框架
1. 基本配置
官网下载文件,下载界面可选:Basic Package、Standard Package、Full Package、Customize。根据自己需求下载不同的安装包,而且每种都有压缩版和源码版可选。其中Customize版本顾名思义可自定义选择自己需要的模块,官方也推荐使用这种方式自定义下载。Customize版本相当于在线上让你通过图形化界面自定义自己想要的插件和皮肤样式等,说实在,这个功能很强大。
1. 1 引入js
<script src="../install package/ckeditor/ckeditor.js"></script>
1.2 写一个textarea标签
<textarea name="editor1" id="editor1" rows="10" cols="80">
This is my textarea to be replaced with CKEditor.
</textarea>
1.3 初始化CKEditor
CKEDITOR.replace('editor1', {
uiColor: '#9AB8F3'
});
1.4 完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="../../install package/ckeditor/ckeditor.js"></script>
<link href="simple-image.css" rel="stylesheet" />
</head>
<form action="" method="post" onclick="save()">
<textarea name="editor1" id="editor1" rows="10" cols="80">
This is my textarea to be replaced with CKEditor.
</textarea>
</form>
<script>
window.onload = function() {
}
// Replace the <textarea id="editor1"> with a CKEditor
// instance, using default configuration.
CKEDITOR.replace('editor1', {
uiColor: '#9AB8F3'
});
function save() {
var data = CKEDITOR.instances.editor1.getData();
alert(data)
console.log(data);
}
</script>
<body>
</body>
</html>
1.5 效果
在这里插入图片描述2. 自定义配置
在这里插入图片描述 在这里插入图片描述CKEditor做得十分人性化,它不需要你去读文档,知道每个插件图标的的配置信息,然后去代码中手动添加或移除,他在full的文档中提供了一个html页面,可以通过图形化界面自定义配置信息,该html的页面在
ckeditor/sample/index.html
在这里插入图片描述生成的配置文件
代码配置
CKEDITOR.replace('editor1', {
toolbarGroups: [
{ name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
{ name: 'editing', groups: [ 'find', 'selection', 'spellchecker', 'editing' ] },
{ name: 'forms', groups: [ 'forms' ] },
'/',
{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
{ name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi', 'paragraph' ] },
{ name: 'links', groups: [ 'links' ] },
{ name: 'insert', groups: [ 'insert' ] },
'/',
{ name: 'styles', groups: [ 'styles' ] },
{ name: 'colors', groups: [ 'colors' ] },
{ name: 'tools', groups: [ 'tools' ] },
{ name: 'others', groups: [ 'others' ] },
{ name: 'about', groups: [ 'about' ] }
],
removeButtons: 'Source,Save,Templates,Undo,Find,SelectAll,Scayt,Form,Bold,CopyFormatting,NumberedList,Outdent,Blockquote,JustifyLeft,BidiLtr,Link,Image,Cut,Copy,Redo,Replace,NewPage,Preview,Print,Paste,PasteText,PasteFromWord,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField',
allowedContent: false //关闭acf功能,不建议关闭
});
三. CKEditor自定义插件
CKEditor4提供了强大的自定义插件功能,强烈建议根据它的官方文档中的4个demo一个个手敲一遍,而且一个都不要落下,敲完之后你会对CKEditor4的自定义插件有个更加深入的认识。因为公司用到的图片服务器是阿里云的oss,所以需要在编辑器中接入oss,针对图片都上传到oss中,我这边就简单介绍下如何自定义oss上传插件
1. 文件结构
CKEditor4规定自定义插件,必须要在plugins目录下新建一个插件同名文件夹,然后在该文件夹下有
plugin.js
(用于写插件的初始化等基本信息),dailog
文件夹,该文件夹下的js用于实现对话框的具体业务逻辑,icons
文件夹,该文件夹下有xxx.png的图片,作为插件的图标。
uploadimages
......dialog
.............uploadimage.js
.............upload.js
......icons
.............uploadimages.png
......plugin.js
2. 代码
2.1 plugin.js
CKEDITOR.plugins.add('uploadimages', {
icons: 'uploadimages',
//初始化方法
init: function(editor) {
//添加命令,最简单的自定义插件,可以在addCommand这个方法里面,通过定义exec,直接插入相应的html即可
/*****
演示最简答的自定义插件,插入相应的html,与本例子无关
editor.addCommand('insertTimestamp', {
exec: function(editor) {
var now = new Date();
editor.insertHtml('<p>The current date and time is232323232: ' + now.toString() + 'xcxzc</p><br/>');
}
});
****/
editor.addCommand('uploadimages', new CKEDITOR.dialogCommand('uploadimagesDialog', {
// allowedContent: 'abbr[title,id]',
// requiredContent: 'abbr'
}));
//添加插件按钮
editor.ui.addButton('UploadImages', {
label: '上传图片',
command: 'uploadimages',
toolbar: 'insert'
});
//添加对话框,配置实现对话框逻辑的js文件
CKEDITOR.dialog.add('uploadimagesDialog', this.path + 'dialog/uploadimages.js');
}
});
2.2. upload.js
我们使用的oss上传是通过官方提供的
plupload.Uploader
与后台进行交互获取秘钥等信息进行上传的,在CKEditor,官方提供的例子要做些修改
由下面的代码可以看出,官方提供的例子的plupload.Uploader的初始化方法被我移走了,是的,初始化事件要在CKEditor中完成,
accessid = ''
accesskey = ''
host = ''
policyBase64 = ''
signature = ''
callbackbody = ''
filename = ''
key = ''
expire = 0
g_object_name = ''
g_object_name_type = 'local_name'
now = timestamp = Date.parse(new Date()) / 1000;
function send_request()
{
var xmlhttp = null;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
if (xmlhttp!=null)
{
// serverUrl是 用户获取 '签名和Policy' 等信息的应用服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
serverUrl = 'http://xxxxxxxx/api/oss/getOssServer'
xmlhttp.open( "GET", serverUrl, false );
xmlhttp.send( null );
return xmlhttp.responseText
}
else
{
alert("Your browser does not support XMLHTTP.");
}
};
function check_object_radio() {
var tt = document.getElementsByName('myradio');
for (var i = 0; i < tt.length ; i++ )
{
if(tt[i].checked)
{
g_object_name_type = tt[i].value;
break;
}
}
}
function get_signature()
{
// 可以判断当前expire是否超过了当前时间, 如果超过了当前时间, 就重新取一下,3s 作为缓冲。
now = timestamp = Date.parse(new Date()) / 1000;
if (expire < now + 3)
{
body = send_request()
var obj = eval ("(" + body + ")");
host = obj['host']
policyBase64 = obj['policy']
accessid = obj['accessid']
signature = obj['signature']
expire = parseInt(obj['expire'])
callbackbody = obj['callback']
key = obj['dir']
return true;
}
return false;
};
function random_string(len) {
len = len || 32;
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = chars.length;
var pwd = '';
for (i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
function get_suffix(filename) {
pos = filename.lastIndexOf('.')
suffix = ''
if (pos != -1) {
suffix = filename.substring(pos)
}
return suffix;
}
function calculate_object_name(filename)
{
if (g_object_name_type == 'local_name')
{
g_object_name += "${filename}"
}
else if (g_object_name_type == 'random_name')
{
suffix = get_suffix(filename)
g_object_name = key + random_string(10) + suffix
}
return ''
}
function get_uploaded_object_name(filename)
{
if (g_object_name_type == 'local_name')
{
tmp_name = g_object_name
tmp_name = tmp_name.replace("${filename}", filename);
return tmp_name
}
else if(g_object_name_type == 'random_name')
{
return g_object_name
}
}
function set_upload_param(up, filename, ret)
{
if (ret == false)
{
ret = get_signature()
}
g_object_name = key;
if (filename != '') { suffix = get_suffix(filename)
calculate_object_name(filename)
}
new_multipart_params = {
'key' : g_object_name,
'policy': policyBase64,
'OSSAccessKeyId': accessid,
'success_action_status' : '200', //让服务端返回200,不然,默认会返回204
'callback' : callbackbody,
'signature': signature,
};
up.setOption({
'url': host,
'multipart_params': new_multipart_params
});
up.start();
}
2.3 uploadimages.js
var srcArray = new Array();
var index = 0;
CKEDITOR.dialog.add('uploadimagesDialog', function (editor) {
var testHtml = new CKEDITOR.template('<div></dvi>').output();
return {
title: '图片上传',
minWith: 400,
minHeight: 200,
//content是子标签
contents: [
{
id: 'ossimage',
label: '上传图片',
//elements是每个子标签下面的ui元素,如表单元素等
elements: [ //自定义弹窗的内容,可以使用模板,也可自定义html及样式
{
type: 'html', //图片上传成功后的容器
html: '<div id="ossfile"</div>',
style: '', //对应的样式
onShow: function () {
//在每次弹窗打开的时候都会调用该方法
},
//点击确定按钮时,在onOK中调用commitContent,会依次触发element的commit方法
commit: function (editor) { //点击确定按钮时,将图片src传入全局src中
src = $('.imgbox img').attr('src');
},
//点击确定按钮时,在onOK中滴啊用superContent,会依次触发element的setup方法
setup: function (editor) {
}
},
{
type: 'html', //图片上传成功后的容器
html: '<a id="postfiles" href="javascript:void(0);" class="btn">开始上传</a>',
style: 'display:none;', //对应的样式
},
{
id: 'myimage', //选择图片按钮
type: 'html',
html: '<div id="container"><a id="selectfiles" href="javascript:void(0);" class="btn">选择文件</a></div>', //plupload按钮
style: 'display:block;width:82px;line-height:34px;background-color:#3366b7;font-size:14px;color:#fff;text-align:center;border-radius:4px;', //html的样式,直接作用于上面的a元素
onShow: function () { //当该元素show的时候执行的方法
document.getElementById('ossfile').innerHTML = '';
},
onLoad: function () {
//uploader需要再onLoad方法中定义,因为只有在onload的时候,才能获取到‘selectfiles’的html元素,uploader才能初始化
var uploader = new plupload.Uploader({
runtimes: 'html5,flash,silverlight,html4',
browse_button: 'selectfiles',
//multi_selection: false,
container: document.getElementById('container'),
flash_swf_url: 'lib/plupload-2.1.2/js/Moxie.swf',
silverlight_xap_url: 'lib/plupload-2.1.2/js/Moxie.xap',
url: 'http://oss.aliyuncs.com',
filters: {
mime_types: [ //只允许上传图片和zip文件
{ title: "Image files", extensions: "jpg,gif,png,bmp" },
{ title: "Zip files", extensions: "zip,rar" }
],
max_file_size: '10mb', //最大只能上传10mb的文件
prevent_duplicates: true //不允许选取重复文件
},
init: {
PostInit: function () {
document.getElementById('ossfile').innerHTML = '';
document.getElementById('postfiles').onclick = function() {
set_upload_param(uploader, '', false);
return false;
};
},
FilesAdded: function (up, files) {
plupload.each(files, function (file) {
document.getElementById('ossfile').innerHTML +=
'<div style="width:150px;height:150px;border:solid 1px;text-align: center;float:left" id="' + file.id + '">'
+ '<image src="" alt=“占位图”/>'
+ '</div>';
});
document.getElementById('postfiles').click();
},
BeforeUpload: function (up, file) {
check_object_radio();
set_upload_param(up, file.name, true);
},
//上传中,这里根据需要自己写上传等待,也可在外部实现
UploadProgress: function (up, file) {
// var d = document.getElementById(file.id);
// d.getElementsByTagName('b')[0].innerHTML = '<span>' + file.percent + "%</span>";
// var prog = d.getElementsByTagName('div')[0];
// var progBar = prog.getElementsByTagName('div')[0]
// progBar.style.width = 2 * file.percent + 'px';
// progBar.setAttribute('aria-valuenow', file.percent);
},
FileUploaded: function (up, file, info) {
if (info.status == 200) {
var imageSrc = get_uploaded_object_name(file.name);
//记住:这里要根据imageSrc最后的图片名字进行从小到大排序,即0.png,1.png这样子依次插入到数组中人,然后再根据输入插入到富文本编辑器中
//因为oss上传针对同个文件名不能上传多次,所以在upload.js要针对文件名进行有规律地自定义
//上传成功之后显示图片缩略图
srcArray[index] = imageSrc;
index = index + 1;
console.log(document.getElementById(file.id))
document.getElementById(file.id).innerHTML = '<image src="https://XXXX/'+ imageSrc +'?x-oss-process=image/resize,w_150,h_150" alt=“”/>';
}
else if (info.status == 203) {
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = '上传到OSS成功,但是oss访问用户设置的上传回调服务器失败,失败原因是:' + info.response;
}
else {
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response;
}
},
Error: function (up, err) {
if (err.code == -600) {
console.log("\n选择的文件太大了,可以根据应用情况,在upload.js 设置一下上传的最大大小")
// document.getElementById('console').appendChild(document.createTextNode("\n选择的文件太大了,可以根据应用情况,在upload.js 设置一下上传的最大大小"));
}
else if (err.code == -601) {
console.log("\n选择的文件后缀不对,可以根据应用情况,在upload.js进行设置可允许的上传文件类型")
// document.getElementById('console').appendChild(document.createTextNode("\n选择的文件后缀不对,可以根据应用情况,在upload.js进行设置可允许的上传文件类型"));
}
else if (err.code == -602) {
console.log("\n这个文件已经上传过一遍了");
// document.getElementById('console').appendChild(document.createTextNode("\n这个文件已经上传过一遍了"));
}
else {
console.log("\nError xml:" + err.response);
// document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response));
}
}
}
});
uploader.init();
}
},
{
//这里只是演示可以通过在外部写html,然后进行展示
id: 'size',
type: 'html',
html: testHtml, //html写到了上面
commit: function (editor) {
var tt = document.getElementsByName('size'); //取radio选项
for (var i = 0; i < tt.length; i++) {
if (tt[i].checked) {
imgsize = tt[i].value;
break;
}
}
}
}
]
}
],
onShow: function () {
},
onOk: function () {
//该方法会依次调用element数组中的commit方法,在这里我们不需要在element中做额外调用,所以不使用,如果需要的话可以开启使用
//this.commitContent(editor);
//点击确定时,把图片依次插入
console.log(srcArray);
for (x in srcArray) {
var realImageSrc = "https://XXXXXXX/" + srcArray[x];
var ele = CKEDITOR.dom.element.createFromHtml('<p style="padding:5px 0;"><img style="width:250px;height:250px" src="' + realImageSrc + '"/></p><br/>');
editor.insertElement(ele); //将element插入editor
}
},
onCancel: function () {
}
}
});
2.4 html
在这里插入图片描述3. 效果图
在这里插入图片描述本来为大家录制了一段效果图,但是gif太大,上传不了,故作罢
四. CKEditor的ACF功能
CKEditor4有个叫做ACF的功能,能过滤掉一些标签,如script等,在前端就已经组织了大部分的我们觉得不需要的标签,提高了整个编辑器的安全性,但是CKEdor4不建议只用这个功能来做为安全性的校验,后台也应该做相应的校验。但是也是因为这个功能的存在,导致你在自定义插件的时候,可能会遇到一些莫名其妙的问题,如标签失效等,这个时候可以先把这个功能个关掉,等插件完成后再根据自己的需要打开。
官方对于ACF的解释:https://ckeditor.com/docs/ckeditor4/latest/guide/dev_acf.html
//disallowedContent: 'img{width,height,float}',
//extraAllowedContent: 'img[width,height,align]',
allowedContent: 'p abbr[title,id]',
网友评论