csdn本文鏈接:https://blog.csdn.net/u010004317/article/details/89513099
一. 前言
目前公司使用的富文本編輯器是百度的UEditor,目前其最新的版本停留在了16年的版本,基本算是無(wú)人維護(hù)的了,作為一個(gè)傳統(tǒng)但是強(qiáng)勁的富文本編輯器,UEditor在初期的確是很多學(xué)生或者小型創(chuàng)業(yè)公司后臺(tái)開(kāi)發(fā)的首選富文本編輯器,但是隨著業(yè)務(wù)的深入,UEditor的問(wèn)題也暴露出來(lái)了。
代碼結(jié)構(gòu)復(fù)雜,無(wú)法很好地自定義和擴(kuò)展(這個(gè)是很重要的原因,改起來(lái)真的很麻煩)
bug多,不夠穩(wěn)定
樣式太過(guò)老舊
富文本編輯器對(duì)于我們來(lái)說(shuō),要簡(jiǎn)單,夠用就好了,太多花里胡哨的功能我們都不需要用到,參考過(guò)公眾號(hào)和知乎的富文本編輯器,都是很簡(jiǎn)單的,雖然對(duì)對(duì)于我們做電商來(lái)說(shuō)太過(guò)簡(jiǎn)單了,但是基本功能和樣式就是我們想要的,我們完全可以在其基礎(chǔ)上進(jìn)行擴(kuò)展。
另外個(gè)原因,我們的圖片服務(wù)器是oss,所以需要通過(guò)富文本編輯器直傳圖片到oss中,直接在百度UEditor中,花了很大的力氣接入了oss,雖然功能基本實(shí)現(xiàn)了,但是效果很不理想,無(wú)法所見(jiàn)即所得。
基于以上種種,我在市面上找了好幾個(gè)編輯器,但是效果都不是很理想,接下來(lái)我列一下。
1. Editor.js
官網(wǎng):https://editorjs.io/
簡(jiǎn)介:該編輯器是以塊作為基本元素來(lái)的,每次編輯都是在一個(gè)塊上進(jìn)行編輯,主要的特點(diǎn)是,它的輸出是json格式的,而不是傳統(tǒng)的html格式,這是我非常喜歡的??勺远x插件,操作簡(jiǎn)單快捷。
建議:該編輯器適合業(yè)務(wù)不復(fù)雜的場(chǎng)景,以及只有pc端展示的場(chǎng)景,如寫(xiě)簡(jiǎn)單文章之類的,如果對(duì)于公司來(lái)說(shuō),需要從word編輯,需要提供移動(dòng)端做html展示(對(duì)于ios和安卓來(lái)說(shuō),他們只能通過(guò)html來(lái)做渲染,無(wú)法引入該編輯器),該編輯器則無(wú)法實(shí)現(xiàn)
2. NKeditor
簡(jiǎn)介:基于 kindeditor 進(jìn)行二次開(kāi)發(fā)的項(xiàng)目,文檔少,理解困難,這是產(chǎn)品找給我的
建議:不推薦用,沒(méi)文檔的開(kāi)源就是坑,而且還是二次開(kāi)發(fā)的。
3. wangEditor
官網(wǎng):http://www.wangeditor.com/
簡(jiǎn)介:這款是我在網(wǎng)上發(fā)現(xiàn)很多人推崇的,github的star數(shù)也有7000多,算是比較活躍的一款開(kāi)源編輯器,文檔算是比較齊全,我剛開(kāi)始也是覺(jué)得挺OK的,但是另外一個(gè)方面我覺(jué)得他的自定義插件功能不夠強(qiáng)大,我的項(xiàng)目對(duì)自定義插件的要求很高,所以放棄
建議:對(duì)自定義插件功能要求不高的,我推薦這款編輯器
4. CKEditor
- 官網(wǎng):https://ckeditor.com/ckeditor-4/ (CKEditor4地址)
https://ckeditor.com/ (CKEditor地址)
簡(jiǎn)介:CKEditor分CKEditor4和CKEditor5,因?yàn)镃KEditor5要用到npm打包,并且其缺少了一些我覺(jué)得很重要的功能,所以我選擇了CKEditor4,而且CKEditor5比較新,CKEditor4經(jīng)歷過(guò)了一段時(shí)間的考驗(yàn)。這也是在機(jī)緣巧合之下發(fā)現(xiàn)了這款編輯器,是國(guó)外開(kāi)源的一款編輯器,擁有自己的詳細(xì)的官網(wǎng),而且官網(wǎng)做得還不錯(cuò),github的社區(qū)也十分活躍,最近一次提交也是在一個(gè)月內(nèi),讓我看到了他的可靠性,至少在未來(lái)兩年內(nèi),該框架都會(huì)持續(xù)更新和優(yōu)化,從長(zhǎng)遠(yuǎn)來(lái)看,這都是其他編輯其所無(wú)法實(shí)現(xiàn)的。
他提供了多種編輯器樣式供選擇,而且文檔很詳細(xì),老外寫(xiě)的文檔,不得不說(shuō),真的很詳細(xì),雖然很長(zhǎng),像nginx,spring-boot的文檔一樣。而且文檔中有各種demo,并且demo也提供了源碼,給初學(xué)者提供了一個(gè)很好的入門(mén)機(jī)會(huì)。
建議:強(qiáng)烈推薦,只是是英文文檔,讀起來(lái)有點(diǎn)費(fèi)力,不過(guò)技術(shù)英文文檔估計(jì)是最簡(jiǎn)單的英文入門(mén)了,所用詞匯相對(duì)簡(jiǎn)單,再結(jié)合詞典就很容易理解了,不推薦把整個(gè)文檔翻譯成中文來(lái)看,先讀英文文檔,發(fā)現(xiàn)不懂的單詞再去查詞典,結(jié)合上下文理解他的語(yǔ)境。
二. 搭建基礎(chǔ)的CKEditor框架
1. 基本配置
官網(wǎng)下載文件,下載界面可選:Basic Package、Standard Package、Full Package、Customize。根據(jù)自己需求下載不同的安裝包,而且每種都有壓縮版和源碼版可選。其中Customize版本顧名思義可自定義選擇自己需要的模塊,官方也推薦使用這種方式自定義下載。Customize版本相當(dāng)于在線上讓你通過(guò)圖形化界面自定義自己想要的插件和皮膚樣式等,說(shuō)實(shí)在,這個(gè)功能很強(qiáng)大。
1. 1 引入js
<script src="../install package/ckeditor/ckeditor.js"></script>
1.2 寫(xiě)一個(gè)textarea標(biāo)簽
<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做得十分人性化,它不需要你去讀文檔,知道每個(gè)插件圖標(biāo)的的配置信息,然后去代碼中手動(dòng)添加或移除,他在full的文檔中提供了一個(gè)html頁(yè)面,可以通過(guò)圖形化界面自定義配置信息,該html的頁(yè)面在
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 //關(guān)閉acf功能,不建議關(guān)閉
});
三. CKEditor自定義插件
CKEditor4提供了強(qiáng)大的自定義插件功能,強(qiáng)烈建議根據(jù)它的官方文檔中的4個(gè)demo一個(gè)個(gè)手敲一遍,而且一個(gè)都不要落下,敲完之后你會(huì)對(duì)CKEditor4的自定義插件有個(gè)更加深入的認(rèn)識(shí)。因?yàn)楣居玫降膱D片服務(wù)器是阿里云的oss,所以需要在編輯器中接入oss,針對(duì)圖片都上傳到oss中,我這邊就簡(jiǎn)單介紹下如何自定義oss上傳插件
1. 文件結(jié)構(gòu)
CKEditor4規(guī)定自定義插件,必須要在plugins目錄下新建一個(gè)插件同名文件夾,然后在該文件夾下有
plugin.js(用于寫(xiě)插件的初始化等基本信息),dailog文件夾,該文件夾下的js用于實(shí)現(xiàn)對(duì)話框的具體業(yè)務(wù)邏輯,icons文件夾,該文件夾下有xxx.png的圖片,作為插件的圖標(biāo)。
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) {
//添加命令,最簡(jiǎn)單的自定義插件,可以在addCommand這個(gè)方法里面,通過(guò)定義exec,直接插入相應(yīng)的html即可
/*****
演示最簡(jiǎn)答的自定義插件,插入相應(yīng)的html,與本例子無(wú)關(guān)
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'
});
//添加對(duì)話框,配置實(shí)現(xiàn)對(duì)話框邏輯的js文件
CKEDITOR.dialog.add('uploadimagesDialog', this.path + 'dialog/uploadimages.js');
}
});
2.2. upload.js
我們使用的oss上傳是通過(guò)官方提供的
plupload.Uploader與后臺(tái)進(jìn)行交互獲取秘鑰等信息進(jìn)行上傳的,在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' 等信息的應(yīng)用服務(wù)器的URL,請(qǐng)將下面的IP和Port配置為您自己的真實(shí)信息。
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()
{
// 可以判斷當(dāng)前expire是否超過(guò)了當(dāng)前時(shí)間, 如果超過(guò)了當(dāng)前時(shí)間, 就重新取一下,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', //讓服務(wù)端返回200,不然,默認(rèn)會(huì)返回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是子標(biāo)簽
contents: [
{
id: 'ossimage',
label: '上傳圖片',
//elements是每個(gè)子標(biāo)簽下面的ui元素,如表單元素等
elements: [ //自定義彈窗的內(nèi)容,可以使用模板,也可自定義html及樣式
{
type: 'html', //圖片上傳成功后的容器
html: '<div id="ossfile"</div>',
style: '', //對(duì)應(yīng)的樣式
onShow: function () {
//在每次彈窗打開(kāi)的時(shí)候都會(huì)調(diào)用該方法
},
//點(diǎn)擊確定按鈕時(shí),在onOK中調(diào)用commitContent,會(huì)依次觸發(fā)element的commit方法
commit: function (editor) { //點(diǎn)擊確定按鈕時(shí),將圖片src傳入全局src中
src = $('.imgbox img').attr('src');
},
//點(diǎn)擊確定按鈕時(shí),在onOK中滴啊用superContent,會(huì)依次觸發(fā)element的setup方法
setup: function (editor) {
}
},
{
type: 'html', //圖片上傳成功后的容器
html: '<a id="postfiles" href="javascript:void(0);" class="btn">開(kāi)始上傳</a>',
style: 'display:none;', //對(duì)應(yīng)的樣式
},
{
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 () { //當(dāng)該元素show的時(shí)候執(zhí)行的方法
document.getElementById('ossfile').innerHTML = '';
},
onLoad: function () {
//uploader需要再onLoad方法中定義,因?yàn)橹挥性趏nload的時(shí)候,才能獲取到‘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 //不允許選取重復(fù)文件
},
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);
},
//上傳中,這里根據(jù)需要自己寫(xiě)上傳等待,也可在外部實(shí)現(xiàn)
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);
//記?。哼@里要根據(jù)imageSrc最后的圖片名字進(jìn)行從小到大排序,即0.png,1.png這樣子依次插入到數(shù)組中人,然后再根據(jù)輸入插入到富文本編輯器中
//因?yàn)閛ss上傳針對(duì)同個(gè)文件名不能上傳多次,所以在upload.js要針對(duì)文件名進(jìn)行有規(guī)律地自定義
//上傳成功之后顯示圖片縮略圖
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訪問(wèn)用戶設(shè)置的上傳回調(diào)服務(wù)器失敗,失敗原因是:' + info.response;
}
else {
document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response;
}
},
Error: function (up, err) {
if (err.code == -600) {
console.log("\n選擇的文件太大了,可以根據(jù)應(yīng)用情況,在upload.js 設(shè)置一下上傳的最大大小")
// document.getElementById('console').appendChild(document.createTextNode("\n選擇的文件太大了,可以根據(jù)應(yīng)用情況,在upload.js 設(shè)置一下上傳的最大大小"));
}
else if (err.code == -601) {
console.log("\n選擇的文件后綴不對(duì),可以根據(jù)應(yīng)用情況,在upload.js進(jìn)行設(shè)置可允許的上傳文件類型")
// document.getElementById('console').appendChild(document.createTextNode("\n選擇的文件后綴不對(duì),可以根據(jù)應(yīng)用情況,在upload.js進(jìn)行設(shè)置可允許的上傳文件類型"));
}
else if (err.code == -602) {
console.log("\n這個(gè)文件已經(jīng)上傳過(guò)一遍了");
// document.getElementById('console').appendChild(document.createTextNode("\n這個(gè)文件已經(jīng)上傳過(guò)一遍了"));
}
else {
console.log("\nError xml:" + err.response);
// document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response));
}
}
}
});
uploader.init();
}
},
{
//這里只是演示可以通過(guò)在外部寫(xiě)html,然后進(jìn)行展示
id: 'size',
type: 'html',
html: testHtml, //html寫(xiě)到了上面
commit: function (editor) {
var tt = document.getElementsByName('size'); //取radio選項(xiàng)
for (var i = 0; i < tt.length; i++) {
if (tt[i].checked) {
imgsize = tt[i].value;
break;
}
}
}
}
]
}
],
onShow: function () {
},
onOk: function () {
//該方法會(huì)依次調(diào)用element數(shù)組中的commit方法,在這里我們不需要在element中做額外調(diào)用,所以不使用,如果需要的話可以開(kāi)啟使用
//this.commitContent(editor);
//點(diǎn)擊確定時(shí),把圖片依次插入
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. 效果圖
本來(lái)為大家錄制了一段效果圖,但是gif太大,上傳不了,故作罷
四. CKEditor的ACF功能
CKEditor4有個(gè)叫做ACF的功能,能過(guò)濾掉一些標(biāo)簽,如script等,在前端就已經(jīng)組織了大部分的我們覺(jué)得不需要的標(biāo)簽,提高了整個(gè)編輯器的安全性,但是CKEdor4不建議只用這個(gè)功能來(lái)做為安全性的校驗(yàn),后臺(tái)也應(yīng)該做相應(yīng)的校驗(yàn)。但是也是因?yàn)檫@個(gè)功能的存在,導(dǎo)致你在自定義插件的時(shí)候,可能會(huì)遇到一些莫名其妙的問(wèn)題,如標(biāo)簽失效等,這個(gè)時(shí)候可以先把這個(gè)功能個(gè)關(guān)掉,等插件完成后再根據(jù)自己的需要打開(kāi)。
官方對(duì)于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]',