wangEditor——一款輕量級html富文本編輯器(開源軟件)javascript
從我發佈wangEditor到如今,大概有七八個月了,隨着近期增長的插入視頻,表情,地圖這三個功能,目前爲止基本的功能已經大致完善了。這期間也修改了幾個bug,都是各位網友反映的。至於程序是否是已經很穩定了,我不敢說。畢竟應用的人不是特別多,目前只有幾十個關注wangEditor的人在應用。他們會偶爾提出一些bug,不過只要告訴我,我會第一時間解決,至少你們對我修改bug增長功能的速度和態度,仍是比較承認的。css
根據github記載,目前有105個commits,即我已經提交了105次代碼更新,這個數量也會繼續增長。你們有bug,有需求能夠經過QQ羣向我提交。html
wangEditor.js源碼目前2200多行,用書寫文字書寫博客的方式介紹它的結構,還真不是一件簡單的事兒。因此,這裏我就長話短說,儘可能簡單的介紹一下重點,不要搞的太羅嗦,不然你們最後會不耐煩的。java
若是讓我本身對這個源碼的設計和架構作一個評價的話,我會打70分。它並非完美的,可是它已經知足了我基本的需求。比方說,我最近新增的幾個功能(插入視頻,地圖,表情)都是經過修改其中的配置項增長上去的,而沒有改動源碼中的核心部分。開放封閉原則——對擴展開放,對修改封閉,我想我已經基本作到了這一點。node
最後,我分享wangEditor源碼設計的目的,爲的是讓你們給一些意見。提出一些疑問,一些建議,或者我目前尚未意識到的一些問題。總之,我是但願這個軟件越作越好。jquery
wangEditor是一款jQuery插件,也是基於jquery開發的(不理解jquery插件的同窗,請自行補課,本文不講)。定義一個jquery插件其實很簡單,wangEditor.js源碼的最後幾十行定義了。git
//------------------------------------生成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 函數這一句話。github
//獲取editor對象 var editor = $E(this, $initContent, menuConfig, onchange, uploadUrl);
你們看這種方式是否是有點 var $div = $('div'); 的意思?——對了,這的設計我就是模仿着jquery來的。web
上文中提到的 $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富文本編輯器
-------------------------------------------------------------------------------------------------------------