You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js生产构建后react-quill编辑器工具栏不显示问题求助

Next.js生产构建下react-quill工具栏缺失问题修复

问题现象

  • 开发环境:执行next dev启动服务时,react-quill编辑器工具栏、编辑区渲染完全正常,所有功能可用
  • 生产环境:执行next build完成构建后,通过next start启动生产服务,编辑器工具栏无法正常显示,对应功能不可用
  • 前置配置:已使用Next.js的dynamicAPI动态引入编辑器组件,配置ssr: false关闭服务端渲染;集成quill-image-resize实现图片缩放能力,自定义了图片上传至S3的处理逻辑

核心复现代码

富文本编辑器组件(IBRichEditor/index.tsx)

import { useRef, useMemo, useState } from 'react'
import ReactQuill, { Quill } from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { AxiosError } from 'axios'
import { AuthAPIKit } from 'src/hooks/APITools/APIKit'
import ImageResize from 'quill-image-resize'
import { IBRichEditorProps } from './types'
import { useSnackbar } from 'notistack'

Quill.register('modules/ImageResize', ImageResize)

const IBRichEditor = ({ defaultValue, onChangeEditor }: IBRichEditorProps) => {
  const QuillRef = useRef<ReactQuill>()
  const { enqueueSnackbar } = useSnackbar()

  const imageHandler = () => {
    const input = document.createElement('input')
    const formData = new FormData()
    let url = ''

    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    input.click()

    input.onchange = async () => {
      const file = input.files
      if (file !== null) {
        formData.append('files', file[0])

        try {
          const res = await AuthAPIKit.post('/s3Upload', formData, {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          })
          url = res.data.IBparams[0].Location

          const range = QuillRef.current?.getEditor().getSelection()?.index
          if (range !== null && range !== undefined) {
            let quill = QuillRef.current?.getEditor()

            quill?.setSelection(range, 1)

            quill?.clipboard.dangerouslyPasteHTML(
              range,
              `<img src=${url} alt="이미지 태그가 삽입됩니다." />`,
            )
          }

          return { ...res, success: true }
        } catch (error) {
          const err = error as AxiosError
          return { ...err.response, success: false }
        }
      }
    }
  }

  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [{ size: ['small', false, 'large', 'huge'] }, { color: [] }],
          [
            { list: 'ordered' },
            { list: 'bullet' },
            { indent: '-1' },
            { indent: '+1' },
            { align: [] },
          ],
          ['image'],
        ],
        handlers: {
          image: imageHandler,
        },
      },
      ImageResize: {
        parchment: Quill.import('parchment'),
      },
    }),
    [],
  )

  return (
    <>
      <ReactQuill
        ref={QuillRef}
        defaultValue={defaultValue}
        onChange={(content, delta, source, editor) => {
          onChangeEditor(editor.getHTML())
        }}
        modules={modules}
        theme="snow"
        placeholder="test"
      />
    </>
  )
}

IBRichEditor.defaultProps = {}
export default IBRichEditor

页面侧动态引入代码

import dynamic from 'next/dynamic'

const IBRichEditor = dynamic(
  import('src/components/common/IBRichEditor/index'),
  { ssr: false },
)

const NewsUpdateModal = ({
  handleClose,
  open,
  newsItem,
  newsList,
  setNewsList,
}: NewsUpdateModalProps) => {
  // 其余业务逻辑省略
  return (
    <RegContentInputBox>
      <RegInputLabel>News Title</RegInputLabel>
      <RegContentInputQuill>
        <IBRichEditor
          defaultValue={inputData.content}
          onChangeEditor={onChangeEditor}
        />
      </RegContentInputQuill>
    </RegContentInputBox>
  )
}

问题根因

  1. 生产构建默认开启Tree Shaking和代码压缩,会误判Quill内置工具栏依赖的图标、样式资源为未引用代码,直接从构建产物中剔除,导致工具栏只剩空DOM结构,没有样式和图标,视觉上呈现为工具栏缺失
  2. Quill.register插件注册逻辑写在组件模块顶层,动态引入+生产压缩时存在模块加载时序问题:插件注册时Quill核心实例还未完成初始化,导致工具栏模块注册失败
  3. 旧版quill-image-resize包未适配ESM/CJS模块混用场景,生产构建下模块导出格式异常,会连带导致整个Quill模块初始化失败,工具栏配置不生效

修复步骤

按顺序操作即可解决问题:

  1. 调整动态引入配置,增加suspense: true,确保组件仅在客户端环境完全加载后再初始化,避免加载时序问题:
    const IBRichEditor = dynamic(
      import('src/components/common/IBRichEditor/index'),
      { ssr: false, suspense: true },
    )
    
  2. 移除组件顶层的插件注册、静态导入逻辑,将相关操作移到客户端挂载生命周期内执行,确保仅在浏览器环境下完成插件注册:
    // 删掉顶层的 import ImageResize from 'quill-image-resize'、Quill.register代码
    // 在组件内新增useEffect
    import { useEffect } from 'react'
    
    const IBRichEditor = ({ defaultValue, onChangeEditor }: IBRichEditorProps) => {
      const QuillRef = useRef<ReactQuill>()
      useEffect(() => {
        // 客户端挂载后再引入依赖、注册插件
        const ReactQuill = require('react-quill')
        const Quill = ReactQuill.Quill
        const ImageResize = require('quill-image-resize').default
        Quill.register('modules/ImageResize', ImageResize)
      }, [])
      // 其余原有逻辑不变
    }
    
  3. 调整modules配置的生成逻辑,将ImageResize依赖的parchment引入也放到客户端侧,不要在顶层调用Quill.import
  4. 检查next.config.js配置,不要把react-quill、quill加入transpilePackages列表,避免转译过程破坏Quill的模块结构;如果开启了swcMinify,不需要额外关闭,保持默认配置即可
  5. 如果以上操作后仍存在异常,直接替换quill-image-resize为适配React生态的quill-image-resize-module-react,该包修复了生产环境模块兼容问题
  6. 操作完成后删除.next缓存目录,重新执行next build && next start验证效果,避免旧构建缓存干扰结果

内容的提问来源于stack exchange,提问作者June

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 17:45:38