DonaldxDonald

DonaldxDonald

twitter
github

Web リッチテキストエディタの進化

この記事は 2022 年に書かれたもので、ここにアップロードします。

リッチテキストとは?#

リッチテキスト(Rich Text)は、プレーンテキスト(Plain Text)に対して、一般的なフォーマットオプション(例えば、太字や斜体)を使用してフォーマットされたテキストです。

rich-text

Web + リッチテキストエディタ#

vs-code

ご存知の通り、Web は現在最も一般的なプラットフォームであり、コンピュータ、スマートフォン、ゲーム機、自動車、Kindle など、ブラウザさえあればウェブサイトを開くことができます。WebAssembly や Electron などの技術とツールのサポートにより、Web 上で複雑な操作を実行するスーパーアプリ(Figma、VS Code)も次々と登場しています。「一度書けば、どこでも実行できる」という誘惑に直面し、多くの従来のデスクトップアプリも徐々に Web に展開しており、リッチテキストエディタもその一つです。

リッチテキストエディタ = 落とし穴?#

Web フロントエンド業界では、リッチテキストエディタは公認の落とし穴とされています。Web 開発の利点はクロスプラットフォームですが、問題はそのクロスプラットフォームによって引き起こされる互換性の問題です。クロスプラットフォームは、各プラットフォームで動作することができるだけですが、動作する際の問題が落とし穴の所在です。

Web 開発のエディタは、まずフォーカスカーソル選択取り消しスタック、外部からのペースト内容の解析などの問題を処理し、その後異なるブラウザ(Chrome、Firefox、Safari など)との互換性を考慮する必要があります...... 基本的な英語入力が処理できた後には、中国語ユーザーを代表とするユーザーがIME 組み合わせ入力のサポートを要求してきます...... IME 入力の問題を解決した後には、RTL 言語(ヘブライ語、アラビア語)ユーザーが現れ...... モバイルユーザーが現れ...... 共同編集ユーザーが現れ......

知乎の一節を借りると、要約すると次のようになります:

後れを取った生産力と人々の高まる需要との矛盾。

後れを取った生産力:

  1. Web 関連の標準の進展が遅い
  2. ブラウザベンダーによる同じ操作やシーンの実装方法の違いが、互換性の問題を引き起こす
  3. HTML DOM を使用してリッチテキスト内容を記述する際に、制御できない状況が多すぎる

高まる需要:

  1. 不確定なインタラクション意図、例えば Delete キーを押すと、異なるフォーカス位置で異なる状況を考慮する必要がある
  2. コンテンツ入力の多様性、例えば、タイピング、ペースト、ドラッグなどがあり、それぞれの処理が非常に複雑
  3. データの完全性と正確性を保証するために、多くのブラウザのデフォルト動作を阻止し、代理する必要がある
  4. ユーザーのエディタに対する使用要求が高まっており、例えば、セルの結合、リストの多層ネスト、共同編集、バージョン比較、段落注釈など、皆がこれを基本的な要求だと考えているが、実際にはその技術的難易度は皆の想像を超えている。

エディタ技術の段階一覧#

段階説明代表的な製品
L0
  1. ブラウザの DOM API(contenteditable、document.execCommand)に強く依存
  2. ビューはデータである
  1. UEditor
  2. TinyMCE
  3. CKEditor 1 ~ 4
L1
  1. 依然として contenteditable に基づいている
  2. document.execCommand による内容操作を放棄し、自分で実装
  3. リッチテキストエディタの内容と状態を記述するための抽象データモデルを持つ
  1. Quill
  2. Slate
  3. CKEditor 5
  4. Draft.js
  5. ProseMirror
  6. wangEditor v5
L2
  1. contenteditable を放棄し、自分で実装
  2. document.execCommand による内容操作を放棄し、自分で実装
  3. 自分でレイアウトエンジンを実装
  1. Google Docs

L0#

L0 段階のエディタは主にブラウザのネイティブなcontenteditable API に依存して編集を実現し、document.execCommand API を使用して多様な操作(太字、リンクのバインド、コピー&ペーストなど)を実現しています。

利点:

  1. 技術的なハードルが低い。上記の 2 つの API を使用するだけで、ウェブページに編集機能を持たせることができます。
  2. ブラウザのネイティブな編集機能に基づいており、入力が非常にスムーズです。
  3. 面倒な組み合わせ入力の問題がありません。

欠点:

  1. 同じ操作が異なるブラウザで異なる実装を持つ。
  2. リッチテキスト内容の出力が HTML であり、データ管理に不利。
  3. 複雑なリッチテキストの拡張が困難。
  4. 共同編集を実現する方法がない。

L1#

現在、大多数のエディタフレームワークは L1 段階にあり、代表的なものは Quill、Slate、ProseMirror、Draft.js です。これらには 2 つの明確な特徴があります:

  1. 依然としてcontenteditable API に依存して内容を編集していますが、document.execCommand API による内容操作には依存せず、自分で実装しています。
  2. リッチテキストエディタの内容と状態を記述するための抽象データモデルを持っています。

2012 - Quill#

quill

Quill は API 駆動のリッチテキストエディタフレームワークで、すぐに使えるエディタ体験を提供します。

Quill の作者Jason Chenは華人で、Quill は実際には Jason のサイドプロジェクトの一つです。彼は当初、Google Docs のような協力的なエディタを専門にする会社を設立したため、Quill を自分で書いて使用しました。

Quill は DOM ツリーとデータの変更操作を抽象化し、実際の使用時には DOM 操作を行う必要がなく、Quill の API を通じて操作します。対応関係は以下の通りです:

Editor Document ====> Parchment

DOM Node ====> Blot

この抽象化により、元々の DOM への直接操作は Blot への操作に変わり、これらの操作はDeltaで表されます。Quill は Delta の中で DOM のノードツリーの階層を放棄しているため、文字を包むタグやノードの関係は完全に見えず、フラット化された配列opsのみが存在します。

データモデル:

img

{
  "ops": [
    {
      "attributes": {
        "bold": true
      },
      "insert": "Check"
    },
    {
      "insert": " "
    },
    {
      "attributes": {
        "link": "https://donaldxdonald.xyz/"
      },
      "insert": "this"
    },
    {
      "insert": " out ~"
    }
  ]
}

Delta のフラットな構造は、実際には共同編集における OT モデルの一種の実装であるため、Quill は共同編集のために設計されています。フラット化による利点はパフォーマンスの向上に寄与しますが、複雑なネストされたコンテンツを表現する際には苦労することがあります。

Quill の特徴は:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. コンテンツと動作を記述するための抽象データ構造を導入
  3. 共同編集のサポートが良好
  4. 出力構造は文字列または Delta(JSON)ですが、Delta はデータモデルとしての可読性が低い

2015 - ProseMirror#

prosemirror

Marijnは CodeMirror エディタと acorn パーサーの作者で、前者は Chrome と Firefox のデバッグツールに使用され、後者は babel の依存関係です。より多くの収入を得るために、Marijn は新しいプロジェクト、ProseMirror を始めました。

Marijn は当時の市場にあるオープンソースエディタが彼の理想的な方法を採用していないと感じ、多くが古いパラダイムを使用して設計されており、contentEditableを使用していることに不満を持っていました。このため、開発者が文書内容を制御できる範囲が非常に狭く、ユーザーやブラウザによって簡単に変更されることが容易でした。ProseMirror は依然としてcontentEditableに基づいて編集機能を実現していますが、選択ロジックを自分で実装するのは非常に面倒です。

ProseMirror はスキーマ(パラダイム)を持っており、スキーマを定義した後、ProseMirror は自動的にパーサーを実装できます。フレームワークレベルで新しいノードを追加する際に必要な属性やメソッドを定義し、例えばnodeFromJSONメソッドは構造を json に変換し、toDOMメソッドは構造データを DOM に変換する方法を定義します(少し JSX に似ています)。ProseMirror はこの中間層で json データから DOM への変更管理を行います。

データモデル:

img

{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "marks": [
        {
          "type": "strong"
        }
      ],
      "text": "Check"
    },
    {
      "type": "text",
      "text": " "
    },
    {
      "type": "text",
      "marks": [
        {
          "type": "link",
          "attrs": {
            "href": "https://donaldxdonald.xyz/",
            "title": ""
          }
        }
      ],
      "text": "this"
    },
    {
      "type": "text",
      "text": " out ~"
    }
  ]
}

ProseMirror の特徴は:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. より抽象的な json 文書モデル。ProseMirror は構成可能なモデルフレームワークを定義しており、具体的な構造は実際の開発時にカスタマイズできます。
  3. ネストされたツリー構造。複雑な構造のコンテンツをサポート。
  4. 共同編集の良好なサポート。ProseMirror は誕生以来、共同編集のサポートに注目しています。
  5. 1.0 以降は不変データを導入し、エディタのデータ処理に完全なデータフローを持たせ、安定性と制御性を高めました。

2016.02 - Draft.js#

draftjs

当時、Facebook の Meta は Draft.js をオープンソース化しました。同じ会社の製品であるため、Draft.js はビュー層で React を使用して UI をレンダリングしました。これは React とエディタの組み合わせの最初の例であり、React の普及により、ユーザーは Draft.js を基に迅速に二次開発を行うことができました。

Draft.js は外見だけでなく、内部でも React の影響を強く受けており、Redux などの状態管理のEditorStateContentStateを使用し、データ層ではImmutableなどの特性を使用しています。JS オブジェクトの属性は自由に値を設定でき、すなわち可変です。一方で、不変データ型は自由に値を設定できず、Immutable API を通じての変更は常に新しい参照を生成します。

Draft.js の特徴は:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. React を使用してビュー層を実現
  3. コンテンツの保存とレンダリングロジックが分離
  4. 不変データを使用
  5. json に基づくデータモデルを抽象化していますが、ネストされたデータのサポートはやや弱い

2016.06 - Slate#

slate

この時点で市場には多くのエディタが存在していましたが、Ian Storm Taylor は自分の CMS 製品を開発する際に、使いやすいエディタがないと感じました。彼は、これらのエディタが簡単な製品を作るためには良いが、Medium、Google Docs、Dropbox Paper のような大規模なアプリを開発するには非常に難しいと考え、Slate を開発しました。

Slate もまた、すぐに使えるエディタツールではなく、エディタフレームワークです。後輩の Slate は、前のエディタたちの多くの利点を集約しており、Draft.js からは不変データ、プラグインメカニズム、React ビュー層を参考にし、ProseMirror からはネストされたデータ構造とスキーマ制約ルールを借りています。多くのエディタフレームワークのコア機能を統合し、フレームワークの理念が先進的であり、著者がアーキテクチャに対する追求を持っているため(2022 年現在もベータ状態で 1.0 には達していない)、Slate はコミュニティで比較的人気があります。

データモデル:

example-slate

{
  "object": "block",
  "type": "paragraph",
  "nodes": [
    {
      "object": "text",
      "text": "This is editable "
    },
    {
      "object": "text",
      "text": "rich",
      "marks": [{ "type": "bold" }]
    },
    {
      "object": "text",
      "text": " text, "
    },
    {
      "object": "text",
      "text": "much",
      "marks": [{ "type": "italic" }]
    },
    {
      "object": "text",
      "text": " better than a "
    },
    {
      "object": "text",
      "text": "<textarea>",
      "marks": [{ "type": "code" }]
    },
    {
      "object": "text",
      "text": "!"
    }
  ]
}

この時点で Slate の特徴は:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. React を使用してビュー層を実現
  3. ネストされた json データ構造をサポート
  4. 不変データ
  5. プラグインメカニズムがコア

2019 - Slate 0.50+#

Slate はアーキテクチャの大規模な更新を行い、著者は「フレームワーク全体を最初から再考した」と述べています。主な更新点は:

  1. 基本ロジックを Slate Core に抽出し、ビュー層と分離
  2. TypeScript で書き直し
  3. プラグインメカニズムを簡素化し、プラグインがレンダリングロジックと結合しないように
  4. Immutable.js を簡単な json オブジェクトに置き換え
  5. 自身の概念といくつかの Commands をより簡素化し、Transform に改名

データモデル:

example-slate

{
  "type": "paragraph",
  "children": [
    { "text": "This is editable " },
    { "text": "rich", "bold": true },
    { "text": " text, " },
    { "text": "much", "italic": true },
    { "text": " better than a " },
    { "text": "<textarea>", "code": true },
    { "text": "!" }
  ]
}

この時点で Slate の特徴は:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. 非常にシンプルなネストされたデータモデルをサポート
  3. 全体のアーキテクチャは純粋な関数とインターフェースの方式を採用し、思考とコードが非常にシンプル
  4. プラグインメカニズムは強力な機能の開発をサポート
  5. 全体の設計理念は DOM に非常に似ている

2022 - Lexical#

lexical

Draft.js が過去にブラウザとの互換性に多くの手間をかけており、これらは現在あまり必要ではないため、開発者の体験を改善するために(多くの開発者が Draft.js が使いにくいと不満を持っている)、Meta は新しいエディタフレームワーク Lexical をオープンソース化しました。これは Draft.js の置き換えを目的としています。

現時点では特に革新的な概念は見られず、主に他のエディタフレームワークの利点を吸収しています:

  1. ブラウザのネイティブな編集機能contentEditableに依存(L1)
  2. Draft.js のいくつかの概念(EditorState)を保持
  3. React にバインドされず、さまざまなフレームワークでビュー層を実現可能
  4. フレームワーク全体が非常に軽量で、ほとんど他の依存関係がない

L2#

前述のように、ブラウザのcontenteditable API を利用することで、迅速にエディタを開発できますが、さまざまな互換性の問題が多すぎます。現在ほとんどのエディタフレームワークが L1 に留まっているように見えますが、実際には 2010 年に資金力のある Google がブラウザのcontenteditableを放棄し始め、新しい Google Docs は Canvas に基づいて独自のカーソルシステム、テキストレイアウトシステム、フォント解析を開発しました。そのため、独自の抽象データ構造に加えて、編集操作も自分で実装し、編集の表示効果が一貫しているため、共同編集も自然に実現されました。

google-docs

もちろん、Google Docs のコア技術はオープンソースではなく、このような資金を使った作業は自社でしっかりと管理する必要があります。国内の Tencent Docs や WPS などのエディタも L2 に分類され、必ずしも Canvas を使用しているわけではありませんが、考え方は編集レイアウト機能を自分で実装し、ブラウザのcontenteditableを放棄するというものです。

まとめ#

contenteditable はひどいですが、エディタはその使用を最小限に抑えています。より厳しいのは、オペレーティングシステム、ブラウザ、入力法が相互に組み合わさって形成される混乱したエコシステムであり、エディタは制御できませんが、製品はその上に繁栄するエコシステムを期待しています。だからこそ、Web リッチテキストエディタはフロントエンドの落とし穴の一つとされています。

参考リンク#

  1. なぜリッチテキストエディタは落とし穴だと言われるのか? - 知乎
  2. 合理的な時間内に商業サイトで使用できるテキストエディタを独立して開発できる前端エンジニアの割合はどのくらいか? - 知乎
  3. オープンソースリッチテキストエディタ技術の進化(2020 1024)
  4. ContentEditable の困難と解決
  5. Slate.js - 革命的なリッチテキストエディタフレームワーク - 掘金
  6. Jason Chen: ブラウザでのエディタ構築 | JSConf.ar 2014
  7. Marijn Haverbeke: contentEditable を救う:堅牢な WYSIWYG エディタの構築 | JSConf EU 2015
  8. ProseMirror
  9. Facebook が Lexical をオープンソース化、拡張可能なテキストエディタライブラリ - Hacker News
  10. なぜ ContentEditable は恐ろしいのか - OSCHINA - 中国オープンソース技術交流コミュニティ
  11. Google Docs が HTML から Canvas ベースのレンダリングに移行することについてどう思うか? - 知乎
  12. 人気のあるエディタアーキテクチャから Web リッチテキストエディタの困難について考える
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。