wangEditor——一款轻量级html富文本编辑器(开源软件)
从我发布wangEditor到现在,大概有七八个月了,随着近期增加的插入视频,表情,地图这三个功能,目前为止基本的功能已经大体完善了。这期间也修改了几个bug,都是各位网友反映的。至于程序是不是已经很稳定了,我不敢说。毕竟应用的人不是特别多,目前只有几十个关注wangEditor的人在应用。他们会偶尔提出一些bug,不过只要告诉我,我会第一时间解决,至少大家对我修改bug增加功能的速度和态度,还是比较认可的。
根据github记载,目前有105个commits,即我已经提交了105次代码更新,这个数量也会继续增加。大家有bug,有需求可以通过QQ群向我提交。
wangEditor.js源码目前2200多行,用书写文字书写博客的方式介绍它的结构,还真不是一件简单的事儿。所以,这里我就长话短说,尽量简单的介绍一下重点,不要搞的太罗嗦,否则大家最后会不耐烦的。
如果让我自己对这个源码的设计和架构做一个评价的话,我会打70分。它并不是完美的,但是它已经满足了我基本的需求。比方说,我最近新增的几个功能(插入视频,地图,表情)都是通过修改其中的配置项增加上去的,而没有改动源码中的核心部分。开放封闭原则——对扩展开放,对修改封闭,我想我已经基本做到了这一点。
最后,我分享wangEditor源码设计的目的,为的是让大家给一些意见。提出一些疑问,一些建议,或者我目前还没有意识到的一些问题。总之,我是希望这个软件越做越好。
wangEditor是一款jQuery插件,也是基于jquery开发的(不理解jquery插件的同学,请自行补课,本文不讲)。定义一个jquery插件其实很简单,wangEditor.js源码的最后几十行定义了。
//------------------------------------生成jquery插件------------------------------------ $.fn.extend({ /* * options: { * $initContent: $elem, //配置要初始化内容 * menuConfig: [...], //配置要显示的菜单(menuConfig会覆盖掉hideMenuConfig) * onchange: function(){...}, //配置onchange事件, * uploadUrl: 'string' //图片上传的地址 * } */ 'wangEditor': function(options){ if(this[0].nodeName !== 'TEXTAREA'){ //只支持textarea alert('wangEditor提示:请使用textarea扩展富文本框。详情可参见作者的demo.html'); return; } var options = options || {}, menuConfig = options.menuConfig, $initContent = options.$initContent || $('<p><br/></p>'), onchange = options.onchange, uploadUrl = options.uploadUrl; //获取editor对象 var editor = $E(this, $initContent, menuConfig, onchange, uploadUrl); //渲染editor,并隐藏textarea this.before(editor.$editorContainer); this.hide(); //页面刚加载时,初始化selection editor.initSelection(); return editor; } });
以上代码其实都很简单,就是接受一些配置项然后调用一个 $E 函数,返回一个 editor 对象,最后渲染到页面上。最关键的就是 $E 函数这一句话。
//获取editor对象 var editor = $E(this, $initContent, menuConfig, onchange, uploadUrl);
大家看这种方式是不是有点 var $div = $('div'); 的意思?——对了,这的设计我就是模仿着jquery来的。
上文中提到的 $E 函数是这样定义的。
//全局的构造函数 $E = function($textarea, $initContent, menuConfig, onchange, uploadUrl){ return new $E.fn.init($textarea, $initContent, menuConfig, onchange, uploadUrl); };
如上代码,其实构造函数是 $E.fn.init 。$E 只不过是一个入口,返回这个构造函数 new 出来的一个对象。
那么 $E.fn 是什么呢? ——它是 $E.prototype 的简写而已——好多js系统都喜欢这么干,我也就随着高大上一些啦!
//prototype简写为fn $E.fn = $E.prototype;
既然 $E.fn.init 是构造函数,那么它 new 出来的对象(即上文中的 editor)的原型要指向:$E.fn.init.prototype ,这样岂不是太长?不如来个简单一些的,将原型指向 $E.fn 吧。
$E.fn.init.prototype = $E.fn;
到了这里,没有看过jquery设计或者源码的人,一定觉得绕晕了——那是很正常的。我一开始接触jquery时,也是绕不过来。不过后来看多了,再后来自己用起来,还真觉得挺简单易用。大家在做自己的js代码时候,也不放试一试!
其实这里也是仿照jquery来设计的。在jquery中,函数都是 $ 的属性,例如 $.trim() ,对象函数都是 $.fn 的属性,例如 $('div').html() 的 html 方法就是 $.fn.html 定义的。
在wangEditor.js也一样。有许多工具函数(例如log输出,引号转译,url安全性检查等)都是 $E 的属性;许多对象函数(例如text,append,change等)都是 $E.fn 的属性。
为什么把函数定义在 $E.fn 上即可成为对象函数呢?——因为构造函数是 $E.fn.init ,而 $E.fn.init.prototype = $E.fn; 不知道大家明白了没有?
wangEditor目前有28个功能菜单,不可能为每一个菜单都写一遍执行代码。因为我们是面向对象的编程,我们是遵循“开放封闭原则”的设计。
还别说,在第一个版本中,我还真就是一个菜单写一遍执行代码,后来发现那样根本无法扩展。现在我的宗旨是:写一个菜单处理引擎(包括菜单初始化,页面弹出关闭,命令执行),菜单的扩展通过配置项实现。这个菜单处理引擎今天就不在本文讲解了,那块挺麻烦的,有时间再通过视频的方式跟大家分享吧。
首先,我们需要把所有的菜单归归类,否则如何确定配置项啊?我把所有的菜单分为4类:
下面是一个菜单按钮配置时的说明:
'menuId-1': { 'title': (字符串,必须)标题, 'type':(字符串,必须)类型,可以是 btn / dropMenu / dropPanel / modal, 'txt': (字符串,必须)fontAwesome字体样式,例如 'fa fa-head', 'style': (字符串,可选)设置btn的样式 'hotKey':(字符串,可选)快捷键,如'ctrl + b', 'ctrl,shift + i', 'alt,meta + y'等,支持 ctrl, shift, alt, meta 四个功能键(只有type===btn才有效) 'command':(字符串)document.execCommand的命令名,如'fontName';也可以是自定义的命令名,如“撤销”、“插入表格”按钮(type===modal时,command无效), 'dropMenu': ($ul,可选)type===dropMenu时,要返回一个$ul,作为下拉菜单, 'dropPanel':($div,可选)type===dropPanel是,要返回一个$div,作为弹出框 'modal':($div,可选)type===modal是,要返回一个$div,作为弹出框, 'callback':(函数,可选)回调函数, },
再配置一个菜单时,必须要遵守这个规则,否则解析引擎无法正确解析配置项。在此,为每个类型的菜单按钮,粘贴几个简单的配置项:
'fontFamily': { 'title': '字体', 'type': 'dropMenu', 'txt': 'icon-wangEditor-font', 'command': 'fontName ', 'dropMenu': function(){ var arr = [], //注意,此处commandValue必填项,否则程序不会跟踪 temp = '<li><a href="#" commandValue="${value}" style="font-family:${family};">${txt}</a></li>', $ul; $.each($E.styleConfig.fontFamilyOptions, function(key, value){ arr.push( temp.replace('${value}', value) .replace('${family}', value) .replace('${txt}', value) ); }); $ul = $( $E.htmlTemplates.dropMenu.replace('{content}', arr.join('')) ); return $ul; }, 'callback': function(editor){ //console.log(editor); } }, 'bold': { 'title': '加粗', 'type': 'btn', 'hotKey': 'ctrl + b', 'txt':'icon-wangEditor-bold', 'command': 'bold', 'callback': function(editor){ //console.log(editor); } }, 'foreColor': { 'title': '前景色', 'type': 'dropPanel', 'txt': 'icon-wangEditor-pencil', //如果要颜色: 'txt': 'fa fa-pencil|color:#4a7db1' 'style': 'color:blue;', 'command': 'foreColor', 'dropPanel': function(){ var arr = [], //注意,此处commandValue必填项,否则程序不会跟踪 temp = '<a href="#" commandValue="${value}" style="background-color:${color};" title="${txt}" class="forColorItem"> </a>', $panel; $.each($E.styleConfig.colorOptions, function(key, value){ var floatItem = temp.replace('${value}', key) .replace('${color}', key) .replace('${txt}', value); arr.push( $E.htmlTemplates.dropPanel_floatItem.replace('{content}', floatItem) ); }); $panel = $( $E.htmlTemplates.dropPanel.replace('{content}', arr.join('')) ); return $panel; } }, 'createLink': { 'title': '插入链接', 'type': 'modal', 'txt': 'icon-wangEditor-link', 'modal': function (editor) { var urlTxtId = $E.getUniqeId(), titleTxtId = $E.getUniqeId(), blankCheckId = $E.getUniqeId(), btnId = $E.getUniqeId(); content = '链接:<input id="' + urlTxtId + '" type="text" style="width:300px;"/><br />' + '标题:<input id="' + titleTxtId + '" type="text" style="width:300px;"/><br />' + '新窗口:<input id="' + blankCheckId + '" type="checkbox" checked="checked"/><br />' + '<button id="' + btnId + '" type="button" class="wangEditor-modal-btn">插入链接</button>', $link_modal = $( $E.htmlTemplates.modalSmall.replace('{content}', content) ); $link_modal.find('#' + btnId).click(function(e){ //注意,该方法中的 $link_modal 不要跟其他modal中的变量名重复!!否则程序会混淆 //具体原因还未查证??? var url = $.trim($('#' + urlTxtId).val()), title = $.trim($('#' + titleTxtId).val()), isBlank = $('#' + blankCheckId).is(':checked'), link_callback = function(){ //create link callback $('#' + urlTxtId).val(''); $('#' + titleTxtId).val(''); }; if(url !== ''){ //xss过滤 if($E.filterXSSForUrl(url) === false){ alert('您的输入内容有不安全字符,请重新输入!') return; } if(title === '' && !isBlank){ editor.command(e, 'createLink', url, link_callback); }else{ editor.command(e, 'customCreateLink', {'url':url, 'title':title, 'isBlank':isBlank}, link_callback); } } }); return $link_modal; } }
以上只是一些重点部分,其他的还有很多。例如富文本编辑器的核心技术:execCommand,如何支持IE6的fontIcon,菜单按钮如何解析,以及表情,地图是如何实现的。时间有限,就不一一说明了,大家有兴趣可以去看源码。
最后还是欢迎大家多多指正!
-------------------------------------------------------------------------------------------------------------
欢迎关注我的教程:《从设计到模式》《深入理解javascript原型和闭包系列》《css知多少》《微软petshop4.0源码解读视频》《json2.js源码解读视频》
也欢迎关注我的开源项目——wangEditor,轻量化web富文本编辑器
-------------------------------------------------------------------------------------------------------------