zoukankan      html  css  js  c++  java
  • 轻量级富文本编辑器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="300px;"/><br />' +
                                            '标题:<input id="' + titleTxtId + '" type="text" style="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富文本编辑器

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

  • 相关阅读:
    js中的原生Ajax和JQuery中的Ajax
    this的用法
    static的特性
    时政20180807
    java compiler没有1.8怎么办
    Description Resource Path Location Type Java compiler level does not match the version of the installed Java project facet Unknown Faceted Project Problem (Java Version Mismatch)
    分词器
    [数算]有一个工程甲、乙、丙单独做,分别要48天、72天、96天完成
    一点感想
    解析Excel文件 Apache POI框架使用
  • 原文地址:https://www.cnblogs.com/wangfupeng1988/p/4492962.html
Copyright © 2011-2022 走看看