為什麼都說富文本編輯器是天坑?

富文本編輯器到底坑在什麼地方?

前端開發造這個輪子是否有必要性?


我說個最簡單的例子吧,這個情況

里,游標究竟是在「??????????」的前面(England 的後面),還是在「??????????」的後面呢?(希伯來文是從右向左排版的)

能把這些邊角情況都考慮到的 WYSIWYG 編輯器,全世界一隻手都數得過來。Word 代碼那麼大不是沒有理由的。


很久之前的問題,我幫忙截取幾頁前同事的ppt @悲劇 當時負責開發核心部分。多圖預警,手機看的謹慎點入。

這僅僅是冰山一角而且只是一些比較可以說的點拿出來,具體實現有多複雜,代碼有多xx,自己腦補吧。有編輯器相關的或者ppt的疑問可以找 @悲劇同學解答啊,現在再看這個ppt,我都有點繞不懂了,真心覺得那半年的活沒白乾,可惜現在剩下的也就只有ppt了。不過你要是寫過一個這個玩意,什麼js的功能在你眼裡基本都是渣渣了。。至少邏輯方面提升好幾個level。。


我是wangEditor - 輕量級web富文本編輯器的作者。

作為一個在web富文本編輯器貢獻了1年半時間的非菜鳥人員,我來談談我的感受。說到這個話題,我感受特別多,也特別亂。因此,想到哪裡就寫到哪裡吧。

2014年11月開始,我是想著練練手,就綳著一股勁寫了1000多行的代碼,提交到github,就有了最初的wangEditor編輯器,那時候的樣子,完全不符合上述的『工業化』這個標準,無論你對『工業化』這個詞的要求有多低。說白了,就是一個簡單的div增加contenteditable屬性,然後用瀏覽器原生的execCommand執行命令,要不然代碼怎麼會那麼少呢?現在想起來,那時候談什麼穩定性、兼容性,那就是妄想了。

後來,經過了大約兩次代碼重構,直到2015年夏天那會兒,總算是有了好轉——只是『好轉』啊,離著『工業化』的要求還是有一段距離。此時的編輯器,在代碼上比較清晰,結構也比較穩定,我可以自己去靈活的擴展一些用戶提出來的功能。但是那時候在功能層級和用戶自定義擴展層級上,還是遠遠不夠。第一,用戶無法自定義菜單和插件,有啥問題只能我來加。第二,現在的功能穩定級別,緊緊是在於用戶完全沒有特殊操作和特殊需求的時候穩定,例如,用戶要是ctrl+a全選內容然後刪除,這時再寫入內容就會有問題,再例如,用戶粘貼一段文字也會有各種問題,再例如,用戶引用了一段內容之後無法通過兩次enter跳出……再多了我就不說了

但是,到那時候通過我一年多的努力,QQ群里有了幾百個關注著,也有人真正的開始嘗試用我的這款產品。然後吸引大家來的,不是功能多麼多麼強大,而是靠漂亮的UI——不要見笑!

到2015年的冬天,將近春節一個多月的時候,我蓄勢待發,重構代碼,開發2.0版本。因此此時,我已經在編輯器這個領域混了1年多了,見過了各種各樣用戶的需求,研究了網路上所有看得見的競品,我現在已經知道了用戶到底需要一款什麼樣子的編輯器,用戶的需求在哪裡、問題在哪裡、痛點在哪裡。

大約2周的事件來開發,春節前發布了基礎版本,直到現在春節後剛上班沒幾天,這段時間裡,我又做了5次小版本升級,基礎版基本穩定。接下來,我還要繼續升級,做一些更加符合用戶需求的功能,例如集成七牛雲存儲的圖片上傳、國際化、標準的表情包、集成第三方上傳插件等等。

另外,我今年還會將移動端集成進來,做成響應式、支持手機pad的編輯器。

總結來說,web富文本編輯器——沒進來的人覺得沒啥大不了的,開源插件那麼多,何必重做輪子。真正進來的人,會發現這東西真的可做的東西特別多。

共勉!


主要是對 DOM 的處理存在兼容性問題,所以要實現一個功能完備的富文本編輯器非常麻煩。

現在的 WYSIWYG 主要用 contentEditable 來實現,例如要對選中的文本進行操作(如加粗、字型大小),需要先判斷游標的位置,用 Range 判斷選中的文本在哪裡,然後判斷這段文本是不是已經被處理過,需要覆蓋、去掉還是保留原效果,這裡還有可能涉及到標籤的嵌套,例如

&aa&a&&&bbbb&cccc

如果用戶選擇了 abbbbcc 這段文字加粗,應該怎麼做?如果用戶跨節點邊界選擇文本按了退格鍵,表現出的行為很大可能和你的預期是不一樣的,光是處理這其中的邏輯就已經很複雜了。還不用說要實現像撤銷這樣的操作必須手動進行 history 棧的管理、對用戶的輸入進行轉義、拖拽上傳操作、Latex 公式支持之類。


update: 2017-10-13

在過去的半年多,所開發的編輯器有了新的變化

  1. 自實現游標/選區
  2. 拋棄了對contenteditable 特性的依賴 (參考 https://drive.googleblog.com/2010/05/whats-different-about-new-google-docs.html)

相關技術發表在 2017-10 期 《程序員》雜誌, 詳見 《程序員》10月精彩內容:iOS Android 10 年

(後轉載在有道技術團隊的公眾號 http://mp.weixin.qq.com/s/9gDI1r9aAu6dHJhXg34eIg )

update: 2017-01-02 Mon

過去一年, 所開發的編輯器有了新變化

  1. 數據層與視圖層分開
  2. 自實現所有操作
  3. 協同支持

下面分享一下詳情.

在過去, 編輯器操作的數據和展現給用戶的視圖層是同一份HTML/DOM. 眾所周知, 樹狀數據不如線性數據數據好處理. HTML 是樹狀結構的, 能不能使用線性結構來表示呢?

最後我們將所有數據分為塊狀(Block) 和 行內(Inline)數據, 內容由若干個塊(Block)數據組成, 每個塊(Block)由行內(Inline)數據組成.

比如說, 一段文字是一個段落(paragraph) , 屬於塊狀數據, 該段落包含

  • block-styles: 描述paragraph 的樣式, 比如行高, 整體縮進
  • text: 純文本內容
  • inline-styles: 描述text中每個字的樣式, 比如字體, 字型大小, 顏色, 背景色.

在內存時, 使用Paragraph 對象管理段落及其樣式(block-styles). text 和 inline-styles 則轉換成一個RichText 對象, 而 RichText 由若干RichChar 組成.

Paragraph 最後會生成html, 用於視圖層的展現給用戶.

這樣, 數據層和視圖層就分開了, 總體如下圖:

支持各類Block:

當我們需要將部分文字加粗時,只要將執行對應的操作(Command 對象表示), 操作會將對應的RichChar串加粗. 當Paragraph對象監聽到內容/樣式有變化時, 會通知對應的ParagraphView 更新視圖.

每個操作都會實現自身的Undo/Redo 方法, 進而實現整體的undo/redo.

每個Command都會計算執行完的Range(NoteRange/BlockRange),

NoteSelection 對象通知NoteView繪製range, 這樣, 用戶就看到了操作完後的游標位置.

當協同編輯時, 我們將Command 序列化, 分發給各個客戶端, 進過衝突處理後, 讓編輯器執行來自server 的Command.

這套架構比舊編輯器的架構好很多, 也一直在填坑.

---------------------------------------------以下是原回答--------------------------------------------------

根據個人的開發體驗, 感覺有以下方面的坑

  1. DOM
  2. Range/Selection
  3. 編輯器本身

# DOM

考慮到瀏覽器兼容, 前端開發一般都會使用jQuery等類庫屏蔽常用DOM操作的差異. 然而, 開發編輯器會經常常遇到jQuery無法處理或者使用jQuery處理不方便的情形, 需要開發者裸寫JavaScript. 一裸寫JavaScript, 就會碰到DOM API的各種坑.

再者, 編輯器中數據一般都是html, 轉換為DOM樹後被編輯器分析處理. 操作樹狀的數據比較麻煩, 遠不如操作線性數據(如字元串, 數組)方便.

# Range/Selection

在常見的前端開發中, Range/Selection的概念基本上遇不到. 做編輯器, 需要理解Range/Selection 和 熟練運用Range/Selection 的操作. (個人在做了編輯器半年左右,才對Range/Selection 有感性的認識)

Range/Selection 的瀏覽器兼容性也是一個很大的坑.

# 編輯器本身

俗話說, 麻雀雖小, 五臟俱全. 編輯器就是一個這樣的麻雀, 需要考慮:

  1. 撤銷(undo)/重做(redo)
  • 如果使用contenteditable 特性自帶的undo/redo, 那麼, 對內容(html)的所有操作都必須使用document.execCommand() 完成, 否則就會破壞內部的undo/redo棧, undo/redo 功能就會不正常.
  • 而且, document.execCommand() 能做的事情太少, 那麼自己開發一套undo/redo棧吧
  1. Command
  • 由於 document.execCommand() 能做的事情太少, 而且提供的命令是也存在bug, 加之瀏覽器實現也不一樣, 那麼自己封裝/開發一套Command 系統. 好複雜呀,各種命令得寫一遍, 自己實現的也有bug.
  1. 格式過濾/轉化
  • 設置編輯器內容和paste粘貼時, 需要過濾內容, 因為html靈活, 奇葩標籤什麼的都可能存在.
  • 獲取內容和往外copy時, 要輸出標準html格式, 需要過濾編輯器內部信息
  1. 工具欄/自實現右鍵菜單
  • 並非接受click事件的按鈕, 還需要狀態管理
  1. 除文字的編輯外, 還需要考慮圖片,表格等特殊格式.
  • 圖片做個縮放操作吧, 能拖動縮放的那種...
  • 表格最好也能調整行列大小吧... (需求一波波的)
  1. 支持一下代碼高亮吧, 我看挺容易的
  • 使用什麼實現代碼高亮? 自己寫?靠, 不太會, 使用開源的吧. 哎呦,偶爾有bug
  • 只高亮沒有問題, 但也要保證編輯沒有問題
  1. 支持一下 LaTeX 公式編輯吧
  2. 支持一下 Markdown 吧
  3. 能支持多人一起編輯就好了, 就像 Google Doc 那樣...
  4. 表格要是能支持Excel的公式計算就好了, 不要全部的, 常用的就行...
  5. Mobile First, Mobile First, 要適配移動web !!! 不,我們也要做成Hybrid的, 內嵌於App中
  • iPhone: 嵌入我這邊, iOS 5 就支持 contenteditable 了
  • Android: 來吧, 4.4 以上支持 contenteditable 了
    • 扒開一看, 哇塞, 一堆問題, 不是說好支持的么...???
    • 我是國產的, 對WebView 做了定製/升級/優化 .... (?_?)
    • 微信: 我的平台多牛逼, 還有JS SDK, 竟然不適配我...
  1. ...

本來想好好答的...(說多了都是淚呀)

一句話吧:

  • 做一件事還是比較容易的, 做好一件事很難.


如果使用contentEditable屬性,就會出現其他答主所說的一系列問題。所以你可以看到但凡複雜的編輯器都是自己實現類似contentEditable功能的,比如Atom、Visual Studio Code、Caret,都是自己用div + 事件監聽來維護一套編輯器狀態,包括游標都是自己用div實現的。所以不管使用什麼方式,都是很複雜,很坑的。


其實還好啦,GDI可以用Uniscribe,D2D可以用DirectWrite,網頁自己都做好排版了,其實給中國人使用的話隨手寫一個就可以了,要賣給綠綠的時候再說。你看我國很多網遊的UI輸入綠文就是直接傻逼的,不也沒理。

不過我給gayui做richedit的時候還是測試了一下綠文,雖然不知道綠綠的編輯習慣是怎麼樣的,但是至少我對照了word的列印結果,每一個曲線都是吻合的,我姑且就認為沒問題(逃


我說一點和富文本編輯器相關的事情。

背景

  • 幾年前做過一點點複雜文本排版的事情, 當時Android應該是2.0或者2.2版本,還不支持複雜的文字(比如阿拉伯文這種從右到左的文字),我當時所在的公司MTK開始重視Android系統,原來的feature Phone 在中東賣得不錯,也希望能把Android改造成能支持阿拉伯文展示, 同時也要支持印度文。

  • 當時開發團隊兩個人,我加上一個台灣的一位女工程師,台灣那邊負責skia文字繪製這塊的改造。我負責上層android framework的改造, 前後應該開發有不到3個月的時間,當時基本上是7*14的節奏,非常辛苦(主要也是對這塊完全沒有積累)。台灣那邊的工程師會定期出差到北京這邊。

具體工作

  1. 通讀android.text/textview裡面的代碼,代碼並不好讀,原來Android都textview就支持簡單的富文本展示,包括支持圖片的展示,這樣文本在真正繪製之前就有一個很複雜的分析過程,計算每一段的屬性(這樣每一段就可以直接調用skia來繪製),如何換行,計算行高等。裡面最複雜的一個函數我記得有29個輸入參數。整個textview + android.text包加起來也有差不多1.5w行代碼的規模。
  2. 了解unicode裡面的一些關鍵概念和演算法, 比如bidi演算法(支持R2L語言), 這個演算法實際上是台灣的工程師負責在skia那一層進行實現, 但是需要對這個演算法有非常好的理解,還有char, glyph的關係等等,當時是看了很多資料,包括看了幾本unicode相關的書籍。
  3. 閱讀開源代碼的實現, 主要是參考pango(http://www.pango.org/),同時和feature Phone做文字排版的同事有一些溝通,弄清楚了整個排版以及游標處理的流程,游標處理很重要,這個用來支撐點選以及文字範圍的選擇。具體游標的處理其實是得到x, y位置之後怎麼轉換到對應字元,這個其實文字繪製的反過程,要考慮到R2L文字,不是很簡單,而且移動游標也是要考慮R2L文字。
  4. coding,過程還是比較艱辛的,基本上是把pango裡面最核心的十幾個對文字排版,游標處理的函數裁剪到Android的環境, 並對android.text進行了大量的修改。當時為了確保正確,還編寫了非常多的測試用例。

最後說一下測試過程。

  • 當時這個需求的測試分布在台灣,印度(當時MTK在印度有分公司)以及北京。主要是印度這邊來負責測試,基本上都是錄製視頻的方式來提bug,我這邊需要通過通過輸入法輸入一樣的文字或者通過copy一份文字輸入進去來複現bug,有時候還需要通過內部IM工具進行溝通, 處理過不少游標不對的bug。
  • 解決過的比較麻煩的一個bug是台灣的測試提出來的,手機在台灣的網路下面會收到某種信息需要展示(不是簡訊,具體網路那塊我也不太懂),在北京這邊沒有辦法復現,我只能單獨為這個bug埋log,並build一個android的鏡像,然後使用這個鏡像來測試和提bug,原因是某些case下,對text的分析會導致數組越界。
  • 除了文本繪製的問題之外還解決了很多使用textview不當的問題,內置的很多應用比如contact , phone,在展示R2L的文字上沒有把這些文字完全放到屏幕的右邊,這樣其實是不符合阿拉伯文的習慣的。其實改動也很簡單,把android的布局從wrap content改為match parent,這樣view的寬度變化之後自然就展示OK了。


瀉藥

我是基本沒寫過這玩意

不過最近被坑夠嗆

關鍵還不是range、selection、標籤生成不一致什麼的問題

主要是無法跟上PM天馬行空的想法

比如

  • 游標在 H1-6 里就不能有子節點

  • 不能在編輯容器的根下產生文本節點

  • 不能產生空標籤

  • P等塊標籤不能在任意時候相互(包括自身)嵌套

  • P等指定標籤只能出現在編輯容器的根下

  • 加粗斜體等等不能嵌套

  • 在某個插入的tag內不能按回車換行

  • 刪除(選擇)某個tag時候得selection它的父一起幹掉

  • 等等

  • 等等


看完答案就好奇為什麼你們都說在瀏覽器里做個Editor,於是打了一大堆為什麼做個編輯器難。

然後翻到問題發現是「前端」,於是我就把答案都刪了。

在瀏覽器里做基本上只會輸入中英文的編輯器已經算好了,要實現的基本就是Word的1%子集。Word就是個妖孽。

舉個栗子:

你在知乎這編輯器里,打一行不加標點的字,是這樣的:

加一個句號,是這樣的

在Word里,是這樣的

加一個句號,是這樣的

但是你加一個字,是這樣的

在瀏覽器里,字距確定,現在瀏覽器還會處理一些顯示規則,譬如句號不能在行首,處理方法就是行尾最後一個字折到下一行。

Word會在一定範圍內調整字距,讓視覺效果更好些。

中文跟英文規則還不一樣,人家英文先調詞距。然而CJK和英文已經是「很好處理的語言」了。來個滿文什麼的簡直就是……

Word及以Word為目標的軟體得先過這第一道坎,排完文字後面還有一大堆坑,例如表格循環排版什麼的。然後才到諸位提到的在瀏覽器里做的功能,那些功能還只是Word的1%……

這才叫天坑好嗎!!!

-----------------------------------萌萌噠分割線-----------------------------------

跑個題,不知道視頻編輯算不算在富文本編輯器里。有興趣的同學可以開個Powerpoint,然後往裡插個視頻。人家還帶簡易好用的視頻編輯,截取片段,調整大小,剪裁,加濾鏡什麼的全有……


三年前不知天高地厚,嘗試寫過類似的東西,後來不得不承認 Word 才是邏輯最複雜的軟體。

大部分邏輯問題不是了解很多瀏覽器 API 就能夠處理的,是需要看邏輯分析能力去解決的。

富文本編輯器是一個非常有難度的課題,做不出來,也別懷疑人生。Word 類的工具也只有少數幾個嘛。


曾為了做一個 PC 和 Mobile 都可用的 contentEditable,光是 複製/粘貼 的兼容性就夠受了(比如當時有一個 feature 是能把網頁內容複製進剪貼板、把剪貼板中的 html 數據粘貼進來並做過濾、格式化,img 標籤需要保留),游標和選中也存在問題(應該是存在空 div 導致的)…勉強實現後我識時務地出了坑…


「word可以xxxx,你怎麼不行」


很有必要。因為都不好用只能自己造了。

基於contentEditable的來實現的,哪怕只要很少的功能,Bug根本就沒希望修到少到可以接受的地步,選這條路可以直接自殺了。

雖然contentEditable有一個好,就是瀏覽器原生功能配合的好。但是輸入中文要啥spell checker提示。反倒是輸入法事件是不能cancel的,這個是致命的,直接就否定了你通過只劫持一部分事件就能造個編輯器的可能。

所以,不要contentEditable。設置user-select: none。自己來實現selection,比較老的瀏覽器就不管了。像ace editor那樣,畫一個看不出內容,只能看到在哪裡閃啊閃的textarea來接收輸入。但是,因為ace editor只是代碼編輯器,這個textarea比較好畫。一般的編輯器還需要根據cursor所在位置設置對應的字體什麼的,假如你允許自定義CSS,估計得用getComputedStyle了。

至少到這一步,我們通過完全禁止瀏覽器直接操作DOM,於是對DOM的操作都在我們掌控之中了。接下來支持不同的功能又有不同的坑,但是和直接用原生程序寫編輯器差距已經不大了。

因為undo/redo,需要使用immutable的結構,不然直接往會跳個幾十步,你一個個apply過來都來不及啊。所以不能瀏覽器里內容變了,直接去改你自己DOM里的節點。簡單一點,瀏覽器里的DOM和你自己維護的DOM都用二叉樹串起來,內容發生變化應該先計算出這是第幾個Text,再到自己維護的DOM里去update第N個文本。


在這東西上面造輪子真的會讓你懷疑人生。。。


基於瀏覽器本身的機製做自然一堆坑,看各位的吐槽就知道了。但只把瀏覽器當作渲染後端和事件源,其他機制都自己實現,理論上也並不難,而且可控。buffer、style、layout、cursor、selection 等等都不依賴瀏覽器的實現,甚至輸入法也可以自己做(瀏覽器的 ime api 似乎還不可用?)。這個路線有不少非 web 平台的編輯器實現可以參考。不過瀏覽器在性能上是否達標就難說。一般最終的展示是通過 dom,如果編輯器也用 dom,可能性能就不行,滾動之類可能也要自己實現以減少開銷。用 canvas 實現可能性能好些,但樣式要做到和最終展示的一致,也不容易。


前幾天試著實現一下,淚奔:

坑一:用window.selection確定游標位置,游標偏移是相對上個標籤來的,插入一個bold後就不能直接獲取,沒辦法用只好previoussibling循環往前查,最後加在一起算位置

坑二:加粗之後要取消,同時選中加粗和非粗要一起加粗,沒辦法就設了一個狀態數組,0表示非粗1表示粗

坑三:設完這個數組後看到斜體,斜體加粗,list加粗,list斜體加粗的需求後選擇狗帶


我曾經問過一個類似的問題:

http://www.zhihu.com/question/26739121

當時我的直覺是用 web 實現可能比用 c++ 容易,但後來我考察了一下發現並不是,dom 介面實際上是很不適合做編輯器功能的。哪怕不考慮兼容性,內部的邏輯問題也已經足夠燒腦了。一般人甚至很難理出一個各方面自洽的需求。

所以,這確實是個大坑。

大家有興趣可以看一下作為富文本編輯器子集的代碼編輯器開源項目 codemirror,僅僅是一個子集,就已經複雜得讓人頭疼了。


我試著做過,文字標籤嵌套邏輯讓我懷疑人生......


富文本編輯器之所以是坑,那是因為所有人都會拿你的產品和word比較。。。。

任何Web端的富文本編輯器都無法和word比易用性。


推薦閱讀:

怎樣可以很好地保證網頁的瀏覽器兼容性?
如何評價Facebook推出的flow.js?
jQuery創始人知道function test(){}這樣定義函數不好嗎?
參加 2017 年 8 月 26 日北京第三屆 FEDAY 是個什麼樣的體驗?
Weebly 官網是怎麼把 2560x1400 的圖片壓縮到如此之小的?

TAG:前端開發 | JavaScript | 前端工程師 | 富文本編輯器 |