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