Slate.js - 革命性的富文本編輯框架

相信不少同窗即使沒有接觸過富文本編輯領域,也必定據說過【富文本編輯是天坑,千萬不要碰】的說法——是的,富文本編輯是天坑,但 Slate 能很好地幫助你。下面會介紹富文本編輯的複雜度所在,以及 Slate 的解決方式。前端

背景

富文本編輯領域和常規的前端開發相比,有個很是微妙的區別:在這個領域裏,最流行的解決方案每每是至關【重】的。爲何在一向推崇【越輕越好】的前端社區,輕量級的編輯器沒有成爲主流呢?這要從編輯器的實現原理提及。git

在瀏覽器中,實現富文本編輯的原理大體可分爲下面這三種:github

  1. <textarea> 上定位各類樣式。這是 Facebook 早期評論系統所使用的。
  2. 實現本身的佈局引擎,連閃爍的光標都是經過 <div> 控制的。這是 Google Docs 所使用的。
  3. 使用瀏覽器原生的 ContentEditable 編輯模式。這是絕大多數現有富文本編輯器所使用的。

三種方案中,第一種連加粗、斜體等操做都很難支持,已經基本棄用;第二種的工做量很是巨大,只有谷歌、微軟這樣可以本身造瀏覽器的巨頭才能玩得好;對於最後一種,若是你不瞭解 contenteditable,你能夠打開任意一個網站,在它的 <body> 標籤里加上這個屬性,而後看看它是怎樣變身爲一個華麗的編輯器的😀看起來這個方式比前兩種都要靠譜許多,瀏覽器已經替你處理好了快捷鍵、撤銷棧、光標、輸入法、兼容性…很體貼啊!瀏覽器

ContentEditable 之殤

她那時候還太年輕,不知道全部命運贈予的禮物,早已在暗中標好了價格。markdown

——茨威格《斷頭王后》數據結構

天下沒有免費的午飯,ContentEditable 也不例外。Medium Editor 的做者寫過一篇文章,介紹了 ContentEditable 的可怕之處。文中的批評能夠歸結爲一句話,即 ContentEditable 的數據結構和行爲缺少一致性架構

好比,對一句【喜迎十九大】,下面的幾種 HTML 表示是徹底等效的:框架

<!--正常-->
<p>喜迎<b>十九大</b></p>
<!--分離的 b 標籤-->
<p>喜迎<b></b><b>九大</b></p>
<!--嵌套的 b 標籤-->
<p>喜迎<b><b>十九大</b></b></p>
<!--空的 b 標籤-->
<p>喜迎<b></b><b></b><b>九大</b></p>
<!--span 代替 b 標籤-->
<p>喜迎<span style="font-weight: bold">十九大</span></p>複製代碼

它們雖然看上去同樣,但對它們的編輯行爲會產生顯著的區別。而在使用 ContentEditable 時,瀏覽器常常會自動插入這些垃圾標籤編輯器

再好比,對於一句【喜迎十九大】,一次簡單的換行操做可能產生這樣的結果:函數

<p>喜迎<br/>十九大</p> <!--插入 br 標籤-->
<p>喜迎</p></p>十九大</p> <!--分割 p 標籤-->複製代碼

不一樣瀏覽器哪怕對於簡單的換行操做,其行爲也是存在各類分歧的。這樣一來,在 Chrome 中編輯的文檔,在 Firefox 中打開繼續編輯後,就頗有可能出現 bug,而這些 bug 並非簡單的樣式問題,而是會破壞數據結構的惡性 bug

社區中有很多所謂的【超輕量級編輯器】,它們幾乎就只是 ContentEditable 加了一層美化的殼。這種編輯器基本徹底依賴瀏覽器的原生行爲,不會顧及 ContentEditable 對數據結構的破壞,基於它們去實現高級的編輯功能是十分困難的。若是抱着【輕量的東西更漂亮】的思路選擇它們,決定前請務必三思。

是應用,是類庫,仍是框架?

另外一個在富文本編輯領域較爲尷尬的問題,是編輯器的定位。通常而言,前端領域接觸的各類項目,不外乎如下三種:

應用 Application

應用泛指包含了界面和交互邏輯的項目,好比各類管理後臺系統。

類庫 Library

類庫提供 API 供用戶調用來開發應用,但並不影響應用的代碼架構,好比 jQuery 和 React:

jQuery: The Write Less, Do More, JavaScript Library

React is a JavaScript library for building user interfaces.

也許很多同窗對 React 有【全家桶】的偏見,在這裏再強調一遍,React 自己僅僅是個視圖層,須要和許多類庫結合,才能用於開發應用。

框架 Framework

框架一樣提供 API,但它對應用代碼有很強的侵入性,須要用戶按照框架的方式,提供代碼供框架運行。Vue 和 Angular 都是典型的框架:

Vue.js - The Progressive
JavaScript Framework

AngularJS — Superheroic JavaScript MVW Framework

那麼,富文本編輯器屬於上面的哪種呢?每一個編輯器項目都會說本身的定位是 Editor,但 Editor 是應用、是類庫、仍是框架呢?許多主打【開箱即用】的編輯器,已經集成了許多樣式和交互邏輯,實際上已是一個應用了

這裏的問題在於,應用的定製性是最差的。於是,在須要定製不一樣的編輯體驗時,許多【開箱即用】的編輯器很難經過簡單的配置來知足需求。這時,每每須要使用各類奇技淫巧,或再學習一套編輯器自身笨拙的插件機制。

Vue 和 Angular 這樣的框架,在易用性上是有口皆碑的。那麼,富文本編輯領域,有沒有這樣的框架呢?有的,而且 Slate 還不是第一個。對編輯器有所瞭解的同窗可能知道,Facebook 出品的 Draft.js 就是一個這樣的編輯框架,能讓你使用 React 技術棧定製本身的編輯器。既然 Draft.js 已經很是出色,那麼 Slate 與之相比,有什麼創新之處呢?而對於上文中 ContentEditable 的各類問題,Slate 又是如何解決的呢?讓咱們來看看吧。

介紹 Slate

Slate 並不是一個編輯器應用,而是一套在 React 和 Immutable 的堅實基礎上,用於操做富文本數據的強大框架。基於 Slate 實現一個富文本編輯器,只至關於使用 React(視圖層)+ Immutable(數據層)開發一個普通 Web 應用。下圖中展現了一個基於 Slate 實現的編輯器架構,數據的流動很是簡單易懂:

editor-arch
editor-arch

圖中,左側視圖層的 Toolbar 工具欄和 Editor 內的各類 Node 都是純粹的 React 組件,右側的模型層則大量應用了 Slate 所提供的支持。下面,咱們簡單介紹一下這個架構中的幾個關鍵角色。

Immutable,迄今最理想的數據結構

咱們知道,JS 對象的屬性是能夠隨意賦值的,也就是 mutable 可變的。而相對地,不可變的數據類型不容許隨意賦值,每次經過 Immutable API 的修改,都會生成一個新的引用。

看起來這並不算什麼,和每次修改都全量複製一份數據比起來並無什麼區別。但 Immutable 的強大之處,在於不一樣引用之間,相同的部分是徹底共享的。這也就意味着,對一棵基於 Immutable 的複雜文檔樹,即使只改變了某一片葉子節點,也會生成一棵新樹,但這棵新樹除了那一片葉子節點外,全部內容都是和原有的樹共享的

這和富文本編輯有什麼關係呢?咱們知道,編輯器的【撤銷】實際上是一個難度很是大的功能,許多定製了撤銷功能的編輯器,很容易出現撤銷先後的狀態不一致的狀況。但有了 Immutable 後,每次編輯都會生成一個全新的編輯器狀態,只需簡單地在不一樣狀態之間切換,就能輕鬆地實現撤銷和重作操做。而且,Immutable 也徹底支持複雜的嵌套來表達文檔的樹形結構。能夠說,Immutable 天生適合用於實現富文本編輯的模型層。在 Slate 和 Draft.js 中,富文本數據就是對 Immutable 的一層封裝,從而自帶了對撤銷操做的支持,不需額外編碼實現。在這方面,Slate 相比 Draft.js 的一個重要加分項是它支持嵌套的數據結構,對錶格等複雜內容的編輯提供了良好的支持。

React,迄今最合適的視圖層

說到 Immutable 就不能不提 React,目前 Immutable.js 這個不可變數據的 JS 庫就是 Facebook 本身實現的,而且一開始引入 Immutable 的目的也不是爲了撤銷,而是爲了優化 React 應用的性能。能夠說,Immutable 和 React 有着天生的默契。

那麼,爲何咱們須要 React 呢?目前,除了 Slate 和 Draft.js 外幾乎全部的編輯器方案,在須要定製編輯節點(如公式、圖表等)時,要麼須要接觸和 DOM 緊密耦合的編輯器插件概念,要麼只能使用編輯器內置的功能。這種作法在學習成本和效率上都不是最優的。

設想一下,若是編輯器中的編輯內容,所有都能以 React 組件的形式(如標題用 Heading 組件,段落用 Paragraph 組件等)來實現,那麼富文本編輯的門檻還會這麼高嗎?從 Immutable 數據映射到一個個 React 組件,是已經在許多 Web 應用中經歷過考驗的成熟模式。而在這種架構下,ContentEditable 那些使人望而生畏的問題也能獲得很好的解決:只須要爲 React 組件增長 contentEditable 屬性,然後對各類按鍵、點擊等事件 preventDefault,由框架決定事件對 Immutable 的變換,最後生成新狀態按需觸發重繪便可!

這種方案下,實現一個編輯器再也不須要精通 DOM 的專家,難度大大下降了。即使像本文做者這樣僅僅熟悉 React,對前端只有一年多經驗的普通開發者,也有能力開發本身的編輯器了。在此稍微夾帶一些私貨:

在富文本編輯領域,React + Immutable 這種在全局粒度全量地更改狀態,然後按需更新組件的方案,比起 Vue 這樣基於依賴追蹤細粒度地更新組件的方案,是更有優點的。Vue 直接 mutate 數據的方式在原理上並不利於實現撤銷與回退,而且函數式組件 VNode 的 API 也沒有 React 這麼直觀易用(Vue 2.5 有改善,但差距仍然存在)。目前,Vue 社區尚未相似的框架出現,這個場景也是 React 技術棧相比 Vue 的一個閃亮之處。

不過,Draft.js 和 Slate 都實現了對 React 的支持。雖然 Slate 定製節點的 API 更方便一些,但這也不是決定性的優點。那麼 Slate 的特殊之處又哪呢?

Slate,迄今最靈活的 Controller

從前面的介紹中,咱們看到至關多創新之處都是來自 Draft.js 的。那麼,Slate 又有什麼獨特之處呢?

Draft.js 有 Immutable 做爲 Model,有 React 做爲 View,但在使用它實現編輯器的過程當中,你可能會感受這比起通常的應用開發來,負擔仍是有些沉重,或者說少了一點什麼東西。嗯,這個東西也許就是你熟悉的 Controller。

即使在前端輪子滿天飛的今天,UI 應用的架構 MVC 也不會過期,而是演化爲了 MVVM 甚至 M-V-Whatever 的架構。編輯器應用一樣是個 UI 應用,咱們一樣須要一種機制,將 Model 和 View 鏈接起來

這可能不是 Draft.js 的閃光之處,它的文檔變換 API 使用起來比較沉重,而且對 EditorState 的修改存在着較多限制。而 Slate 則提供了更加靈活的概念,來鏈接 Model 與 View。咱們簡單介紹一下 Slate 中編輯操做發生時的處理流程:

  1. 用戶在編輯器光標所在的 Node 內按鍵,觸發事件。
  2. 根據按鍵的鍵值,分發不一樣的 Change,如換行、加粗等。
  3. Change 修改 State,生成新 State。
  4. 新 State 通過 Schema 校驗後,渲染到編輯器內,按需更新相應的 Node。

整個流程中最核心的機制可歸納爲一個公式:state.change().change(),Change 是一個很是優雅的 API,全部的變換都是都經過 Change 對象實現的。好比,用戶先插入了文本,又刪除了另外一個段落,這時對文檔的變動就能夠抽象爲:

state.change().insertText().deleteBlock()複製代碼

每一個操做都是鏈式調用!在協同編輯的場景下,來自不一樣用戶的操做其實也能夠歸結爲這樣對 State 的鏈式調用,這也讓基於 Slate 實現協同編輯成爲了可能。另外一方面,每個 Change 鏈式調用中的 API 均可實現爲純函數,然後經過 Slate 的 call API 來鏈式執行,這也讓編寫本身的 Change 並添加單元測試成爲了可能。

這種優雅地處理編輯操做的方式,使得 Slate 可以更簡單地將 Model 與 View 鏈接起來,實現對富文本數據的複雜操做。另外,Slate 支持自定義對狀態的 Schema 校驗規則,能夠添加一些形如【第一個節點必須是 Heading 節點】或者【圖片節點必須包含 src 屬性】的校驗規則,並對異常數據進行過濾。

固然,Slate 中並無 Controller 的概念,不過實際上,基於 Slate 編寫的富文本編輯 Change 操做,和編寫傳統 MVC 應用中 Controller 邏輯的體驗有些接近。換句話說,Slate 把編寫複雜操做邏輯的難度,下降到了編寫 Change 函數的水平。在這一點上,Slate 的架構是十分易用的。

總結

在富文本編輯領域,Slate 是一個後起之秀。不過在推出迄今的短短一年內,它的社區貢獻者數量已經和 Draft.js 甚至 Vue 接近,達到了百人級別。而且,它的 Issue 和 PR 處理比 Draft.js 更加及時,做者對新想法也更加開放,迭代更加活躍。

Slate 的許多核心特性是從其餘優秀編輯器項目中借鑑的,如其 Immutable 數據層與框架理念來自 Draft.js、Schema 與 Change 概念來自 ProseMirror 等。雖然它的許多閃光點單獨看來並不是獨樹一幟,但在宏觀層面上作到了博採衆長(聽起來和 Vue 有些接近?)。目前它還處於快速的迭代中,對有興趣參與的同窗,成爲貢獻者的機會不少哦。

Resources

相關文章
相關標籤/搜索