【基于 WangEditor v5 + Vue2 封装 CSDN 风格富文本组件】

基于 WangEditor v5 + Vue2 封装 CSDN 风格富文本组件

一、wangEditor封装及使用技术文章大纲

引言
  • 介绍wangEditor的基本信息(轻量级富文本编辑器、开源、功能特点)
  • 封装的目的和意义(提高复用性、统一配置、简化调用)
wangEditor基础集成
  • 通过npm或CDN引入wangEditor
  • 初始化编辑器的基本代码示例
  • 核心配置项说明(如菜单配置、上传图片配置)
封装策略设计
  • 创建独立Vue/React组件封装编辑器
  • 通过props传递配置参数(如工具栏选项、初始内容)
  • 暴露编辑器实例方法(如获取内容、清空内容)
关键功能封装实现
  • 图片/视频上传功能统一处理(对接OSS或后端API)
  • 自定义扩展菜单项的实现方法
  • 内容变化监听与双向数据绑定处理
主题样式定制
  • 覆盖默认样式实现UI主题定制
  • 响应式布局适配方案
  • 暗黑模式等主题切换实现
典型应用场景
  • 表单中的富文本输入场景
  • 与Markdown的双向转换实现
  • 协同编辑场景下的封装注意事项
性能优化建议
  • 懒加载实现方案
  • 大文档编辑时的性能处理
  • 销毁实例避免内存泄漏
常见问题解决方案
  • XSS安全防护配置
  • 粘贴内容格式处理
  • 移动端适配问题修复
测试与部署
  • 单元测试编写要点
  • 构建为独立npm包的方法
  • 版本更新与维护策略
结语
  • 封装带来的收益总结
  • 未来可扩展方向(插件系统、AI集成等)
  • 官方资源与社区支持信息CSDN 风格富文本组件封装(Vue2)
vue 复制代码
<template>
  <div class="csdn-editor-container">
    <!-- 编辑器工具栏(CSDN 风格:简洁+核心功能) -->
    <Toolbar
      :editor="editor"
      :defaultConfig="toolbarConfig"
      :mode="mode"
      style="border-bottom: 1px solid #e5e7eb; background: #f8f9fa; padding: 4px 8px;"
    />
    <!-- 富文本编辑区(CSDN 经典白色背景+代码高亮适配) -->
    <Editor
      v-model="html"
      :defaultConfig="editorConfig"
      :mode="mode"
      style="height: 600px; overflow-y: auto; background: #fff; font-size: 15px; line-height: 1.8;"
      @onCreated="onCreated"
      @onChange="onChange"
      @customPaste="customPaste"
    />
  </div>
</template>

<script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
// 引入代码高亮插件(CSDN 风格高亮)
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css' // CSDN 常用深色代码主题

export default Vue.extend({
  name: 'CsdnWangEditor',
  components: { Editor, Toolbar },
  props: {
    // 双向绑定内容
    value: {
      type: String,
      default: '<p>请输入博客内容...</p>'
    },
    // 编辑模式(default:富文本 / simple:精简模式)
    mode: {
      type: String,
      default: 'default'
    },
    // 最大字数限制(CSDN 默认无限制,可自定义)
    maxLength: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      editor: null,
      html: this.value,
      // 工具栏配置(复刻 CSDN 核心功能)
      toolbarConfig: {
        excludeKeys: [
          'insertVideo', // 隐藏视频插入(CSDN 需单独配置)
          'fullScreen', // 可选:保留/隐藏全屏按钮
          'codeBlock', // 替换为自定义代码块(支持高亮)
          'fontSize', 'fontFamily' // CSDN 默认不显示字体字号工具栏
        ],
        // 自定义工具栏顺序(CSDN 风格)
        order: [
          'bold', 'italic', 'underline', 'strikeThrough', 'sub', 'sup',
          '|', 'fontColor', 'bgColor', 'clearStyle',
          '|', 'header', 'blockquote', 'code',
          '|', 'list', 'todo', 'align', 'lineHeight',
          '|', 'link', 'image', 'table', 'hr',
          '|', 'undo', 'redo', 'preview', 'print'
        ]
      },
      // 编辑器配置(适配 CSDN 场景)
      editorConfig: {
        placeholder: '在这里写下你的技术博客...',
        // 图片上传(CSDN 风格:支持拖拽/粘贴/点击上传)
        MENU_CONF: {
          uploadImage: {
            // 最大文件大小(CSDN 限制 5MB)
            maxFileSize: 5 * 1024 * 1024,
            // 支持的图片格式
            accept: ['image/jpg', 'image/jpeg', 'image/png', 'image/gif'],
            // 自定义上传逻辑(对接 CSDN 图片接口或自己的 OSS)
            async customUpload(file, insertFn) {
              // 示例:模拟 CSDN 图片上传(实际需替换为真实接口)
              const formData = new FormData()
              formData.append('file', file)
              
              // 这里替换为 CSDN 开放接口或自建上传接口
              // const res = await axios.post('CSDN_UPLOAD_API', formData)
              // insertFn(res.data.url) // 上传成功后插入图片

              // 模拟上传成功(测试用)
              setTimeout(() => {
                const mockUrl = `https://picsum.photos/800/400?random=${Math.random()}`
                insertFn(mockUrl)
              }, 1000)
            },
            // 显示上传进度
            onProgress(progress) {
              console.log('图片上传进度:', progress)
            }
          },
          // 代码块配置(支持 CSDN 常用语言)
          codeBlock: {
            languages: [
              { value: 'html', name: 'HTML' },
              { value: 'css', name: 'CSS' },
              { value: 'javascript', name: 'JavaScript' },
              { value: 'vue', name: 'Vue' },
              { value: 'react', name: 'React' },
              { value: 'java', name: 'Java' },
              { value: 'python', name: 'Python' },
              { value: 'sql', name: 'SQL' },
              { value: 'shell', name: 'Shell' }
            ]
          }
        },
        // 字数统计(CSDN 风格:实时显示)
        maxLength: this.maxLength,
        // 粘贴处理(保留 CSDN 格式,过滤无用样式)
        pasteFilterStyle: true,
        pasteIgnoreImg: false, // 允许粘贴图片
        // 自定义粘贴逻辑(如粘贴 Markdown 自动转换)
        customPaste: (editor, event, callback) => {
          const text = event.clipboardData.getData('text/plain')
          // 若粘贴的是 Markdown 文本,可自动转换为 HTML(需引入 markdown-it)
          // const html = markdownit().render(text)
          // editor.insertHtml(html)
          // callback(false) // 阻止默认粘贴
          callback(true) // 保留默认粘贴行为
        }
      }
    }
  },
  watch: {
    value(newVal) {
      this.html = newVal // 双向绑定同步
    }
  },
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor) // 必须用 Object.seal() 避免报错
      // 初始化代码高亮(CSDN 风格)
      this.initCodeHighlight(editor)
    },
    onChange(editor) {
      this.html = editor.getHtml()
      this.$emit('input', this.html) // 同步给父组件
      this.$emit('change', editor)
    },
    // 自定义粘贴处理(适配 CSDN 粘贴逻辑)
    customPaste(editor, event, callback) {
      // 过滤 Word 粘贴的冗余样式(CSDN 常用优化)
      const html = event.clipboardData.getData('text/html')
      if (html.includes('msword')) {
        // 清除 Word 样式
        const cleanHtml = html.replace(/<style[\s\S]*?<\/style>/gi, '')
                              .replace(/class="[^"]*"/gi, '')
                              .replace(/style="[^"]*"/gi, '')
        editor.insertHtml(cleanHtml)
        event.preventDefault()
        callback(false)
        return
      }
      callback(true)
    },
    // 初始化代码高亮(适配 CSDN 代码块风格)
    initCodeHighlight(editor) {
      // 监听代码块变化,触发高亮
      editor.on('codeBlockChange', () => {
        this.$nextTick(() => {
          document.querySelectorAll('pre code').forEach(block => {
            hljs.highlightElement(block)
          })
        })
      })
      // 初始渲染时高亮
      this.$nextTick(() => {
        document.querySelectorAll('pre code').forEach(block => {
          hljs.highlightElement(block)
        })
      })
    }
  },
  beforeDestroy() {
    // 销毁编辑器(避免内存泄漏)
    if (this.editor) {
      this.editor.destroy()
      this.editor = null
    }
  }
})
</script>

<style scoped>
/* CSDN 风格样式优化 */
.csdn-editor-container {
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* 代码块样式优化(贴近 CSDN) */
::v-deep pre {
  background: #282c34 !important;
  border-radius: 4px !important;
  padding: 16px !important;
  margin: 16px 0 !important;
  overflow-x: auto !important;
}
::v-deep code {
  font-family: 'Consolas', 'Monaco', 'Menlo', monospace !important;
  font-size: 14px !important;
}
/* 图片样式(CSDN 居中+边框) */
::v-deep img {
  max-width: 100% !important;
  margin: 16px auto !important;
  display: block !important;
  border: 1px solid #e5e7eb !important;
  border-radius: 4px !important;
  padding: 4px !important;
}
</style>

二、组件使用示例(CSDN 博客编辑页)

vue 复制代码
<template>
  <div class="csdn-blog-edit">
    <!-- CSDN 博客编辑头部(模拟) -->
    <div class="edit-header">
      <input
        v-model="blogTitle"
        placeholder="请输入博客标题(不超过100字)"
        class="blog-title-input"
        maxlength="100"
      >
      <div class="edit-toolbar">
        <button @click="saveDraft" class="btn draft-btn">保存草稿</button>
        <button @click="publishBlog" class="btn publish-btn">发布博客</button>
      </div>
    </div>

    <!-- 分类/标签(模拟 CSDN 侧边栏) -->
    <div class="edit-sidebar">
      <div class="sidebar-item">
        <label>博客分类:</label>
        <select v-model="blogCategory" class="category-select">
          <option value="frontend">前端开发</option>
          <option value="backend">后端开发</option>
          <option value="mobile">移动开发</option>
          <option value="ai">人工智能</option>
          <option value="other">其他分类</option>
        </select>
      </div>
      <div class="sidebar-item">
        <label>博客标签:</label>
        <input v-model="blogTags" placeholder="输入标签,用逗号分隔" class="tags-input">
      </div>
    </div>

    <!-- 核心:CSDN 风格富文本编辑器 -->
    <div class="editor-wrapper">
      <CsdnWangEditor
        v-model="blogContent"
        :maxLength="50000"
        @change="handleContentChange"
      />
    </div>
  </div>
</template>

<script>
import Vue from 'vue'
import CsdnWangEditor from './CsdnWangEditor.vue'

export default Vue.extend({
  components: { CsdnWangEditor },
  data() {
    return {
      blogTitle: '',
      blogContent: '<p>请输入博客内容...</p>',
      blogCategory: 'frontend',
      blogTags: '',
      draftTimer: null // 自动保存草稿定时器
    }
  },
  methods: {
    // 保存草稿(CSDN 自动保存逻辑)
    saveDraft() {
      if (!this.blogTitle.trim()) {
        alert('请先输入博客标题')
        return
      }
      // 模拟保存到本地存储或后端
      localStorage.setItem('csdn_draft', JSON.stringify({
        title: this.blogTitle,
        content: this.blogContent,
        category: this.blogCategory,
        tags: this.blogTags,
        saveTime: new Date().toLocaleString()
      }))
      alert('草稿保存成功!')
    },
    // 发布博客(对接 CSDN 发布接口)
    publishBlog() {
      if (!this.blogTitle.trim() || !this.blogContent.trim()) {
        alert('标题和内容不能为空')
        return
      }
      // 模拟发布逻辑
      const blogData = {
        title: this.blogTitle,
        content: this.blogContent,
        category: this.blogCategory,
        tags: this.blogTags.split(',').map(tag => tag.trim()),
        publishTime: new Date().toLocaleString()
      }
      console.log('发布博客数据:', blogData)
      alert('博客发布成功!')
      // 实际项目中对接 CSDN 开放接口或自己的后端
      // axios.post('CSDN_PUBLISH_API', blogData)
    },
    // 内容变化时触发(自动保存草稿)
    handleContentChange() {
      // 10秒自动保存一次草稿(CSDN 逻辑)
      clearTimeout(this.draftTimer)
      this.draftTimer = setTimeout(() => {
        this.saveDraft()
      }, 10000)
    }
  },
  mounted() {
    // 加载本地草稿(CSDN 自动恢复草稿)
    const draft = localStorage.getItem('csdn_draft')
    if (draft) {
      const draftData = JSON.parse(draft)
      this.blogTitle = draftData.title
      this.blogContent = draftData.content
      this.blogCategory = draftData.category
      this.blogTags = draftData.tags
    }
  },
  beforeDestroy() {
    clearTimeout(this.draftTimer)
  }
})
</script>

<style scoped>
.csdn-blog-edit {
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.edit-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
}
.blog-title-input {
  flex: 1;
  padding: 12px 16px;
  font-size: 20px;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  outline: none;
}
.blog-title-input:focus {
  border-color: #1890ff;
  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.btn {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  border: none;
}
.draft-btn {
  background: #f8f9fa;
  color: #333;
  border: 1px solid #e5e7eb;
}
.publish-btn {
  background: #1890ff;
  color: #fff;
  margin-left: 8px;
}
.edit-sidebar {
  width: 260px;
  align-self: flex-start;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  padding: 16px;
}
.sidebar-item {
  margin-bottom: 16px;
}
.sidebar-item label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  font-size: 14px;
}
.category-select, .tags-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  font-size: 14px;
}
.editor-wrapper {
  flex: 1;
  margin-left: 280px;
  margin-top: -240px;
}
</style>

三、关键适配 CSDN 特性说明

  1. 功能适配

    • 核心工具栏:复刻 CSDN 常用功能(代码块、图片上传、表格、公式等)
    • 代码高亮:使用 highlight.js 实现 CSDN 经典深色代码主题
    • 图片上传:支持拖拽/粘贴/点击上传,适配 CSDN 5MB 大小限制
    • 自动保存:10秒自动保存草稿,支持本地存储恢复
  2. 样式适配

    • 编辑器背景:白色背景+15px 字体,贴近 CSDN 阅读体验
    • 代码块:深色背景+ monospace 字体,优化代码可读性
    • 图片样式:居中显示+边框+内边距,符合 CSDN 图片展示规范
    • 整体布局:模拟 CSDN 编辑页(标题栏+侧边分类+编辑区)
  3. 交互适配

    • 粘贴优化:过滤 Word 冗余样式,支持 Markdown 粘贴转换
    • 字数限制:默认 5 万字,可自定义调整
    • 草稿恢复:加载本地存储的草稿内容,避免内容丢失

四、使用前准备

  1. 安装依赖
bash 复制代码
# 核心依赖
npm install @wangeditor/editor @wangeditor/editor-for-vue --save
# 代码高亮依赖
npm install highlight.js --save
# 可选:Markdown 粘贴转换依赖
npm install markdown-it --save
  1. 接口替换
    • 图片上传:将 customUpload 中的模拟接口替换为 CSDN 开放接口或自建 OSS 接口
    • 博客发布:将 publishBlog 中的模拟逻辑替换为 CSDN 发布接口
    • 草稿保存:可对接 CSDN 草稿接口,替换本地存储逻辑
相关推荐
开发者小天2 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者3 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
杰克尼3 小时前
vue_day04
前端·javascript·vue.js
QuantumLeap丶5 小时前
《uni-app跨平台开发完全指南》- 05 - 基础组件使用
vue.js·微信小程序·uni-app
紫小米5 小时前
Vue 2 和 Vue 3 的区别
前端·javascript·vue.js
程序媛_MISS_zhang_01106 小时前
浏览器开发者工具(尤其是 Vue Devtools 扩展)和 Vuex 的的订阅模式冲突
前端·javascript·vue.js
fruge6 小时前
Vue3.4 Effect 作用域 API 与 React Server Components 实战解析
前端·vue.js·react.js
外公的虱目鱼6 小时前
基于vue-cli前端组件库搭建
前端·vue.js
Sheldon一蓑烟雨任平生8 小时前
Vue3 任务管理器(Pinia 练习)
vue.js·vue3·pinia·任务管理器·pinia 练习