1. 程式人生 > >輕量級富文字編輯器wangEditor原始碼結構介紹

輕量級富文字編輯器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,選單按鈕如何解析,以及表情,地圖是如何實現的。時間有限,就不一一說明了,大家有興趣可以去看原始碼。

  最後還是歡迎大家多多指正!

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

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