最近在做富文本相關的探索,看了下wangEditor和draft.js,draft.js相對複雜一些吧,wangEditor的代碼就比較直白,註釋比較多,比較適合閱讀,大體討論一下wangEditor的結構。版本是v3.1.1,講下大概原理,不會每個功能都講。
先看使用方式,
var E = require('wangeditor');
var editor = new E('#editor');
editor.create()
// 自己定義分開的工具欄和編輯區
var editor1 = new E('#div1', '#div2')
代碼大體目錄結構如下:
-
/src/js/editor中是編輯器的構造函數和一些初始化
-
/src/js/util中是各種輔助功能,其中有一個dom-core.js文件,封裝了一些dom方法並導出爲$,可以稍微看一下,避免後面看其他代碼被原生方法和封裝的方法弄混
-
/src/js/command中封裝了document的execCommand、queryCommandValue、queryCommandState、queryCommandSupported等方法,是改變編輯區內文本樣式的核心方法
-
/src/js/menus中是支持的功能的實現,比如加粗、斜體、字體顏色等
-
/src/js/selection中封裝了和選區有關的方法,也屬於核心方法
-
/src/js/text中是編輯區的事件綁定,及文本內容相關的代碼
原理
主要原理是使用contenteditable屬性,配合selection、range、execCommand等API,實現富文本編輯功能,這裏的富文本是指html標籤和css樣式。
實現
我們先來看/src/js/editor/index.js,首先定義了一個editorId變量用來維護一個頁面上存在多個編輯器的情況。構造函數本身沒幹什麼事情,記錄了toolbarSelector、textSelector這兩個變量,分別是工具欄和編輯區的選擇器,構造出來以後調用create函數,看一眼create函數幹了什麼,一堆的函數調用,挨個進去看。
_initConfig
,把用戶自定義的設置項和默認設置項merge好,_initDom
,把編輯區和工具欄的dom創建出來,還處理了一些事件,比如輸入法的開始和結束事件,焦點事件,其中處理了點擊工具欄時,不認爲失去編輯區的焦點這一邏輯,這個函數裏掛到editor實例上的變量有$toolbarElem工具欄dom, $textContainerElem編輯區容器, $textElem編輯區dom, toolbarElemId工具欄dom的id, textElemId編輯區dom的id
,可以看到變量比較多比較亂,對後面理解相關代碼也造成了一定麻煩_initCommand、_initSelectionAPI、_initUploadImg、_initMenus、_initText
這些函數都是將相應的模塊創建出來,看到具體的模塊時再說,這些模塊都掛到了editor的實例上面,有this.cmd、this.selection、this.uploadImg、this.menus、this.txt這些- initSelection,初始化選區,將光標定位到內容尾部(代碼註釋原文)
_bindEvent
,綁定事件,包含節流的this.change,還有一些事件是通過config引入的
接下來看看各個模塊分別做了哪些事情。所有模塊都會在構造函數中把editor實例引入。
selection
獨立的模塊,不依賴其他模塊,有一個_currentRange
變量,保存了當前的選區,並提供了getRange函數,來將當前選區暴露給其他模塊使用。另外還有如下函數:
- saveRange,如果選區在編輯區內的,則將選中的區域設置爲
_currentRange
。 - collapseRange摺疊選區
- getSelectionText獲取選區內容並轉換爲文字
- getSelectionContainerElem返回選區內的元素節點
- getSelectionStartElem、getSelectionEndElem獲取選區中起始和結束的元素節點
- isSelectionEmpty,判斷選區是否爲空
- restoreSelection,把當前選區裏的range都移除,並設置爲
_currentRange
- createEmptyRange,創建一個空的選區
- createRangeByElem,使用元素創建選區
如果不瞭解selection和range API的話,建議先看一看文檔
command
同樣是獨立的模塊,command模塊的函數有如下這些:
- do,包含自定義事件和瀏覽器支持的命令的執行,執行完畢命令後立刻更新菜單區域的狀態,並調用selection模塊的saveRange和restoreSelection函數,這兩個函數會多次遇到,並且多是成對出現,最後觸發editor的change函數
_insertHTML
和_insertElem
,插入html和節點_execCommand
、queryCommandValue、queryCommandState、queryCommandSupported,對document對應方法的包裝
text
除了菜單的內容外,主要的編輯器的邏輯,比editor的代碼還多
init函數調用了_bindEvent
函數來綁定事件,和editor裏的風格一樣,一堆事件,
-
_saveRangeRealTime
內部寫了一個saveRange函數,並綁定了keyup、mousedown、mouseup事件,mousedown和mouseup主要是用來處理在編輯區域內按下鼠標,然後移動到了編輯區域外的情況 -
_enterKeyHandle
按下回車時的處理,keydown綁定了對code標籤的處理,主要是在code標籤中連續回車兩次的話,就跳出code塊,keyup綁定了對p標籤的處理,主要處理了回車換行插入一個p標籤 -
_clearHandle
,主要處理按下backspace時的事件,因爲使用的是div標籤作爲編輯器的根節點,所以保留一個p標籤作爲新創建的一行,如果沒有這個p標籤,回車創建出來的都是div標籤,這個特性應該是瀏覽器的實現 -
_pasteHandle
,處理粘貼事件,裏面有個canDo比較奇怪,定時100ms來判斷是否可以執行粘貼,獲取粘貼的內容的函數都在util/paste-handle.js裏 -
_dragHandle
,拖曳事件,如果拖曳的內容包含文件,調用上傳函數
看到這裏爲止其實大部分實現我們都已經看完了,你可能覺得我說了一堆廢話,因爲很多內容代碼的註釋裏都是有的,並且說的零零碎碎,不成體系,那麼我們來梳理一下,當你使用富文本編輯器的時候,會有哪些操作,首先肯定是輸入文本,然後選中了其中的某一段,點擊了加粗或者斜體或者顏色等菜單,然後被選中的那些文本按照菜單功能展示了,然後可能還有插入圖片,插入鏈接這些稍微高級一些的操作,還有複製粘貼這些操作等。
文本的輸入由contenteditable屬性處理了,我們註冊了一些事件處理一些比較特別的按鍵,比如回車和刪除,都在text裏;選擇某段文字,這個在selection裏;加粗、斜體、顏色這些,一部分在command裏,一部分在menus裏,接下來我們來看menus。
menus
menus裏有很多文件夾,backColor、bold等等,都是editor所支持的菜單功能,在menu-list.js裏引用了全部這些菜單項(😂),index裏引用了menu-list,把菜單項一個個渲染出來,並綁定點擊事件,這麼說完全沒感覺,來看一個菜單項,backColor,構造函數裏創建了一個droplist用來選擇顏色,然後onClick裏調用_command
方法,_command
裏執行的就是execCommand,感覺還是沒說什麼(😂)
總結
這篇源碼解析怎麼說呢,寫得有點難受,感覺一直在說些沒必要說的話,自己去看的話,很多邏輯註釋裏都寫了,但是如果不說呢,第一次看又會稍微有點點亂,因爲各個模塊之間並沒有太強的關聯,也沒有什麼流程,而是以事件觸發的形式來觸發各種處理函數,這篇文章寫的也只能作爲一個看過這個源碼的記錄而已。如果真的要用它,還是自己去看一看