В один прекрасный момент мне понадобилось прикрутить WYSIWYG редактор в проект написанный на Nuxt 3. Очень быстро выяснилось что готовых решений полно, но, подавляющее большинство написано для Nuxt 2 и Vue 2, есть немало решений поддерживающих Vue 3, правда прикрутить их в Nuxt 3 это целый квест, о прохождении которого я хотел бы и рассказать.

Для начала вот список того что так или иначе рассматривал как потенциальных кандидатов:

  • CKEditor имеет официальную поддержку Vue 3

  • TipTap в меню слева можно увидеть "Nuxt.js", к сожалению это про Nuxt 2, но установка для Vue 3 полностью работает из коробки и для Nuxt 3. Правда это не то чтобы прям готовый редактор, а скорее заготовка для редакторов, тут надо будет и поверстать, и подобрать иконки для кнопок редактора. В общем как-то мне не подошло.

  • Element Tiptap это редактор основанный на element-ui, но он для Nuxt 2 (Vue 2), правда 2.0.0.1 alpha версия - это Tiptap 2 для Vue 3 на element-plus. Тут я обрадовался т.к. мой проект использует element-plus и вроде бы пазл сошелся, но не тут то было, пару часов танцев с бубном, нормально оживить пациента так и не удалось, жаль.

  • Vue SimpleMDE судя по гитхабу не особо-то и живой, не нашел нормальной демки, до экспериментов так и не дошло. Просто знайте, есть и такой.

  • TipTap Vuetify симпатичное решение, но не хотелось тащить еще и vuetify в проект, оставил про запас. Из коробки подходит для Nuxt 2, про Vue 3 информации в доке нет.

  • mavonEditor markdown редактор, отзывчивый, симпатичный, функциональный, обязательно его где-то задействую, в текущем проекте объяснять юзерам что такое markdown не представляется возможным.

Не буду рассказывать про все грабли на которые пришлось наступить, хочу сразу сделать что-то вроде руководства по CKEditor'у для Nuxt 3, т.к. найти какую-то общую статью по этому вопросу так и не удалось.

Добавим в проект

npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic

Для подключения к проекту создадим плагин - plugins/editor.client.js "client" в имени файла означает mode:"client" (еслиб мы подключали плагин через конфиг). Корень папки plugins сканируется и оттуда все подключается автоматически. Подробнее в доке.

import CKEditor from '@ckeditor/ckeditor5-vue';

export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.vueApp.use(CKEditor)
})

mode:"client" нам нужно для того, чтобы SSR не включал в себя этот плагин, потому что плагин использует всякое вроде window. о чьем существовании SSR не в курсе и мы будем получать ошибки "window is not defined"

Далее, если сделать в компоненте import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; то мы опять столкнемся с ошибками порождаемыми ssr, чтобы их обойти пойдем на хитрость, создаем компонент, например components/editor.vue

<template>
    <div>
        <ckeditor :editor="ClassicEditor" :config="editorConfig" v-model="editorHtml"></ckeditor>
        <div> Content is: <div v-html="editorHtml"></div> </div>
    </div>
</template>
<script setup>
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import '@ckeditor/ckeditor5-build-classic/build/translations/ru';

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const editorConfig = ref({
    language: 'ru'
})

const editorHtml = computed({
    get: () => props.modelValue,
    set: (value) => emit('update:modelValue', value),
})

</script>

Теперь, если мы хотим отрисовать ckeditor делаем это так:

<template>
  <div>
    <ClientOnly>
      <Editor v-model="content" />
    </ClientOnly>
    <div>content is:{{ content }}</div>
  </div>
</template>
<script setup>
import Editor from "~/components/editor"
const content = ref()
</script>

Тут вся магия в компоненте ClientOnly , таким образом SSR не добирается до нашего компонента где вызывается ClassicEditor который и вызывает ошибки. Теперь достаточно обновить конфиг примерно так:

const editorConfig = ref({
    language: 'ru',
    ckfinder: {
        uploadUrl: '/api/file/upload'
    }
})

и мы получаем полноценный редактор, но тут опять "НО", ckfinder не умеет добавить к запросу headers, а мне не хочется костылить эндпоинт без аутентификации для загрузки файлов. В такой эндпоинт вполне возможно кидать токен get параметром и проверять токен на бэке, но если нет, то переходим к сборке собственного билда или к необходимости ставить плагины, дока тут. Для решения проблемы нам понадобится плагин SimpleUploadAdapter, первое что делаем (бегло почитав доку) добавляем плагин в проект (npm install не делаем т.к. он в зависимостях у @ckeditor/ckeditor5-build-classic) , добавим в components/editor.vue

import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';

и получим ошибку ckeditor‑duplicated‑modules. Дело в том, что этот плагин уже импортится в@ckeditor/ckeditor5-build-classic и когда мы используем не готовую сборку, а собираем свою, нам нужно использовать @ckeditor/ckeditor5-editor-classic т.е. не build, а editor. Для наглядности оставил одну кнопку чтобы не растягивать код, собственно новый components/editor.vue

<template>
    <div>
        <ckeditor :editor="ClassicEditor" :config="editorConfig" v-model="editorHtml"></ckeditor>
        <div> Content is: <div v-html="editorHtml"></div> </div>
    </div>
</template>
<script setup>
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload'
import ImagePlugin from '@ckeditor/ckeditor5-image/src/image'
import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'
import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'
import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'
import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'
import '@ckeditor/ckeditor5-build-classic/build/translations/ru';

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const editorConfig = ref({
    language: 'ru',
    plugins: [
        SimpleUploadAdapter,
        ImagePlugin,
        ImageCaptionPlugin,
        ImageToolbarPlugin,
        ImageStylePlugin,
        ImageUploadPlugin
    ],
    toolbar: {
        items: [
            'imageUpload'
        ]
    },
    simpleUpload: {
        uploadUrl: '/api/upload',
        withCredentials: true,
        headers: {
            Authorization: 'Bearer <token>',
        }
    },
})

const editorHtml = computed({
    get: () => props.modelValue,
    set: (value) => emit('update:modelValue', value),
})

</script>

после сборки Nuxt преподносит новый сюрприз,в консоле браузера будет что-то вроде

TypeError: Cannot read properties of null (reading 'getAttribute')

если дебажить дальше то придем к строчке

const viewBox = svg.getAttribute('viewBox')

google мало что про это подскажет, но подобное есть, например тут, потратив еще немного нервов понимаем что дело в vite, оказывается CKEditor об этом в курсе, подробнее тут. А решение такое, сначала делаем

npm install @ckeditor/vite-plugin-ckeditor5 @ckeditor/ckeditor5-theme-lark

потом идем в nuxt.config.ts импортим плагин vite , совсем пустой конфиг будет выглядеть так

import ckeditor5 from '@ckeditor/vite-plugin-ckeditor5'
export default defineNuxtConfig({
  devtools: { enabled: true },
  vite: {
    plugins: [ckeditor5({ theme: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) })]
  }
})

Вот собственно и все, теперь можно собирать свои build'ы, CKEditor довольно мощный инструмент, а для Nuxt 3 практически нет полноценных WYSIWYG редакторов. Потратил много времени и хотелось бы все это обобщить, оставить эту мини-инструкцию. Строго не судите :-)

P.S. бэк для загрузки изображений может возвращать

{
    "url": "https://example.com/images/foo.jpg"
}

или

{
    "urls": {
        "default": "https://example.com/images/foo.jpg",
        "800": "https://example.com/images/foo-800.jpg",
        "1024": "https://example.com/images/foo-1024.jpg",
        "1920": "https://example.com/images/foo-1920.jpg"
    }
}

или

{
    "error": {
        "message": "The image upload failed because the image was too big (max 1.5MB)."
    }
}

Подробнее Simple upload adapter

Комментарии (2)


  1. ArutaGerman
    25.09.2023 14:50

    Ставил я, значится, для ckeditor и билд и не билд... результат один: ругается на уже подключенный модуль. А я всего лишь хотел иметь возможность подключать только нужные плагины, но пришлось отложить до "будет время - поковыряю еще"


    1. levantez Автор
      25.09.2023 14:50

      Если ругается на уже подключенный то это точно значит, что где-то подключен билд, скорее всего в плагине, т.к. в сети везде рекомендуют (зачем-то) билд так же в плагине подключать