為什么 ContentEditable 很恐怖

jopen 10年前發布 | 62K 次閱讀 HTML編輯器 ContentEditable

為什么 ContentEditable 很恐怖

我第一次坐在Jacob(@fat)桌子對面時,他直率的問:“你是怎么寫一個文字編輯器的?”

我在白板上畫了一個樹結構,揮舞著手臂,說“這個是如shit一般的編輯界面。“然后我畫了一列方框,用箭頭指向數組,更多的揮舞手臂,說”這個是一個好的編輯界面。”

Jacob揚了一下眉。

這篇文章是我本應該那時說的,如果我有一年的時間去思考的話。

為什么 ContentEditable 這么恐怖: 一個數學上的佐證

ContentEditable 是一種在web瀏覽器上進行富文本編輯的本地原生組件. 它是那樣讓人…傷感.

我會用一些順手拈來的數學方法來想你證明,目前的ContentEditable的方法是不好的. 這并不是因為我覺得數學是解釋這個論點的一個有說服力的方式。它實際上使得這個論點更加的異類話.

但是我真的覺得文本編輯器導致了太多的模棱兩可, 就像“所見即所得 (WYSIWYG) 是啥意思?”還有 “當你選擇了這段文本并敲下了Enter會發生什么?”這樣不明確的問題。公理化的數學是我所知道的能解決模棱兩可的問題并對它們進行明確的最佳工具.

那么所見即所得是什么意思呢?一個好的所見即所得編輯器應該滿足下面3個定理:

    1.DOM內容和可視化(Visible)內容能夠很好地進行映射。
    2.DOM選擇和可視化(Visible)選擇能夠很好地進行映射。
    3.所有的可視化編輯都能夠映射到一個從代數上來說封閉的和完整的可視化內容集合上面。

首先,我會解釋這3個定理所表達的意思,并且一個好的編輯器為什么要遵守這些規則。但是我們首先要明白:他們是定理。他們最弱的部分是缺少證明。但是我們假設他們是沒問題的除非我們能拿出明顯的證據來。

其次,我會證明ContentEditable不滿足這3個定理。

最后,我會討論瀏覽器新的特性和庫是怎樣針對這些問題的,以及我們怎樣在普通的編輯器中處理他們的。

DOM空間是我們能在HTML中表述的所有網頁頁面的集合。所有頁面都能夠被表示成一個元素樹,而這些樹把文本節點作為葉子。

可視化空間(“所見即所得”)是所有可視化頁面的集合 —?也就是我們在屏幕上看到的瀏覽器渲染的一個頁面。我們常常會把看起來一樣的兩個頁面的可視化空間認為是一樣的。

瀏覽器渲染引擎是一個把DOM空間映射到可視化空間的一個映射。“映射”是指對某個DOM樹 x 進行 Render(x)操作產生所有的可視化頁面。

當我們說一個映射在編輯器中表現良好時,我們的意思是這個映射保留了所有的編輯操作(見注1)。更準確的說,如果渲染的定義是明確的話,那么

for all edit operations E, and DOM pages x and y
Render(x) = Render(y) 
implies Render(E(x)) = Render(E(y))

這是一種在“你看到的是什么”之后確定“你得到了什么”的方式。如果兩個部分看起來是一樣的,而我們對它們進行了相同的編輯,那么兩種結果應該看起來是一樣的. (請再看看第 1 條)

我已經很驚奇的看到網絡上那么多的 “WYSIWYG” 編輯器都打破了這個規則. 這聽起來應該是一個理所當然的規則。但是它會導致你陷入有關“相同”是什么意思這個有點怪異的問題,而對這個問題的問題的最好探討就是示例了.

行為良好的內容

看看下面這個例句:

The hobbit was a very well-to-do hobbit, and his name was Baggins.

編輯器里面呈現這句話,大致上如下所示.

The <a href=”http://en.wikipedia.org/wiki/The_Hobbit">hobbit</a> was a very well-to-do hobbit, and his name was <strong><em>Baggins</em></strong>.

有許多許多的方式可以用來對最后一個詞—— Baggins —— 進行編碼. (見注2)

<strong><em>Baggins</em></strong>
<em><strong>Baggins</strong></em>
<em><strong>Bagg</strong><strong>ins</strong></em>
<em><strong>Bagg</strong></em><strong><em>ins</em></strong>

編輯器應該明智的認為這些形式是等價的。你對這篇文章所進行的任何編輯都應該對所有這種形式同等對待。編寫一個編輯動作來了解所有這些不同的DOM形式是需要令人極其驚訝的技巧.

對于網上大量的可編輯內容的實現,一些不顯眼的字符或者空的span標簽可能會進入到HTML中,因此兩個可編輯內容的元素的表現完全不同(即使看起來是一樣的)。這樣的體驗會激怒用戶,而工程師也很難去調試。

即使我們知道如何編寫一個表現良好的編輯動作,但我們怎么檢查它呢?如果我們把HTML限制到一些簡單的標簽,證明兩個表單視覺上是相等的將。。。非常復雜。你最好能夠在每個字符上迭代,分配一個樣式,并比較結果。

在理想世界里,我們對于DOM的“可視編輯”會有一些高級的API。每個操作將保證它的有效性,對于所有視覺上相等的頁面做“相同”的操作。這樣,只要你的編輯器僅僅使用這些API,你就能保證它的有效性。

有效性選擇

DOM和可見內容的映射是很丑陋的,但至少是多對一的關系。一個DOM表示有一個準確的可見表示。

選擇更加丑陋,因為映射是多對多的關系。

你可以很輕松的看到一個可見的選擇可以有很多的DOM表示。如果你有HTML,

his name was <strong><em>Baggins</em></strong>

那么“Baggins”之前的指針可以在三個DOM位置之一:在 strong開始標簽前,在 strong開始標簽和 em開始標簽中間,以及在 em開始標簽之后。如果你把指針放在“Baggins”之前開始輸入,你的字符會是粗體,斜體,或者都不是?

更微妙地,一個DOM選擇可以有多種可視化表示。比如下面這種情況,“well-to-do”在“to-”之后換行,如上圖所展示的。光標在第一行的末尾和在第二行開頭擁有同樣的DOM位置,但是卻擁有不同的可視化位置。就我所知,我們沒有辦法讓瀏覽器去優先選擇哪一個可視化位置。當我們設計編輯器命令的時候,我們會讓選擇操作表現的和看起來是一樣的。但是那太痛苦了,因為這種映射太混亂了。

封閉并且完整的編輯操作

幾年前的某一天, 我的朋友Julie在Gchat上給我發了一個消息:

We can remove Apple Style Span…Oh happy day!

Ryosuke Niwa 在WebKit的博客上發表了一個友好的帖子 ,這篇帖子請求移除蘋果風格的span(apple-style-span)。如果你之前讀過這篇文章的話,那么他提出的許多問題聽上去很耳熟。WebKit的ContentEditable 編輯器增加許多“bookkeeping”HTML標簽,這種標簽不會改變任何的可視化效果,僅僅是使編輯器表現的不同。

他也指出WebKit的ContentEditable的實現必須能夠處理由其他任何的CMS或其他任何的瀏覽器的ContentEditable 的實現創建的HTML。我們的編輯器在這種生態環境下應該是一個好的公民。這意味著我們應該制作易讀易懂的HTML。但另一面,我們要意識到我們的編輯器必須處理那些我們不能在編輯器中創建的拷貝內容。

我見過許多種問題,復現這種問題的唯一方式是在Firefox中寫文本,然后切換到Chrome中做編輯操作,再然后切回到Firefox中。這對開發者和用戶來說是非常令人沮喪的。

一個好的所見即所得編輯器框架

一個最基本的 ContentEditable 元素是一個非常差的編輯器, 因為它打破了前面提到的所有定理.。那么我們怎樣構建一個好的所見即所得的編輯器呢?

對一個編輯器來說,有4個關鍵點。

  1. 創建一個文檔模型,并且能夠用一種簡單的方式去區分兩個模型是否在視覺上相等

  2. 創建一個在DOM與我們的模型之間的映射

  3. 在這種模型上能夠定義表現良好的編輯操作

  4. 能夠把所有的按鍵操作和鼠標點擊轉換成相應操作的序列

我會簡要的介紹每個關鍵點以及我們怎樣對他們做出改變。最后,我會討論瀏覽器工程師怎樣才能把ContentEditable 做的更好,并且去掉這些組件中不好的部分

編輯器模型

編輯器模型有兩個領域:一連串的段落以及一連串的區域。

每一個段落包括下面這些內容

  • 文本,一個普通文本的字符串

  • 標記,一連串格式化好的文本范圍,比如“對位置1到5的字符進行加粗”

  • 圖像或嵌入的元數據

  • 布局,一種我們怎樣放置段落的描述

一個區域描述一個子列表段落的背景。

在編輯器中的任何選擇操作都會被表述成兩個點。每個點代表的是一個段落的索引和那么段落的文本偏移,以及一個類別。大多數選擇操作都是文本類型的選擇。我們也有媒介類型的選擇(當提示信息顯示在圖片上時)和區域類型的選擇(當提示信息顯示在區域背景上面)。

這種模型的優點是如果僅當這兩種模型相等時他們會有同樣的可視化渲染效果。對模型的任何改變都能轉換到一個定義很好的可視化改變上來。

編輯器映射

下面我會定義DOM空間到這種模型空間的映射。我們把這種映射分成兩種:“室內”映射和“室外”映射。

室內映射是指我們從編輯器中取出內容并且來回的在DOM空間和這種模型空間進行映射。我們希望室內映射是一對一的。

室外映射是指我們從編輯器外獲取HTML,就像用戶從Word中拷貝HTML到一篇帖子中一樣。我們需要把它轉換到我們的段落和區域模型中。我們希望室外映射是有損的。我們優先處理普通文本,然后加粗/傾斜/超鏈接標簽,然后圖片以及其他各種各樣的格式。

我們的模型映射到DOM樹后,看起來像下面這樣:

<div> <!-- root -->
  <section> <!-- section -->
    <!-- section-inner -->
    <div class="section-inner layout-column">
      <p>  <!-- paragraph -->
        <strong><em>Baggins</em></strong> <!-- text -->

這個區域(section)節點是從區域模型中產生的,并且會將背景圖片和顏色應用到一連串的段落中。

這個區域內(section-inner)節點是根據段落排版屬性而產生的,并且決定了主列的寬度。對于大部分段落來說,它是狹窄并且居中的。對于全寬的圖片段落來說,它是100%寬的。對于上面的網格來說,它是原始的一般。

下一個節點是段落的語義類別: P, H2, H3, PRE, FIGURE, BLOCKQUOTE, OL-LI (有序列表項), 和 UL-LI (無序列表項).

當我們把標簽類別轉換到DOM節點的時候,我們會按照類別排序它們:A,然后 STRONG,再然后 EM。我們永遠不會打印一個包含錨定的STRONG標簽。我們會拆散它而讓錨定包含STRONG標簽。

編輯操作

編輯器主要包括6個編輯操作:插入段落,移除段落,更新段落,插入區域,移除區域,以及更新區域。

這些操作表現的會和描述的那樣。段落操作會接受一個段落模型和一個索引。區域操作會接受一個區域模型和一個索引。

所有可能的編輯器內容都能夠被一系列的這些操作所表述,并且構造這樣一個序列通常是比較簡單的。

顯而易見,內容在這些編輯操作下能夠表現的很好。這些操作是直接應用于我們的模型上面的,而不是DOM上面,并且這個模型能夠更容易地區分兩件東西在視覺上是否是相等的。

捕獲編輯操作

當你和編輯器交互的時候,我們必須將你的按鍵操作和鼠標點擊操作轉換成那6個操作的一個序列。

這是最復雜的部分。我們不會對那么多的每種可能按鍵的序列履行職責。這對于一個以英語為語言的用戶來說是一個非常巨大的列表,永遠不考慮非拉丁字符和鍵盤。

通過觀察,我們能夠用常規的ContentEditable鍵盤操作枚舉所有對段落插入和移除的操作方式。他們是:回車(enter,ctrl-m,等。),刪除(delete,backspace,等。),懸浮輸入(type-over)(在一段選擇的文本上輸入),以及拷貝。所以我們能夠捕獲,取消,以及手動將這些鍵盤事件轉換到我們的編輯器內部操作上來。

對于所有其他鍵盤事件,我們讓原有的ContentEditable 行為生效。在鍵盤事件結束之后,我們把段落的DOM映射回到段落的模型中來,并且和我們之前的模型進行比較。如果DOM改變了,我們會創建一個新的更新段落操作并且通過編輯器管線應用它,保持DOM和模型同步。

快速捕獲編輯操作

如果我們有無限計算的能力,那么直接應用這些編輯操作就可以了。我們應用這些操作到模型上,重新渲染的整個頁面,最后結束操作。

但是在現實生活中,對每個按鍵操作都重新渲染整個頁面是非常慢的。并且你會看到許多丑陋的閃爍現象,因為內嵌框架(iframes)和圖片會一直處于加載中。相反,我們會對模型的改變事件進行監聽,并且盡最大可能減少對DOM的改變。

當我在寫篇文章時,我可以看到Chrome拼寫檢查程序在“keypress”單詞下面所加的紅色下劃線在閃爍。這是因為編輯器正在同時對整個段落進行改變,而不是僅僅改變這個段落的一小塊。如果我們僅僅對DOM進行相對很小的改變,那么閃爍就會消失,但是這樣的代碼會相對的更復雜。

期待將來有一個更智能的文本編輯操作

最近有一些來自Chromium貢獻者 (Levi Weintraub, Julie Parent, and Jelte Liebrand) 的流言說這些貢獻者想去基于聚合元素(Polymer Elements)和Shadow DOM特性重做ContentEditable。這個方案也會像編輯器一樣去嘗試解決許多同樣高級架構方面的問題。

  1. 創建一個由自定義 聚合元素(Polymer elements)構成的編輯器模型

  2. 定義編輯器模型與真實的具有 Shadow DOM特性的DOM之間的映射

  3. 所有在ContentEditable中的按鍵操作和鼠標點擊操作都會被轉換成一種抽象的編輯含義,被表示成像{editIntent: ‘delete’}這樣的JSON對象。

  4. 聚合元素(Polymer Elements)對這樣的編輯含義操作定義相應的處理方法

如果編輯器能夠獲取某種編輯含義的API,那么我們就能夠拋棄許多轉換按鍵操作到抽象編輯操作的自定義代碼了。將我們的段落模型當作聚合(Polymer)/ ShadowDOM元素是一件很有趣的嘗試。

ContentEditable是什么

不管我什么時候向那些從事于文本編輯器工作的人解釋的時候,他們都認為我在玩花招。

“當然編輯器要比ContentEditable更棒一些。你錯了。ContentEditable努力的去成為一個通用的所見即所得HTML編輯器。而一般的編輯器放棄了'通用目的'的需求,所以你能夠挑選你想去處理的任何HTML結構。”

這是事實。但是確是誤導的。

一個好的所見即所得編輯器和一個好的具有通用目的HTML編輯器在理論上是不一致的。不可能把ContentEditable 構建成那樣的,因為它們在需求上是沖突的。

Steve Yegge的那篇“The Nonesuch Beast”文章影響了我的想法。 設計和UX(用戶體驗)問題與big-O算法問題一樣地棘手。一個好的WYSIWYG任意HTML編輯器與halting問題一樣不可能。

ContentEditable可以挽救。 但它的使命必須改變。 豐富的DOM API像Shadow DOM一樣,ContentEditable可能成為一個平臺用來構建新一代的網絡編輯器。但我們必須把它作為一個編輯平臺和API,相對于獨立的組件總比什么都自己做要好。

腳注:

  1. 如果你學過本科的高等數學, 一個良態的映射是一個態射。 那個單詞感覺是費解的, 還帶來了額外的包袱,在這里它不能幫助我們。 所以這次我們使用了良態。

  2. CSS復雜化這個討論,和復雜的編輯算法,它的瀏覽器算法已經被實現。但是它不是一個徹底的證據證明,可以用來證明為什么ContentEditable是可怕的。 一般來說,我們忽視CSS, 也會限制我們對簡單HTML的分析。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!