На Хабре периодически появляются статьи про библиотеки для построения своего WYSIWYG редактора. Такая потребность появилась и в моей команде - «билайн дом», для создания новостей. В этой статье взглянем на них более общим взглядом и дополнительно разберем библиотеку LexicalJs.
Эта статья основана на моем выступление на нашем внутреннем frontend митапе, поэтому иногда здесь будут появляться слайды.
WYSIWYG — расшифровывается как What You See Is What You Get, «Что видишь, то и получаешь». В таком редакторе содержание отображается в процессе редактирования и выглядит максимально близко похожим на конечную верстку, которую увидит конечный пользователь. Это очень удобно, если редактор не особо владеет возможностями HTML и CSS. В большинстве случаев те кто пользуются такими редакторами привыкли к MS Word или Google-документы.
WYSIWYG-редакторы появились довольно давно. В разное время были популярны разные редакторы. Также эволюционировали подходы к организации их работы. Давайте попробуем сформировать типы подходов.
Форматирование по принципу html в html. Такой подход был очень популярен раньше. Его принцип в том, что в код вставляется html-тэги которые можно редактировать и пользователь их редактирует. При этом возможно допускаются какие-либо ошибки, еще если вы хотите сделать какой то кастомный компонент, то это становится довольно тяжело. Я на слайде отметил Quill, CkeEditor (старой версии), TinyMCE редактор
Следующий принцип когда-то был очень популярен на форумах. Если вы помните те времена, а, может быть, до сих пор пользуетесь некоторыми форумами, то знаете про bbcode. Это когда есть специальные символы с открывающимся и закрывающимся квадратными скобками. Здесь заметна попытка упростить html-формат и предоставления более дружественного синтаксиса. Хотя, судя по тому, что сейчас такой подход не особо используется, то он не особо прижился. В данном классе редакторов bbcode форматируется в html.
Говоря про WYSIWYG-редакторы, я не могу не упомянуть про подход на основе markdown-разметки. Очень много технически подкованных людей предпочитают вести записи в markdown. Readme все программисты пишут в markdowm. Это на самом деле довольно удобно, и я очень рад что, например, Notion поддерживает markdown, и я часто им пользуюсь, но я не оператор УК и не контент-менеджер. Поэтому этот подход для решения моей задачи на самом деле не подходит.
И наконец самый популярный на сегодня подход - это когда исходное форматирования после цепочек событий преобразуется в дерево данных, а потом в html. При этом прямого доступа к html нет. Такой подход похож на flux и то, что мы как фронтенд-разработчики видим во взаимодействии react и redux. При этом благодаря тому, что у нас имеется дерево, которое можно с легкостью сериализовать в JSON, мы можем удобнее добавлять свои кастомные объекты. Также упрощается возможность навигации по истории изменений. В целом сейчас большинство доступных распространенных редакторов для web используют именно такой подход. На таком подходе остановился и я, поэтому давайте рассмотрим поподробнее его представителей.
Начнем с DraftJs
Думаю, довольно продолжительное время для React-разработчиков это де-факто был стандарт для создания WYSIWYG. Он сильно интегрирован с React и разрабатывался Facebook. Для b2c билайна глубокая интеграция с React не имеет особо большого значения, потому что у нас все проекты на React. Но если вы используете angular, vue или другой фреймворк, то это будет большим стопером.
DraftJs дает довольно гибкое API для взаимодействия, позволяя реализовать все необходимые функции для WYSIWYG. Но обратной стороной этого является то, что необходимо написание довольно большой обвязки из хуков.
Из положительных сторон также можно выделить большое комьюнити и сложившиеся практики использования этой библиотеки.
Из минусов — использование immutable, что в 2024 году уже смотрится устаревше. И самый главный минус, последняя публикация была четыре года назад и библиотека больше не сопровождается и не развивается.
Следующая библиотека — это EditorJs
Лично мне подход, который используется в этой библиотеке, нравится больше всего. Это блочная структура форматирования. Вы могли встретить его, если писали статью во вконтакте или дзене. Модно, молодежно — да, привычно для контент менеджеров и операторов УК — нет.
У такого подхода есть проблема, в том числе в EditorJs. Выделение текста ограничено одним блоком. То есть, если вы нажмете левую кнопку мыши в середине одного параграфа и переместите указатель мыши на середину следующего параграфа, чтобы выделить несколько предложений, то выделение текста остановится на конце первого параграфа. Этого можно избежать при включении специального режима, но это еще кому-то надо показать, обучить.
Дополнительно из плюсов можно отметить, что он фреймворк независимый, и разрабатывается комьюнити, в core-команде которого ребята из России.
ProseMirror
В целом это хороший выбор. Там довольно неплохо спроектировано API автором книги «Выразительный javascript» Марейном Хавербеком. Библиотека является фреймворк-независимой и может использоваться не только для react.
Из минусов там, на мой взгляд, не очень хорошая документация и мало возможностей из коробки.
В комментариях указали на библиотеку, построенную поверх ProseMirror, которая значительно увеличивает возможности из коробки - tiptap.
Теперь давайте перейдем к тому что я выбрал.
Lexical
Lexical — это не только фреймворк, но и платформо-независимая библиотека. Она разрабатывается Facebook и позволяет создать свой собственный WYSIWYG-редактор.
Важно отметить, что Lexical не создан для какой-либо конкретной платформы. Скорее, он разработан таким образом, чтобы быть полностью кроссплатформенным и независящим от фреймворка, что подразумевает, что базовый API можно использовать для мобильных и настольных устройств. Уже есть официально реализованная версия для IOS, например.
Давайте перейдем к возможностям, которые предоставляет Lexical.
Обычный текст
Форматированный текст
Выбор
История
Буфер обмена
Список
Таблицы
Код
Ссылки
Все эти возможности поставляются в виде отдельных пакетов. Так что если вам что-то из этого не нужно, то можно не использовать, экономя размер бандла.
Также важно отметить, что Lexical не ограничивает вид или стиль пользовательского интерфейса редактора. Вы можете настроить внешний вид так, как вам хочется, а Lexical предоставит вам для этого API для удобной работы.
Как мы видим, очень много возможностей. Сами вы можете в этом убедиться перейдя на playground.
Давайте теперь взглянем на архитектуру библиотеки.
Начнем мы с архитектуры подсистем.
Lexical работает, присоединяясь к элементу contentEditable, и оттуда вы можете работать с декларативными API-интерфейсам.
Основное api хранится в пакете lexical, который отвечает за обновление внешнего вида с помощью своего механизма сравнений (упрощенный вариант реконселяции реакта), который позволяет добиться лучшей производительности, узлы, исполнения команд, состояние редактора и некоторые утилиты. При этом состояние хранится в виде дерева, что вы могли видеть в примере работы редактора. Соответственно, само состояние обновляется при помощи команд.
Также lexical расширяется при помощи плагинов. Плагины могут быть как официальные, предоставляемые командой lexical, так и и можно написать свой собственный плагин.
Теперь поговорим о состоянии.
Состояние редактора - это базовая модель данных, которая представляет то, что вы хотите отобразить в DOM. Состояния редактора состоят из двух частей:
дерева лексических узлов
объекта выделения лексики (узлов)
Состояние редактора имутабельно. Для изменения необходимо задиспатчить команду, которая будет обработано либо core-пакетом, либо плагином. Для обработки команд плагином эти команды, конечно же, нужно зарегистрировать. После чего произойдет сравнение состояний и при необходимости оно обновится. Очень похоже на redux, как я уже говорил.
Состояния редактора также полностью преобразуются в JSON и могут быть легко преобразованы обратно в редактор с помощью editor.parseEditorState().
Возможности библиотеки можно расширять при помощи собственных плагинов.
С помощью плагинов можно создавать любые конструкции, расширяющие встроенные возможности редактора. Основная идея плагинов заключается в том, что они обрабатывают определенные команды от пользователя или программы.
Также плагины могут содержать свои собственные стили, функции, иконки и т.д. Плагину доступно все api, предоставляемое core-пакетом. Важно не забыть зарегистрировать свой пакет.
Взаимодействие можно организовывать с помощью расширения существующих узлов. Например, как на картинке. А также можно создавать собственные узлы, как вы видели на примере с эксалидрау.
Резюмируя
WYSIWYG-подход позволяет редактировать текст таким образом, что редактор и пользователь видят его одинаковым.
Существуют различные подходы к организации работы wysiwyg-редактора. Наиболее распространенный подход для web-приложений — построение состояния, хранящегося в виде сериализуемого javascript-объекта, изменяемого при помощи событий, а после все это преобразуется в html.
Из рассмотренных редакторов (DraftJs, Editorjs, ProseMirror, lexical) я выбрал Lexical, потому что:
Библиотека развивается, в отличие от DraftJs
Не имеет недостатка блочных редакторов и привычна для контент-менеджеров и операторов УК
Имеет большое комьюнити, много реализованных модулей и поддержку крупной компании, в отличие от ProseMirror
По размеру эти библиотеки отличаются незначительно, но lexical тоже выигрывает в этом вопросе
P.S. LexicalJs можно рассмотреть более подробнее, например создание своих плагинов. Если будет интересно, то я напишу еще одну более подробную статью.