<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      深感一無所長,準備試著從零開始寫個富文本編輯器

      富文本編輯器是允許用戶在輸入和編輯文本內容時,可以應用不同的格式、樣式等功能,例如圖文混排等,具有所見即所得的能力。與簡單的純文本編輯組件<input>等不同,富文本編輯器提供了更多的功能和靈活性,讓用戶可以創建更豐富和結構化的內容。現代的富文本編輯器也已經不僅限于文字和圖片,還包括視頻、表格、代碼塊、附件、公式等等比較復雜的模塊。

      從零實現富文本編輯器項目的相關文章:

      Why?

      那么為什么要從零設計實現新的富文本編輯器,編輯器是公認的天坑,且當前已經有很多優秀的編輯器實現。例如極具表現力的數據結構設計Quill、結合React視圖層的Draft、純粹的編輯器引擎Slate、高度模塊化的ProseMirror、開箱即用的TinyMCE/TipTap、集成協同解決方案的EtherPad等等。

      我也算是比較關注于各類富文本編輯器的實現,包括在各個站點上的編輯器實現文章我也會看。但是我發現這其中極少有講富文本編輯器的底層設計,絕大多數都是講的應用層,例如如何使用編輯器引擎實現某某功能等。雖然這些應用層的實現本身也會有一定復雜性,但是底層的設計卻是更值得探討的問題。

      此外,我覺得富文本編輯器很類似于低代碼的設計,準確來說是No Code的一種實現。本質上低代碼和富文本都是基于DSL的描述來操作DOM結構,只不過富文本主要是通過鍵盤輸入來操作DOM,而無代碼則是通過拖拽等方式來操作DOM,我想這里應該是有些共通的設計思路。

      而我恰好前段時間都在專注于編輯器的應用層實現,在具體實現的過程中也遇到了很多問題,并且記錄了相關文章。然而在應用層實現的過程中,遇到了很多我個人覺得可以優化的地方,特別是在數據結構層面上,希望能夠將我的一些想法應用出來。而具體來說,主要有下面的幾個原因:

      編輯器專欄

      紙上得來終覺淺,絕知此事要躬行。

      我的博客是從20年開始寫的,記錄的內容很多,基本上是想到什么就寫什么,畢竟是作為平時學習的記錄。然后在24年寫了比較多的富文本編輯器的文章,主要是整理了平時遇到的問題以及解決方案,集中在應用層的設計上,例如:

      此外,前段時間還研究了slate富文本編輯器相關的實現,并且也給slate的倉庫提過一些PR。還寫了一些slate相關的文章,并且還基于slate實現了一個文檔編輯器,同樣也是比較關注于應用層的實現,例如:

      在實現了諸多的應用層的功能之后,發現整個編輯器有很多可以深入研究的地方。特別是有些實現看似很理所當然,但是仔細研究起來會發現這其中有很多細節可以探究,例如在DOM結構后常見的零寬字符、Mention節點的渲染等等,這些內容都可以單獨拿出來記錄文章,這其實就是我想從零實現編輯器的最重要原因。

      24年開始寫了很多業務上的東西,到了25年就略感題窮,而目前我也沒有別的擅長的方面,由此寫編輯器相關的內容是比較好的選擇,這樣對于文章的選題也會簡單些。不過,雖然想的是深入寫編輯器相關的內容,但是在平時遇到問題的時候,還是會記錄下來,例如最近有個基于immer配合OT-JSON實現的狀態管理的想法可以實現。

      而對于編輯器的具體實現,我目前的目標是實現可用的編輯器,而不是兼容性非常好且功能完備的編輯器。主要是現在已經有非常多優秀的編輯器實現,且有很多生態插件可以支持,能夠滿足大部分的需求。目前我想實現的編輯器主要是兼容Chrome瀏覽器即可,移動端的問題暫時不會考慮。不過,如果能夠將編輯器做得比較好的話,自然可以去做兼容性適配。

      不過目前還是試探性地來設計并實現編輯器,期間必然會遇到很多問題,這些問題也將會成為專欄的主體內容。最開始的時候,我是準備將編輯器完善后再開始撰寫文章,后來發現設計過程中的歷史方案同樣很有價值,因此決定將設計過程也一并記錄下來。如果將來真的能夠將編輯器適用于生產環境,那么這些文章就能夠溯源到模塊為什么這么設計,想必也是極好的。整體來說,我們不能一口吃成胖子,但是一口一口吃卻是可以的。

      深入編輯器

      這部分是讓我想起來一句話:我們富文本編輯器是這樣的,你不寫你不懂。

      編輯器是個非常注重細節的工程,很多時候都需要深入研究瀏覽器的API,例如document上的caretPositionFromPoint方法,用以獲取當前某個點所在的選區位置,通常用于拖拽文本后的落點定位。除此之外,還有很多選區相關的API,例如SelectionRange等等,這些都是編輯器實現的基礎。

      那么深入編輯器底層就是很有意義的事情,很多時候我們都需要跟瀏覽器打交道,即使是對我們平時的業務開發也會有價值。在這里我想聊一下編輯器中的零寬字符,以此例學習編輯器的細節設計,這是一個非常有意思的話題,類似這種內容就是不研究則不會關注到的有趣事情。

      零寬字符顧名思義是沒有寬度的字符,因此就很容易推斷出這些字符在視覺上是不顯示的。因此這些字符就可以作為不可見的占位內容,實現特殊的效果。例如可以實現信息隱藏,以此來實現水印的功能,以及加密的信息分享等等,某些小說站點會通過這種方式以及字形替換來追溯盜版。

      而在富文本編輯器中,如果我們在開發者工具檢查元素時,可能會發現一些類似于&ZeroWidthSpace;U+200B類似的字符,這就是常見的零寬字符。例如在飛書文檔的編輯器中,我們通過("[data-enter]")就可以檢查到其中存在的零寬字符。

      <!-- document.querySelectorAll("[data-enter]") -->
      <span data-string="true" data-enter="true" data-leaf="true">\u200B</span>
      <span data-string="true" data-enter="true" data-leaf="true">&ZeroWidthSpace;</span>
      

      那么從名字上來看,這個零寬字符在視覺上是不顯示的,因為其是零寬度。但是在編輯器中,這個字符卻是很重要的。簡單來說,我們需要這個字符來放置光標,以及做額外的顯示效果。需要注意的是我們在這里指的是ContentEditable實現的編輯器,如果是自繪選區的編輯器則不一定需要這部分設計。

      我們先來聊一下額外的顯示效果,舉個例子,我們在選擇飛書文檔文本內容,如果選中到文本末尾時,會發現末尾會額外多出形似xxx|的效果。在平時不關注的話可能會覺得這是編輯器默認行為,但是實際上這個效果無論是slate還是quill中都是不存在的。

      實際上這個效果就是使用零寬字符來實現的,在行內容的末尾后面插入零寬字符,就可以做到末尾的文本選中效果。實際上這個效果在word中更常見,也就是額外渲染的回車符號。

      <div contenteditable="true">
        <div><span>末尾零寬字符 Line 1</span><span>&#8203;</span></div>
        <div><span>末尾零寬字符 Line 2</span><span>&#8203;</span></div>
        <div><span>末尾純文本 Line 1</span></div>
        <div><span>末尾純文本 Line 2</span></div>
      </div>
      

      那么在這個零寬字符如果只是渲染效果的話,那么可能實際上起的作用并不很必要。但是在交互上這個效果卻很有用,例如此時我們有3行文本,如果此時從第1行末尾選到第2行時,并且按下Tab鍵,那么此時這兩行的內容就會縮進。

      那么如果沒有這個顯示效果,此時進行縮進操作,用戶可能認為僅僅是選中了第2行,但是實際上是選中了1/2兩行文本。這樣的話用戶可能會以為是BUG,而我們也實際接受過這個交互效果的反饋。

      123|
      4|x56
      

      也對各個在線文檔實現進行了簡單調研: 基于contenteditable實現的編輯器中,飛書文檔、早期EtherPad存在這個交互實現;自繪選區的編輯器中,釘釘文檔存在這個實現;Canvas引擎實現的編輯器中,騰訊文檔、Google Doc存在這個實現。

      在渲染效果部分,零寬字符還有一個重要的作用是撐起行內容。當我們的行內容為空時,此時這個行DOM結構的內容就是空,這就導致此行的高度塌陷為0,且無法放置光標。為了解決這個問題,我們可以選擇在行內容中插入零寬字符,這樣就可以撐起行內容且可以放置光標。當然使用<br>來撐起行高也是可以的,使用這兩種方案會各有優劣,且兼容性方面也有所不同。

      <div data-line-node></div>
      <div data-line-node><br></div>
      <div data-line-node><span>&#8203;</span></div>
      

      在類似于Notion這種塊結構的編輯器中,還有個比較重要的交互效果。即塊級結構獨立選擇,例如我們可以直接將整個代碼塊獨立選出來,而不是僅僅能選擇其中的文本。這種效果在目前的開源編輯器很少有實現,都是需要自行以塊結構重新組織設計選區。

      通常來說,這個交互同樣可以使用零寬字符來實現。因為我們的選區通常是需要放置在文本節點上的,因此我們很容易可以想到,可以在塊結構所在行的末尾放置零寬字符,當選區在零寬字符上時就將整個塊選中。這里用零寬字符而不是<br>的好處是,零寬字符本身就是零寬,不會引起額外的換行。

      <div>
        <pre><code>
          xxx
        </code></pre>
        <span data-zero-block>&#8203;</span>
      </div>
      

      在結構上,零寬字符還有個非常重要的實現。在編輯器內的contenteditable=false節點會存在特殊的表現,在類似于inline-block節點中,例如Mention節點中,當節點前后沒有任何內容時,我們就需要在其前后增加零寬字符,用以放置光標。

      在下面的例子中,line-1是無法將光標放置在@xxx內容后的,雖然我們能夠將光標放置之前,但此時光標位置是在line node上,是不符合我們預期的文本節點的。那么我們就必須要在其后加入零寬字符,在line-2/3中我們就可以看到正確的光標放置效果。這里的0.1px也是個為了兼容光標的放置的magic,沒有這個hack的話,非同級節點光標同樣無法放置在inline-block節點后。

      <div contenteditable style="outline: none">
        <div data-line-node="1">
          <span data-leaf><span contenteditable="false" style="margin: 0 0.1px;">@xxx</span></span>
        </div>
        <div data-line-node="2">
          <span data-leaf>&#8203;</span>
          <span data-leaf><span contenteditable="false" style="margin: 0 0.1px;">@xxx</span></span>
          <span data-leaf>&#8203;</span>
        </div>
        <div data-line-node="3">
          <span data-leaf>&#8203;<span contenteditable="false">@xxx</span>&#8203;</span>
        </div>
      </div>
      

      除此之外,編輯器自然是需要跟字符打交道的,那么在js表現出來的Unicode編碼實現中,emoji就是最常見且容易出問題的表達。除了其單個長度為2這種情況外,組合的emoji也是使用獨特的零寬連字符\u200d來表示的。

      "??".length
      // 2
      "??" + "\u200d" + "??"
      // ?????
      

      數據結構設計

      編輯器數據結構的設計是影響面非常廣的事情,無論是在維護編輯器的文本內容、塊結構嵌套、序列化反序列化等,還是平臺應用層面上的diff算法、查找替換、協同算法等,以及后端服務的數據轉換、導出md/word/pdf、數據存儲等,都會涉及到編輯器的數據結構設計。

      通常來說,基于JSON嵌套的數據結構來表達編輯器Model是很常見的,例如SlateProseMirrorLexical等等。以slate編輯器為例,無論是數據結構還是選區的設計,都盡可能傾向于HTML的設計,因此可以存在諸多層級節點的嵌套。

      [
        {
          type: "paragraph",
          children: [{ text: "editable" }],
        },
        {
          type: "ul",
          children: [
            {
              type: "li",
              children: [{ text: "list" }],
            },
          ],
        },
      ];
      

      通過線性的扁平結構來表達文檔內容也是常見的實現方案,例如QuillEtherPadGoogle Doc等等。以quill編輯器為例,其內容上的數據結構表達不會存在嵌套,當然本質上還是JSON結構,而選區則采用了更精簡的表達。

      [
        { insert: "editable\n" },
        { insert: "list\n", attributes: { list: "bullet" } },
      ];
      

      當然還有很多特別的數據結構設計,例如vscode/monacopiece table數據結構。代碼編輯器又何嘗不是一種富文本編輯器,畢竟其是可以支持代碼高亮的功能的,只不過類似piece table的結構我還沒有太深入研究。

      在這里我希望能夠以線性的數據結構來表達整個富文本結構,雖然嵌套的結構能夠更加直觀地表達文檔內容,但是對于內容的操作起來會更加復雜,特別是存在嵌套的內容時。以slate為例,在0.50之前的版本API設計非常復雜,需要比較大的理解成本,雖然之后將其簡化了不少:

      // https://github.com/ianstormtaylor/slate/blob/6aace0/packages/slate/src/interfaces/operation.ts
      export type NodeOperation =
        | InsertNodeOperation
        | MergeNodeOperation
        | MoveNodeOperation
        | RemoveNodeOperation
        | SetNodeOperation
        | SplitNodeOperation;
      export type TextOperation = InsertTextOperation | RemoveTextOperation;
      

      從這里可以看出來,slate對于文檔內容的完整操作是需要9種類型的Op。而如果是基于線性結構的話,我們就只需要三種類型的操作,即可表達整個文檔的操作。當然對于一些類似Move的操作,則需要額外的選區Range計算處理,相當于將計算成本移交到了應用層。

      // https://github.com/WindRunnerMax/BlockKit/blob/c24b9e/packages/delta/src/delta/interface.ts
      export interface Op {
        // Only one property out of {insert, delete, retain} will be present
        insert?: string;
        delete?: number;
        retain?: number;
      
        attributes?: AttributeMap;
      }
      

      此外,嵌套結構的normalize會變得很復雜,且變更造成的時間復雜度也會變高,特別是臟路徑標記算法,以及標記后的數據處理也需要由上述Op處理。還有用戶操作導致的嵌套層級無法非常好地控制,就要normalize過程時規范數據,否則下面例如粘貼HTML時就可能會出現大量的數據嵌套。

      [{
        children: [{
          children: [{
            children: [{
              children: [{
                // ...
                text: "content"
              }]
            }]
          }]
        }]
      }]
      

      再舉個更加實用的例子,如果我們此時存在格式的嵌套內容。例如quotelist兩種格式嵌套,如果此時我們文檔的數據結構是嵌套結構,那么操作內容就會存在ul > quote或者quote > ul的兩種情況,正常情況下我們必須要設計規則來做normalize;而扁平結構下,屬性全部寫在attrs內,不同操作造成的數據格式變更是完全冪等的。

      // slate
      [{
        type: "quote",
        children: [{
          type: "ul",
          children: [{ text: "text" }]
        }],
      }, {
        type: "ul",
        children: [{
          type: "quote",
          children: [{ text: "text" }]
        }],
      }]
      
      // quill
      [{
        insert: "text",
        attributes: { blockquote: true, list: "bullet" }
      }]
      

      扁平的數據結構在數據處理方面會存在優勢,而在視圖層面上,扁平的數據結構表達結構化的數據會是比較困難的,例如表達代碼塊、表格等嵌套結構。但是這件事并非是不可行的,例如Google Doc的復雜表格嵌套就是完全的線性結構,這其中是存在很巧妙的設計在里邊的,在這里先不展開了。

      此外,如果我們需要實現在線文檔的編輯器的話,在整個管理流程中可能會需要diff,即取得兩邊數據結構的增刪改。這種情況下扁平的數據結構能夠更好地處理文本內容,而JSON嵌套結構的數據則會麻煩很多。還有一些其他關于數據處理方面的周邊應用,整體復雜度都要提升不少。

      最后還是有協同相關的實現,協同算法是富文本編輯器的可選模塊。無論是基于OT的協同算法,還是Op-Based CRDT的協同算法,都是需要傳輸上述的op類型與數據的,那么很顯然9種操作的op類型會比3種操作的op類型更加復雜。

      因此,我希望能夠以線性的數據結構來實現整個編輯器結構,這樣quilldelta就是非常好的選擇。但是quill是自行實現的視圖層結構,并非是可以組合react等視圖層的形式,組合這些視圖層的優勢就是可以直接使用組件庫樣式來實現編輯器,而避免了每個組件都需要自行實現。那么這里我準備基于quill的數據結構,來從零實現富文本編輯器核心層,并且像slate一樣以此組合基本的視圖層。

      方案選型

      其實這里有個有趣的問題,為什么用不到1mb的代碼量就可以實現部分類似office word編輯器的能力,是因為瀏覽器已經幫我們做了很多事情,并通過API提供給開發者,包括輸入法處理、字體解析、排版引擎、視圖渲染等等。

      因此我們是需要設計出如何跟瀏覽器交互的方案,畢竟我們實際上是需要跟瀏覽器交互的。而對于富文本編輯器最經典的描述則是分為了三級:

      • L0: 基于瀏覽器提供的ContentEditable實現富文本編輯,使用瀏覽器的document.execCommand執行命令操作。 是作為早期輕量編輯器,可以較短時間內快速完成開發,但可定制的空間非常有限。
      • L1: 同樣基于瀏覽器提供的ContentEditable實現富文本編輯,但數據驅動可以自定義數據模型與命令的執行。常見的實現有語雀、飛書文檔等等,可以滿足絕大部分使用場景,但無法突破瀏覽器自身的排版效果。 |
      • L2: 基于Canvas自主實現排版引擎,只依賴少量的瀏覽器API。常見的實現有Google Docs、騰訊文檔等等,具體實現需要完全由自己控制排版,相當于使用畫板而不是DOM來繪制富文本,技術難度相當高。

      實際上在目前的開源產品中,這三種類型的編輯器都有涉及到,特別是絕大多數開源的都是L1類型的實現。而這其中還分化了不依賴ContentEditable卻也不是完全自繪引擎,而是依賴DOM呈現內容外加自繪選區的實現,實際上倒是可以算作L1.5的級別。

      本著學習的目的,自然要選擇開源產品多的實現,這樣遇到問題可以更好地借鑒和分析相關內容。因此我同樣打算選擇基于ContentEditable,實現數據驅動的標準MVC模型的富文本編輯器,基于這種方式來與瀏覽器交互,實現基本的富文本編輯能力。在此之前,我們還是先了解一下基本的編輯器實現:

      ExecCommand

      如果我們僅僅需要最基本的行內樣式,例如加粗、斜體、下劃線等,這可能在一些基本輸入框中是足夠的,那么我們自然可以選擇使用execCommand來實現。甚至直接基于execCommand的好處就是,其體積會非常小,例如 pell 的實現,僅僅需要3.54KB的代碼體積,此外還有 react-contenteditable 等實現。

      我們也可以實現可以加粗的最小DEMOexecCommand命令可以在contenteditable元素中選區內的元素執行,document.execCommand方法接受三個參數,分別是命令名稱、顯示用戶界面、命令參數。顯示用戶界面一般都是falseMozilla沒有實現,而命令參數則是可選的,例如超鏈接命令則需要傳遞具體鏈接地址。

      <div>
        <button id="$1">加粗</button>
        <div style="border: 1px solid #eee; outline: none" contenteditable>
          123123
        </div>
      </div>
      <script>
        $1.onclick = () => {
          document.execCommand("bold");
        };
      </script>
      

      當然,這個示例過于簡單,我們還可以在選區變換的時候,來判斷加粗按鈕的加粗狀態,以此來顯示當前選區狀態。不過我們需要對齊execCommand的命令行為,前邊也提到了可控性非常差,因此我們需要通過document.createTreeWalker迭代所有的選區節點,以此來判斷當前選區的狀態。

      其實這里還需要注意的是,execCommand命令的行為在各個瀏覽器的表現是不一致的,這也是之前我們提到的瀏覽器兼容行為的一種,然而這些行為我們也沒有任何辦法去控制,這都是其默認的行為:

      • 在空contenteditable編輯器的情況下,直接按下回車鍵,在Chrome中的表現是會插入<div><br></div>,而在FireFox(<60)中的表現是會插入<br>IE中的表現是會插入<p><br></p>
      • 在有文本的編輯器中,如果在文本中間插入回車例如123|123,在Chrome中的表現內容是123<div>123</div>,而在FireFox中的表現則是會將內容格式化為<div>123</div><div>123</div>
      • 同樣在有文本的編輯器中,如果在文本中間插入回車后再刪除回車,例如123|123->123123,在Chrome中的表現內容會恢復原本的123123,而在FireFox中的表現則是會變為<div>123123</div>
      • 在同時存在兩行文本的時候,如果同時選中兩行內容再執行("formatBlock", false, "P")命令,在Chrome中的表現是會將兩行內容包裹在同個<p>中,而在FireFox中的表現則是會將兩行內容分別包裹<p>標簽。
      • ...

      此外還有類似于實現加粗的功能,我們無法控制是使用<b></b>來實現加粗還是<strong></strong>來實現加粗。還有瀏覽器的兼容性問題,例如在IE瀏覽器中是使用<strong></strong>來實現加粗,在Chrome中是使用<b></b>來實現加粗,IESafari不支持通過heading命令來實現標題命令等等。且對于一些比較復雜的功能,例如圖片、代碼塊等等,是無法很好實現的。

      當然,默認的行為并不是完全沒有用的,在某些情況下,我們可能要實現純HTML的編輯器。畢竟如果在基于MVC模式的編輯器實現中,會處理掉對Model來說無效的數據內容,這樣就導致原本的HTML內容丟失,因此在這種需求背景下依賴瀏覽器的默認行為可能是最有效的,這種情況下我們可能主要關注的就是XSS的處理了。

      ContentEditable

      ContentEditableHTML5中的一個屬性,可以讓元素變得可編輯,再配合上內置的execCommand就是我們上邊聊的最基本DEMO。那么如果要實現最基本的文本編輯器,就只需要在地址欄中輸入下面的內容:

      data:text/html,<div contenteditable style="border: 1px solid black"></div>
      

      那么通過document.execCommand來執行命令修改HTML的方案雖然簡單,我們也聊過了其可控性很差。除了上述的execCommand命令執行兼容性問題之外,還有很多DOM上的需要兼容處理的行為,例如下面存在簡單加粗格式的句子:

      123**456**789
      

      有許多方式可以表達這樣的內容,編輯器可以認為顯示效果是等價的,此時可能也需要對此類DOM結構等同處理:

      <span>123<b>456</b>789</span>
      <span>123<strong>456</strong>789</span>
      <span>123<span style="font-weight: bold;">456</span>789</span>
      

      但是這里僅僅是視覺上相等,將其完整對應到Model上時,自然會是件麻煩的事。除此之外,選區的表達同樣也是復雜的問題,以下面的DOM結構為例:

      <span>123</span><b><em>456</em></b><span>789</span>
      

      如果我們要表達選區折疊在4這個字符左側時,同樣會出現多種表達可以實現這個位置,這實際上就會很依賴瀏覽器的默認行為:

      { node: 123, offset: 3 }
      { node: <em></em>, offset: 0 }
      { node: <b></b>, offset: 0 }
      

      因此為了更強的擴展以及可控性,也解決數據與視圖無法對應的問題,L1的富文本編輯器使用了自定義數據模型的概念。即在DOM樹的基礎上抽離出來的數據結構,相同的數據結構可以保證渲染的HTML也是相同的,配合自定義的命令直接控制數據模型,最終保證渲染的HTML文檔的一致性。對于選區的表達,則需要根據DOM選區來不斷normalize選區Model

      其實這也就是我們常見的MVC模型,當執行命令時會修改當前的模型,進而表現到視圖的渲染上。簡單來說就是構建一個描述文檔結構與內容的數據模型,并且使用自定義的execCommand對數據描述模型進行修改。在這個階段的富文本編輯器,通過抽離數據模型,解決了富文本中臟數據、復雜功能難以實現的問題。我們也可以大概描述流程:

      <script>
        const editor = {
          // Model 選區
          selection: {},
          execCommand: (command, value) => {
            // 執行具體的命令, 例如 bold
            // 命令執行后, 更新 Model 以及 調用 DOM 渲染
          },
        }
        const model = [
          // 數據模型
          { type: "bold", text: "123" },
          { type: "span", text: "123123" },
        ];
        const render = () => {
          // 根據 type 渲染具體 DOM
        };
        document.addEventListener("selectionchange", () => {
          // 選區變換時
          // 根據 dom 選區來更新 model 選區
        });
      </script>
      

      而類似這種方案,無論是 quill 還是 slate 都是這樣的調度。而類似于slate的實現,通過適配器來連接React之后,就需要更復雜的兼容處理。在React節點中加入ContentEditable后,會出現類似下面的warning:

      <div
        contentEditable
        suppressContentEditableWarning
      ></div>
      //  A component is `contentEditable` and contains `children` managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.
      

      這個warning的意思是,React無法保證ContentEditable中的children不會被意外修改或復制,這可能是不被意料到的。也就是說除了React本身是會需要執行DOM操作的,使用了ContentEditable之后,這個行為就變的不受控了,自然這個問題同樣會出現在我們的編輯器中。

      此外還有一些其他的行為,例如下面的例子中,我們無法從123字符選中到456上。也就是這里存在跨越ContentEditable節點了,就不能夠正常使用瀏覽器的默認行為來處理,雖然這個處理是很合理的,但畢竟也會對我們實現blocks編輯器造成一些困擾。

      <div contenteditable="false">
        <div contenteditable="true">123</div>
        <div contenteditable="true">456</div>
      </div>
      

      那么其實我們是可以避免使用ContentEditable的,設想一下即使我們沒有實現編輯器,同樣是可以選擇頁面上的文本內容的,就是我們普通的選區實現。那么如果借助原生的選區實現,然后在此基礎上實現控制器層,就可以實現完全受控的編輯器。

      但是這里存在一個很大的問題,就是內容的輸入,因為不啟用ContentEditable的話是無法出現光標的,自然也無法輸入內容。而如果我們想喚醒內容輸入,特別是需要喚醒IME輸入法的話,瀏覽器給予的常規API就是借助<input>來完成,因此我們就必須要實現隱藏的<input>來實現輸入,實際上很多代碼編輯器例如 CodeMirror 就是類似實現。

      但是使用隱藏的<input>就會出現其他問題,因為焦點在input上時,瀏覽器的文本就無法選中了。因為在同個頁面中,焦點只會存在一個位置,因此在這種情況下,我們就必須要自繪選區的實現了。例如釘釘文檔、有道云筆記就是自繪選區,開源的 Monaco Editor 同樣是自繪選區,TextBus 則繪制了光標,選區則是借助了瀏覽器實現。

      在這里可以總結一下,使用ContentEditable需要處理很多DOM的特異行為,但是明顯我們是不需要太過于處理喚醒輸入這個行為。而如果不使用ContentEditable,卻使用DOM來呈現富文本內容,則必須要借助額外的隱藏input節點來實現輸入,由于焦點問題在這種情況下就不能使用瀏覽器的選區行為,因此就需要自繪選區的實現。

      Canvas

      基于Canvas繪制我們需要的內容,頗有些文藝復興的感覺,這種實現方式是完全不依賴DOM的,因此可以完全控制排版引擎。那么文藝復興指的是,基于DOM兼容實現的任何生態都會失效,例如無障礙、SEO、開發工具支持等等。

      那么為什么要拋棄現有的DOM生態,轉而用Canvas來繪制富文本內容。特別是富文本會是非常復雜的內容,因為除了文本外,還有圖片的內容,以及很多結構話格式的內容,例如表格等。這些內容都需要自行實現,那么在Canvas中實現這些內容其實相當于重新實現了部分skia

      基于Canvas繪制的編輯器,當前主要有騰訊文檔、Google Doc等,而開源的編輯器實現有 Canvas Editor。而除了文檔編輯器之外,在線表格的實現基本都是Canvas實現,例如騰訊文檔Sheet、飛書多維表格等,開源的實現有 LuckySheet

      Google Doc發布的Blog中,對于使用Canvas繪制文檔主要選了兩個原因:

      • 文檔的一致性: 這里的一致性指的是瀏覽器對于類似行為的兼容,舉個例子: 在Chrome中雙擊某段文本的內容,選區會自動選擇為整個單詞,而在早期FireFox中則是會自動選擇一句話。類似這種行為的不一致會導致用戶體驗的不一致,而使用Canvas繪制文檔可以自行實現保證這種一致性。
      • 高效的繪制性能: 通過Canvas繪制文檔,可以更好地控制繪制時機,而不需要等待重繪回流,更不需要考慮DOM本身復雜的兼容性考量,以此造成的性能損失。此外,Canvas繪圖替代繁重的DOM操作,通過逐幀渲染和硬件加速可以提升渲染性能,從而提高用戶操作的響應速度和整體使用體驗。

      此外,排版引擎還可以控制文檔的排版效果,做富文本的各種需求,我們就可能面臨產品為什么不能支持像office word那樣的效果。例如如果我們編寫的文字正好排滿了一行,假如在這里再加一個句號,那么前邊的字就會擠一擠,從而可以使這個句號是不需要換行。而如果我們再敲一個字的話,這個字是會換行的。在瀏覽器的排版中是不會出現這個狀態的,所以假如需要突破瀏覽器的排版限制,就需要自己實現排版能力。

      <!-- word -->
      文本文本文本文本文本文本文本文本文本文本文本文本文本文本。
      <!-- 瀏覽器 -->
      文本文本文本文本文本文本文本文本文本文本文本文本文本文本
      。
      

      也就是說,在word中通常是不會出現句號在段落起始的,而在瀏覽器中是會存在這種情況的,特別是在純ASCII字符的情況下。如果想規避這種排版狀態的差異,就必須要自行實現排版引擎,以此來控制文檔的排版效果。

      此外,還有一些其他的功能,例如受控的RTL布局、分頁、頁碼、頁眉、腳注、字體字形控制等等。特別是分頁的能力,在某些需要打印的情況下,這個效果是很必要的,但是DOM的實現在繪制前是無法得知其高度的,因此也就無法很好地實現分頁的效果。除此之外,還有大表格的分頁渲染效果等等,都變得難以控制。

      因此,這些如果希望對齊word的實現,就必須要用Canvas從頭造一遍。除了這些額外的功能,還有原本的瀏覽器基于DOM實現的基本功能,例如輸入法的支持、復制粘貼的支持、拖拽的支持等等。而基本的Canvas是無法支持這些功能的,特別是輸入法IME的支持,以及文本選區的實現,都需要很復雜的交互實現,這樣的成本自然不會是很容易接受的。

      總結

      在本文我們聊了很多關于富文本編輯器基礎能力的實現,特別是在DOM結構表現和數據結構之間的設計。并且在瀏覽器交互的方案上,我們也聊了ExecCommandContentEditableCanvas實現方案的特點,簡單總結了當前成熟產品以及開源編輯器,并且描述了相關實現的優缺點。

      在后邊我們會基于ContentEditable實現基本的富文本編輯器,首先對于整體的架構設計,以及數據結構的操作做概述。然后開始分別實現具體的模塊,例如輸入模塊、剪貼板模塊、選區模塊等等。實現編輯器從來都不是一件簡單的事情,除了在核心層面的基礎框架設計,應用層上也會有很多問題需要兼容處理,因此這將會是一份大工程,需要慢慢積累。

      每日一題

      參考

      posted @ 2025-04-09 10:33  WindRunnerMax  閱讀(2981)  評論(7)    收藏  舉報
      ?Copyright    @Blog    @WindRunnerMax
      主站蜘蛛池模板: 国产视频一区二区三区四区视频| 视频一区视频二区视频三| 一卡二卡三卡四卡视频区| 9l精品人妻中文字幕色| 人妻少妇无码精品专区| 国内揄拍国内精品人妻久久| 如皋市| 久久99精品久久99日本| 林甸县| 国产中文字幕日韩精品| 国产又黄又爽又不遮挡视频| 嫩草成人AV影院在线观看| 亚洲中文无码av永久不收费| 国产偷窥熟女精品视频大全| 亚洲国产精品人人做人人爱| 久久99久国产麻精品66| 亚洲精品一区二区三天美| 在线看av一区二区三区| 最新精品国偷自产在线美女足| 艳妇乳肉豪妇荡乳xxx| 国产精品一区二区久久精品| 剑川县| 亚洲高清成人av在线| 日本一区二区三区在线 |观看| 成人福利国产午夜AV免费不卡在线| 激情综合网激情综合网五月| 国产亚洲欧洲AⅤ综合一区| 五月综合激情婷婷六月| 欧美日韩一区二区三区视频播放| 国产精品视频一区二区噜| 天堂…中文在线最新版在线| 亚洲男人第一无码av网| 亚洲中文字幕国产综合| 国产一区二区不卡在线| 天堂中文最新版在线官网在线| 久热这里只有精品12| 99九九视频高清在线| 卢湾区| 中国女人高潮hd| 欧美偷窥清纯综合图区| 亚洲伊人久久综合影院|