目前公司使用的富文本编辑器是百度的UEditor,目前其最新的版本停留在了16年的版本,基本算是无人维护的了,作为一个传统但是强劲的富文本编辑器,UEditor在初期的确是很多学生或者小型创业公司后台开发的首选富文本编辑器,但是随着业务的深入,UEditor的问题也暴露出来了。
- 代码结构复杂,无法很好地自定义和扩展(这个是很重要的原因,改起来真的很麻烦)
- bug多,不够稳定
- 样式太过老旧
富文本编辑器对于我们来说,要简单,够用就好了,太多花里胡哨的功能我们都不需要用到,参考过公众号和知乎的富文本编辑器,都是很简单的,虽然对对于我们做电商来说太过简单了,但是基本功能和样式就是我们想要的,我们完全可以在其基础上进行扩展。
另外个原因,我们的图片服务器是oss,所以需要通过富文本编辑器直传图片到oss中,直接在百度UEditor中,花了很大的力气接入了oss,虽然功能基本实现了,但是效果很不理想,无法所见即所得。
基于以上种种,我在市面上找了好几个编辑器,但是效果都不是很理想,接下来我列一下。
官网下载文件,下载界面可选:Basic Package、Standard Package、Full Package、Customize。根据自己需求下载不同的安装包,而且每种都有压缩版和源码版可选。其中Customize版本顾名思义可自定义选择自己需要的模块,官方也推荐使用这种方式自定义下载。Customize版本相当于在线上让你通过图形化界面自定义自己想要的插件和皮肤样式等,说实在,这个功能很强大。
CKEDITOR.replace('editor1', {
uiColor: '#9AB8F3'
});
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功能,不建议关闭
});
CKEditor4提供了强大的自定义插件功能,强烈建议根据它的官方文档中的4个demo一个个手敲一遍,而且一个都不要落下,敲完之后你会对CKEditor4的自定义插件有个更加深入的认识。因为公司用到的图片服务器是阿里云的oss,所以需要在编辑器中接入oss,针对图片都上传到oss中,我这边就简单介绍下如何自定义oss上传插件
CKEditor4规定自定义插件,必须要在plugins目录下新建一个插件同名文件夹,然后在该文件夹下有
plugin.js
(用于写插件的初始化等基本信息),dailog
文件夹,该文件夹下的js用于实现对话框的具体业务逻辑,icons
文件夹,该文件夹下有xxx.png的图片,作为插件的图标。
uploadimages
......dialog
.............uploadimage.js
.............upload.js
......icons
.............uploadimages.png
......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('The current date and time is232323232: ' + now.toString() + 'xcxzc
');
}
});
****/
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');
}
});
我们使用的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();
}
var srcArray = new Array();
var index = 0;
CKEDITOR.dialog.add('uploadimagesDialog', function (editor) {
var testHtml = new CKEDITOR.template('').output();
return {
title: '图片上传',
minWith: 400,
minHeight: 200,
//content是子标签
contents: [
{
id: 'ossimage',
label: '上传图片',
//elements是每个子标签下面的ui元素,如表单元素等
elements: [ //自定义弹窗的内容,可以使用模板,也可自定义html及样式
{
type: 'html', //图片上传成功后的容器
html: '',
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: '开始上传',
style: 'display:none;', //对应的样式
},
{
id: 'myimage', //选择图片按钮
type: 'html',
html: '', //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 +=
''
+ ''
+ ' ';
});
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 = '' + file.percent + "%";
// 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 = '';
}
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('![](' + realImageSrc + ')
');
editor.insertElement(ele); //将element插入editor
}
},
onCancel: function () {
}
}
});
2.4 html
3. 效果图
四. 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]',
五. 参考文档
https://liyang0207.github.io/2017/08/22/ckeditor自定义按钮及阿里oss上传/
https://ckeditor.com/docs/ckeditor4/latest/index.html