iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

深入 slate.js x 一起打造專屬的富文字編輯器吧!系列 第 5

Day 5. Compare × G1

https://ithelp.ithome.com.tw/upload/images/20210920/20139359bBS9xjqBDA.png

The World of G1


G1 的編輯器在網路還沒進入到前端 framework 御三家(Angular、React、Vue)之前就已經存在了

在 WEB 界 WYSIWYG 圈的一團迷霧之中首先搶得風采的是 TinyMCE 與 CKEditor 這兩包 Library,一直到現在我們依然能時不時看見他們的身影,許多知名企業也都是使用他們作為自家網頁 APP 的 Rich text editor 的核心,可想而知他們在社群發展上經營出的規模之大。

https://ithelp.ithome.com.tw/upload/images/20210920/20139359NOWeSI1pwt.png

CKEditor 背書團隊

https://ithelp.ithome.com.tw/upload/images/20210920/201393597xwBOpmW6K.png

TinyMCE 背書團隊

在各個前端 frameworks 陸續登場以後,它們也接著推出了各式各樣的擴充套件讓使用者能跟這些新興的網頁前端技術自由結合,避免自己淹沒於這來勢洶洶的網頁改革浪潮。

即便如此,身為第一世代編輯器,使用 contenteditable 屬性做為編輯器主要的資料來源依附對象仍讓它們難以在當前愈趨複雜的 Modern web develop 中存活。

上一篇 我們有提到第二世代的編輯器透過自訂義編輯器依賴的 Data model 以及專注於如何將資料轉換為 DOM structure render 於 Web 上。

這邊為了讓讀者更有畫面,我先稍微破梗一下讓大家看看 Slate 餵給 Document model 的資料格式:

https://ithelp.ithome.com.tw/upload/images/20210920/20139359SEWODUxwrr.png

https://ithelp.ithome.com.tw/upload/images/20210920/20139359DzlNrDQN0Q.png

上方截圖取自 Slate 官網提供的 richtext example,稍作比對後我們可以發現 user 主要是透過一包 JSON object 在與 Slate 進行溝通的。餵給 Slate 一包自定義的、符合格式規範的 JSON object 後,會經由一層 Render engine 去決定出渲染在畫面上的最終結果,後續 user 在畫面上與編輯器進行互動也都是間接地操作這包 object ,並再次經由 Render engine 去進行渲染結果的運算。

與上一篇對第二世代編輯器所描述的相同, Slate 依賴於瀏覽器的只有取得當前使用者與頁面互動的即時資訊,如:光標、反白,等,其餘的一切都是由 JS 完成,使用者需要依循著套件的規範與使用邏輯去建立自己的編輯器,這也讓開發的自由度很高。

『對於 Slate 而言, HTML 的 contenteditable 屬性單純就只是 input ,拿來決定 user 能不能打字而已。』

反觀第一世代的 TinyMCE 與 CKEditor 則是完全依賴於 contenteditable 屬性實作的,他們視 HTML document 上的 DOM 資料為『唯一真值表示法( single source of truth )』,並沒有額外拆出一層資料與編輯器進行互動,也因此存取的資料正是網頁上直接能看到的,完整的 HTML tags 。所有樣式與功能也都是 library 事先提供好的。

Library 提供給開發者操作編輯器的 api 也是為了取代 execCommand 所製造的,解決跨瀏覽器相容性問題的 DOM api 操作語法糖,有點像是幫你多包裝一層優化處理 DOM 的轉譯器的感覺?

讓我們再拿另一個第一世代編輯器:百度的 UEditor 。為例,它也是依附於 contenteditable 卻放棄大部分 execCommand 的原生功能,自己實現一套貼近於原生功能語法糖的 apis 提供給開發者使用。

這邊我們以 UEditor 的 bold 操作為例:

同時附上 資料來源

UEditor 自己定義了一個 bold 操作的命令邏輯,可以看到操作的尾端做的事是新建一個 <strong> tag 並插入進 DOM 中。

https://ithelp.ithome.com.tw/upload/images/20210920/20139359GGJpmsJHFZ.jpg

https://ithelp.ithome.com.tw/upload/images/20210920/20139359uC4WoFxpLg.jpg

它同時也實踐了自己的一套 undo 、 redo 功能,透過 array 來記錄 redo 、 undo content:

https://ithelp.ithome.com.tw/upload/images/20210920/201393595sv92n6Q5P.jpg

當觸發 undo 時會直接重新設置 innerHTML 來恢復內容

https://ithelp.ithome.com.tw/upload/images/20210920/20139359BWjXHXrcQw.jpg

但我們可以看到這些功能並沒有被系統性地管理,而且因為是一直 access nature DOM 所以不難想像使用它的可維護性與效能並不會多高。


The Difficulties in G1


第一世代的編輯器幾乎都屬於『儲存的資料為整包結構訂死的 HTML Tags 』的類型,而這種做法延伸出了以下幾點問題,這也是為何它們難以在 Modern Web 之下存活的原因:

  • 違背 Data / View 應該拆開的資料結構原則
  • 因為排版的靈活度大幅降低,因而提高了實現 RWD 的難度
  • 要針對內容做相關性分析時也要先清除這些無意義的雜訊( HTML tags )
  • 要傳遞的數據實在太容易變得肥大了!還記得 JSON 是如何戰勝 XML 的嗎?

再來就是我們上一篇也有提到過的:雖然操作上很容易,客製化的程度卻非常有限,幾乎只能活在他們提供的 plugin 下。難以滿足於當前快速發展、常常蹦出各種新需求的網頁社群。

拿 TinyMCE 官網提供的 Full-Featured init 起手式當作範例,可以從滿滿的 config code 看出它的使用性質就是圍繞在他提供給你的各種 config options 上。

var useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

tinymce.init({
  selector: 'textarea#full-featured',
  plugins: 'print preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link media mediaembed template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker imagetools textpattern noneditable help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable export',
  tinydrive_token_provider: 'URL_TO_YOUR_TOKEN_PROVIDER',
  tinydrive_dropbox_app_key: 'YOUR_DROPBOX_APP_KEY',
  tinydrive_google_drive_key: 'YOUR_GOOGLE_DRIVE_KEY',
  tinydrive_google_drive_client_id: 'YOUR_GOOGLE_DRIVE_CLIENT_ID',
  mobile: {
    plugins: 'print preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link media mediaembed template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker textpattern noneditable help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable'
  },
  menu: {
    tc: {
      title: 'Comments',
      items: 'addcomment showcomments deleteallconversations'
    }
  },
  menubar: 'file edit view insert format tools table tc help',
  toolbar: 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent |  numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | fullscreen  preview save print | insertfile image media pageembed template link anchor codesample | a11ycheck ltr rtl | showcomments addcomment',
  autosave_ask_before_unload: true,
  autosave_interval: '30s',
  autosave_prefix: '{path}{query}-{id}-',
  autosave_restore_when_empty: false,
  autosave_retention: '2m',
  image_advtab: true,
  link_list: [
    { title: 'My page 1', value: 'https://www.tiny.cloud' },
    { title: 'My page 2', value: 'http://www.moxiecode.com' }
  ],
  image_list: [
    { title: 'My page 1', value: 'https://www.tiny.cloud' },
    { title: 'My page 2', value: 'http://www.moxiecode.com' }
  ],
  image_class_list: [
    { title: 'None', value: '' },
    { title: 'Some class', value: 'class-name' }
  ],
  importcss_append: true,
  templates: [
        { title: 'New Table', description: 'creates a new table', content: '<div class="mceTmpl"><table width="98%%"  border="0" cellspacing="0" cellpadding="0"><tr><th scope="col"> </th><th scope="col"> </th></tr><tr><td> </td><td> </td></tr></table></div>' },
    { title: 'Starting my story', description: 'A cure for writers block', content: 'Once upon a time...' },
    { title: 'New list with dates', description: 'New List with dates', content: '<div class="mceTmpl"><span class="cdate">cdate</span><br /><span class="mdate">mdate</span><h2>My List</h2><ul><li></li><li></li></ul></div>' }
  ],
  template_cdate_format: '[Date Created (CDATE): %m/%d/%Y : %H:%M:%S]',
  template_mdate_format: '[Date Modified (MDATE): %m/%d/%Y : %H:%M:%S]',
  height: 600,
  image_caption: true,
  quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
  noneditable_noneditable_class: 'mceNonEditable',
  toolbar_mode: 'sliding',
  spellchecker_ignore_list: ['Ephox', 'Moxiecode'],
  tinycomments_mode: 'embedded',
  content_style: '.mymention{ color: gray; }',
  contextmenu: 'link image imagetools table configurepermanentpen',
  a11y_advanced_options: true,
  skin: useDarkMode ? 'oxide-dark' : 'oxide',
  content_css: useDarkMode ? 'dark' : 'default',
  /*
  The following settings require more configuration than shown here.
  For information on configuring the mentions plugin, see:
  https://www.tiny.cloud/docs/plugins/premium/mentions/.
  */
  mentions_selector: '.mymention',
  mentions_fetch: mentions_fetch,
  mentions_menu_hover: mentions_menu_hover,
  mentions_menu_complete: mentions_menu_complete,
  mentions_select: mentions_select,
  mentions_item_type: 'profile'
});

看得出來這兩種類型的library在使用上的差異真的很大呢!但我認為是各有利弊拉,我想針對舊網頁的支援程度,以及在操作上的容易度肯定是後者更勝一籌的


你分析得沒錯, Tiny 與 CK 對舊網頁的支援程度真的是其他 libraries 所望塵莫及的(畢竟他真的很老嘛XD),操作也相對單純,幾乎就是 plugins 的裝載而已。

而提到操作上的難易程度,我們又可以再把所有 WYSIWYG 相關的 Libraries 分成兩大類:

  • Editor
  • Editor Framework。

前者包含我們這篇文章提到的第一世代編輯器,以及下一篇文章的主角 Quill 都屬於這類型的 Library ,它們主要的賣點在於『直接提供你一個現成的編輯器讓你運用在你的 Web App 上』。

開發者在這類 Library 做開發時起步會較容易,你不一定要寫一堆客製化的 code 因為他們已經提供給你一堆可重用的預設模組了。雖然因為程式碼設計的不同而在客製化程度上有所差異,但相對於後者他們是較難在樣式與功能上做客製化,或是要實現客製化這件事時會經過更多複雜的步驟。

後者就包含整個系列的主角: Slate ,以及 Draft ,他們的理念更貼近於『提供開發者去建立自己的編輯器的框架』。

這類的 Library 基本上就是設計來讓開發者去建立客製化的編輯器的,他們通常會定義好自己的建立規則,並在這些規則下提供各種 api 讓開發者做操作,基本上只要開發者遵循這些規則,想開什麼腦洞功能都沒問題,當然相對的學習曲線就會較高,畢竟要先花時間去學習對方的邏輯嘛~

那我們接下來就要進到 Modern web 的範疇了!會挑出幾個現代網頁社群下較為知名的幾個 Libraries 做比較,但這又是另一項大主題了,我們就留到下一篇吧~


上一篇
Day 4. Compare × Generations
下一篇
Day 6. Compare × G2 × Quill
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言