封装一个基于 WangEditor 的富文本编辑器组件(Vue 3 + TypeScript 实战)

在后台管理系统或 CMS 中,富文本编辑器是必不可少的一环。为了更好地控制交互细节、接口集成和样式定制,我们将基于 Vue 3 + TypeScript 封装一个可复用的富文本组件 —— JEditor,并使用 WangEditor 官方提供的 Vue 适配包。


安装依赖

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

你也可以参考官方文档了解更多配置及插件信息:https://www.wangeditor.com/


完整组件源码

<script setup lang="ts">
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
import { toolbarConfigDefault } from './defaultConfig'
import { ref, watch, shallowRef, onBeforeUnmount } from 'vue'

defineOptions({
  name: 'JEditor',
  inheritAttrs: false
})

type InsertFnType = (url: string, alt: string, href: string) => void

const props = defineProps({
  editorId: {
    type: String,
    default: 'j-editor'
  },
  modelValue: {
    type: String,
    required: true
  },
  height: {
    type: [String, Number],
    default: '400px'
  },
  readonly: {
    type: Boolean,
    default: false
  },
  toolbarConfig: { type: Object as () => IToolbarConfig, default: () => ({}) }, // 工具栏配置
  editorConfig: { type: Object as () => IEditorConfig, default: () => ({}) } // 编辑器配置
})

const emit = defineEmits(['update:modelValue', 'change'])

// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef<IDomEditor | null>(null)

const valueHtml = ref('')

// 编辑器配置
const mergedConfig = {
  placeholder: '请输入内容...',
  readOnly: props.readonly,
  MENU_CONF: {
    // 上传图片配置
    uploadImage: {
      server: 'https://xxxxxxxx', //服务端地址
      maxFileSize: 10 * 1024 * 1024, // 10M
      // 自定义上传参数。参数会被添加到 formData 中,一起上传到服务端。
      meta: {},
      // 自定义增加 http  header
      headers: {
        Accept: '*',
        authorization: 'Bearer xxxxxxxx'
      },
      // 超时时间
      timeout: 5 * 1000,
      // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
      fieldName: 'file',
      // 上传之前触发
      onBeforeUpload(file: File) {
        return file
      },
      // 自定义插入图片
      customInsert(res: any, insertFn: InsertFnType) {
        let d = res.data
        // url(图片 src ,必须) alt(图片描述文字,非必须) href(图片的链接,非必须)
        insertFn(d.fileUrl, d.fileName, d.fileUrl)
      }
    },
    // 上传视频配置
    uploadVideo: {}
  },
  ...props.editorConfig
}

const toolbarConfig = computed(() => Object.assign({}, toolbarConfigDefault, props.toolbarConfig))

const editorStyle = computed(() => {
  return {
    height: isNumber(props.height) ? `${props.height}px` : props.height
  }
})

// 初始化编辑器实例
function initEditor(editorInstance: IDomEditor) {
  editorRef.value = editorInstance
}

// 监听modelValue变化
watch(
  () => props.modelValue,
  (newVal: string) => {
    if (newVal !== valueHtml.value) {
      valueHtml.value = newVal
    }
  },
  {
    immediate: true
  }
)

watch(
  () => valueHtml.value,
  (val: string) => {
    emit('update:modelValue', val)
  }
)
// 编辑器内容变化回调
const handleChange = (editor) => {
  emit('change', editor)
}

// 销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value
  if (editor == null) return
  editor.destroy()
})

const isNumber = (val: any) => {
  return typeof val === 'number' && !isNaN(val)
}
</script>

<template>
  <div class="jMdEditor">
    <Toolbar
      :editorId="editorId"
      :editor="editorRef"
      :defaultConfig="toolbarConfig"
      v-bind="$attrs"
      mode="default"
    />
    <Editor
      :editorId="editorId"
      v-model="valueHtml"
      :defaultConfig="mergedConfig"
      mode="default"
      @onCreated="initEditor"
      @onChange="handleChange"
      :style="editorStyle"
    />
  </div>
</template>

<style scoped lang="scss">
.jMdEditor {
  border: 1px solid #ccc;
  border-radius: 4px;
}
</style>
export const toolbarConfigDefault = {
  toolbarKeys: [
    'headerSelect', // 标题选择
    'bold', // 加粗
    'italic', // 斜体
    'through', // 删除线
    'underline', // 下划线
    'justifyCenter', // 居中对齐
    'justifyJustify', // 两端对齐
    'justifyLeft', // 左对齐
    'justifyRight', // 右对齐
    'bulletedList', // 无序列表
    'numberedList', // 有序列表
    'color', // 文字颜色
    'insertLink', // 插入链接
    'fontSize', // 字体大小
    'lineHeight', // 行高
    'delIndent', // 缩进
    'indent', // 增进
    'divider', // 分割线
    'insertTable', // 插入表格
    'undo', // 撤销
    'redo', // 重做
    'clearStyle', // 清除格式
    'fullScreen', // 全屏
    'blockquote', // 引用
    'codeBlock', // 代码块
    'insertImage', // 插入图片
    'uploadImage', // 上传图片
    'insertVideo' // 插入视频
  ]
}

核心亮点解析

  1. 双向绑定与回显
    使用 v-model + watch 保证外部 modelValue 与内部 valueHtml 同步,组件内容更改后自动向父组件派发更新事件。

  2. 编辑器实例管理
    通过 shallowRef 存储 editorRef,并在 onBeforeUnmount 中销毁实例,避免内存泄漏。

  3. 高度可配置

    • props.editorConfig / props.toolbarConfig 支持用户自定义全部配置。

    • 图片、视频上传功能已集成,示例展示了自定义 customInsert 的典型用法。

  4. 样式灵活
    height 属性既可接收数字(自动追加 px),也可传入字符串(如 %rem 等),满足多种布局需求。


富文本编辑器的可扩展性非常强,你可以在此基础上进一步添加:

  • Markdown 支持

  • 公式与图表插入

  • 代码高亮插件

希望这篇文章能帮助你快速搭建一个可复用的富文本组件,让你的项目编辑体验更加专业、高效!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞天巨兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值