前端项目中如何实现富文本编辑功能(含源码)

1,988 阅读5分钟

前端项目中如何实现富文本编辑功能(含代码)

开源

个人开源的leno-admin后台管理项目,前端技术栈:reactHooksant-design;后端技术栈:koamysqlredis,整个项目包含web端electron客户端mob移动端template基础模板,能够满足你快速开发一整套后台管理项目;如果你觉得不错,就为作者点个✨star✨吧,你的支持就是对我最大的鼓励;

演示地址

文档地址

源码github地址

前段时间突发异想跑去写一个开源项目,这个项目中前端技术栈使用的是React18ant;后端用的是koa2mysql;从前端webpack的开发环境到后端tsnode开发环境等等都是自己一点一滴搭建起来的;其中也踩了很多坑,也学习成长了很多,项目还在点滴的搭建中;这次的富文本编辑功能也是其中使用到的,找了一些开源的富文本,最终发现了一款国产的富文本编辑器非常好用,而且上手简单,文档清晰,故而写一篇入门文章帮助有同样需求的小伙伴们~🎄

项目地址

富文本编辑功能实现后端部分(附源码)

一、安装

项目需要安装两个包:一个是基础包,另一个是组件包(组件包支持vue2vue3react);

// 基础包
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

// react 组件包
yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save

// Vue2 组件包
yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save

// Vue3 组件包
yarn add @wangeditor/editor-for-vue@next
# 或者 npm install @wangeditor/editor-for-vue@next --save

因为开源项目使用的是react技术栈,所以下面的代码案例均是使用的react; 如果有疑问可直接查看wangEditor官网

二、2-1、封装 TextEditor 组件

将富文本编辑单独封装为一个组件,以保证功能的二次复用,也是为了项目中代码生成功能的扩展;

import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { IDomEditor, IEditorConfig, IToolbarConfig, SlateElement } from '@wangeditor/editor'
// 此处是 导入 @import '@wangeditor/editor/dist/css/style.css'; 样式文件,因为项目中直接导入css文件会导致报错,所以转接了一层scss
import './index.module.scss'
// 上传图片接口,富文本编辑中图片上传功能思路:用户上传图片,获取图片发送后端,后端保存图片返回url地址,前端用url地址访问图片
import { commonUploadImgAPI } from '@/api/modules/common'

type ImageElement = SlateElement & {
  src: string 
  alt: string
  url: string
  href: string
}
// url:图片地址 ;alt:图片描述;href:图片链接
type InsertFnType = (url: string, alt: string, href: string) => void

const TextEditor = (props: { editorHtml: string }, ref: any) => {
  // 用于父组件编辑回显 富文本的内容
  const { editorHtml } = props
  // editor 实例
  const [editor, setEditor] = useState<IDomEditor | null>(null)
  // 编辑器里面的内容
  const [html, setHtml] = useState('')
  // 存储上传的图片的 唯一hash值,后面需要用来与保存的图片进行对比,删除不用保存的图片
  const [uploadedImg, setUploadedImg] = useState<string[]>([])

  // 监听父组件回显的值
  useEffect(() => {
    setHtml(editorHtml)
  }, [editorHtml])

  // ref 用于父组件获取编辑器内的数据项 
  useImperativeHandle(ref, () => ({
    editor,
    html,
    uploadedImg,
  }))

  // 工具栏配置
  const toolbarConfig: Partial<IToolbarConfig> = {}
  
  // 此方法为查看 所有工具栏所有的配置项
  // const toolbar = DomEditor.getToolbar(editor as IDomEditor)
  // if (toolbar) {
  //   const curToolbarConfig = toolbar.getConfig()
  //   console.log(30, curToolbarConfig.toolbarKeys) // 当前菜单排序和分组
  // }
  // 过滤不需要的配置项 填写工具项的key即可
  toolbarConfig.excludeKeys = ['group-video']

  // 编辑器配置 
  const editorConfig: Partial<IEditorConfig> = {
    placeholder: '请输入内容...',
    MENU_CONF: {},
  }

  // 图片上传
  // 1 自定义校验图片
  function customCheckImageFn(src: string, alt: string, url: string): boolean | undefined | string {
    if (!src) {
      return
    }
    if (src.indexOf('http') !== 0) {
      return '图片网址必须以 http/https 开头'
    }
    return true
  }

  // 转换图片链接
  function customParseImageSrc(src: string): string {
    if (src.indexOf('http') !== 0) {
      return `http://${src}`
    }
    return src
  }

  // 上传图片时触发
  if (editorConfig.MENU_CONF) {
    editorConfig.MENU_CONF['uploadImage'] = {
      // file为富文本编辑器内上传图片的图片文件,insertFn是向编辑器内容展示插入<img />标签
      async customUpload(file: File, insertFn: InsertFnType) {
        // 转化为 formdata格式传至后端
        const fd = new FormData()
        fd.append('avatar', file)
        // 获取后端的图片地址 src
        const {
          data: {
            result: { imgUrl },
          },
        } = await commonUploadImgAPI(fd)
        // 插入img,接受三个参数 url:图片地址 ;alt:图片描述;href:图片链接
        insertFn(imgUrl, '', '')
      },
    }

    // 插入图片时触发
    editorConfig.MENU_CONF['insertImage'] = {
      onInsertedImage(imageNode: ImageElement | null) {
        if (imageNode == null) return
        const { src, alt, url, href } = imageNode
        // 将 所有上传到后端图片的 hash标识存储到数组中
        uploadedImg.push(src.split('/')[src.split('/').length - 1])
        setUploadedImg(uploadedImg)
      },
      checkImage: customCheckImageFn,
      parseImageSrc: customParseImageSrc,
    }
    
    // 编辑图片
    editorConfig.MENU_CONF['editImage'] = {
      onUpdatedImage(imageNode: ImageElement | null) {
        if (imageNode == null) return
      },
      checkImage: customCheckImageFn,
      parseImageSrc: customParseImageSrc,
    }
  }

  // 及时销毁 editor ,重要!
  useEffect(() => {
    return () => {
      if (editor == null) return
      editor.destroy()
      setEditor(null)
    }
  }, [editor])

  return (
    <>
      <div style={{ border: '1px solid #ccc', zIndex: 100 }}>
        <Toolbar
          editor={editor}
          defaultConfig={toolbarConfig}
          mode="default"
          style={{ borderBottom: '1px solid #ccc' }}
        />
        {/* editor.getHtml()获取富文本编辑器里面的内容 */}
        <Editor
          defaultConfig={editorConfig}
          value={html}
          onCreated={setEditor}
          onChange={(editor) => setHtml(editor.getHtml())}
          mode="default"
          style={{ height: '400px', overflowY: 'hidden' }}
        />
      </div>
    </>
  )
}

export default forwardRef(TextEditor)

展示效果:

三、父组件保存数据及回显

组件TextEditor只负责功能的实现,其余的保存数据等都需要放置到父组件中;

组件使用

ref用于获取组件内的数据,editorHtml用于编辑器回显

  <Form.Item label="公告内容" name="noticeContent" hidden={false}>
      <TextEditor ref={editorRef} editorHtml={editorHtml} />
  </Form.Item>

确认提交操作

  // 添加编辑 确认
  const addEditFn = async () => {
    const { editor, html, uploadedImg } = editorRef.current as unknown as {
      editor: IDomEditor
      html: string
      uploadedImg: string[]
    }
    // 1 获取富文本保存的 图片
    const saveImgs = editor.getElemsByType('image') as unknown as {
      src: string
    }[]
    // 2 用保存全部图片的 uploadedImg 对比 saveImgs 得出需要删除的img 调用后端接口删除图片
    const delImgs: string[] = []
    uploadedImg.forEach((item) => {
      if (
        !saveImgs.find((value) => value.src.split('/')[value.src.split('/').length - 1] === item)
      ) {
        delImgs.push(item)
      }
    })
    await commonDelImgAPI(delImgs)

    // 3 将 html 存储到 form 表单
    addEditForm.setFieldValue('noticeContent', html)

    // 4 触发表单提交函数,将富文本数据提交到后端
    addEditForm.submit()
  }

四、结语

以上便是在项目中完成了富文本编辑的功能,wangEditor这款插件我个人还是觉得很棒的,是我们国内大佬开源的项目,一直有稳定更新,并且上手都很简单,值得点赞;本文是实现了富文本功能的前端部分,后面会更新一篇富文本后端的部分文章;

wangEditor作者

wangEditor源码