Nuxt3, Vue3, CKEditor и прочие WYSIWYG

от автора

В один прекрасный момент мне понадобилось прикрутить 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


ссылка на оригинал статьи https://habr.com/ru/articles/763252/