轻量级富文本编辑器wangEditor源码结构介绍

1. 引言

  wangEditor——一款轻量级html富文本编辑器(开源软件)

  从我发布wangEditor到现在,大概有七八个月了,随着近期增加的插入视频,表情,地图这三个功能,目前为止基本的功能已经大体完善了。这期间也修改了几个bug,都是各位网友反映的。至于程序是不是已经很稳定了,我不敢说。毕竟应用的人不是特别多,目前只有几十个关注wangEditor的人在应用。他们会偶尔提出一些bug,不过只要告诉我,我会第一时间解决,至少大家对我修改bug增加功能的速度和态度,还是比较认可的。

  

  根据github记载,目前有105个commits,即我已经提交了105次代码更新,这个数量也会继续增加。大家有bug,有需求可以通过QQ群向我提交。

2. 介绍源码结构

  wangEditor.js源码目前2200多行,用书写文字书写博客的方式介绍它的结构,还真不是一件简单的事儿。所以,这里我就长话短说,尽量简单的介绍一下重点,不要搞的太罗嗦,否则大家最后会不耐烦的。

  如果让我自己对这个源码的设计和架构做一个评价的话,我会打70分。它并不是完美的,但是它已经满足了我基本的需求。比方说,我最近新增的几个功能(插入视频,地图,表情)都是通过修改其中的配置项增加上去的,而没有改动源码中的核心部分。开放封闭原则——对扩展开放,对修改封闭,我想我已经基本做到了这一点。

  最后,我分享wangEditor源码设计的目的,为的是让大家给一些意见。提出一些疑问,一些建议,或者我目前还没有意识到的一些问题。总之,我是希望这个软件越做越好。

3. 一个jQuery插件

  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来的。

4. 仿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代码时候,也不放试一试!

5. 工具函数 & 对象函数

  其实这里也是仿照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;  不知道大家明白了没有?

6. menu配置项

  wangEditor目前有28个功能菜单,不可能为每一个菜单都写一遍执行代码。因为我们是面向对象的编程,我们是遵循“开放封闭原则”的设计。

  还别说,在第一个版本中,我还真就是一个菜单写一遍执行代码,后来发现那样根本无法扩展。现在我的宗旨是:写一个菜单处理引擎(包括菜单初始化,页面弹出关闭,命令执行),菜单的扩展通过配置项实现。这个菜单处理引擎今天就不在本文讲解了,那块挺麻烦的,有时间再通过视频的方式跟大家分享吧。

  首先,我们需要把所有的菜单归归类,否则如何确定配置项啊?我把所有的菜单分为4类:

  • command类型:点击按钮即可执行命令,如“粗体”,“下划线”
  • dropMenu类型:点击按钮弹出下拉menu,再选择命令。如“字体”,“字号”
  • dropPanel类型:点击按钮弹出panel,再选择命令。如“背景色”,“表情”
  • modal类型:点击按钮弹出对话框,需要填写内容,再执行命令。如“插入图片”,“插入地图位置”

  下面是一个菜单按钮配置时的说明:

'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">&nbsp;</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;
                    }
                }

7. 总结

  以上只是一些重点部分,其他的还有很多。例如富文本编辑器的核心技术:execCommand,如何支持IE6的fontIcon,菜单按钮如何解析,以及表情,地图是如何实现的。时间有限,就不一一说明了,大家有兴趣可以去看源码。

  最后还是欢迎大家多多指正!

-------------------------------------------------------------------------------------------------------------

欢迎关注我的教程:从设计到模式深入理解javascript原型和闭包系列》《css知多少》《微软petshop4.0源码解读视频》《json2.js源码解读视频

也欢迎关注我的开源项目——wangEditor,轻量化web富文本编辑器

-------------------------------------------------------------------------------------------------------------

 

posted @ 2015-05-11 08:24  王福朋  阅读(15448)  评论(24编辑  收藏  举报