基于 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 特性说明
-
功能适配
- 核心工具栏:复刻 CSDN 常用功能(代码块、图片上传、表格、公式等)
- 代码高亮:使用
highlight.js实现 CSDN 经典深色代码主题 - 图片上传:支持拖拽/粘贴/点击上传,适配 CSDN 5MB 大小限制
- 自动保存:10秒自动保存草稿,支持本地存储恢复
-
样式适配
- 编辑器背景:白色背景+15px 字体,贴近 CSDN 阅读体验
- 代码块:深色背景+ monospace 字体,优化代码可读性
- 图片样式:居中显示+边框+内边距,符合 CSDN 图片展示规范
- 整体布局:模拟 CSDN 编辑页(标题栏+侧边分类+编辑区)
-
交互适配
- 粘贴优化:过滤 Word 冗余样式,支持 Markdown 粘贴转换
- 字数限制:默认 5 万字,可自定义调整
- 草稿恢复:加载本地存储的草稿内容,避免内容丢失
四、使用前准备
- 安装依赖
bash
# 核心依赖
npm install @wangeditor/editor @wangeditor/editor-for-vue --save
# 代码高亮依赖
npm install highlight.js --save
# 可选:Markdown 粘贴转换依赖
npm install markdown-it --save
- 接口替换
- 图片上传:将
customUpload中的模拟接口替换为 CSDN 开放接口或自建 OSS 接口 - 博客发布:将
publishBlog中的模拟逻辑替换为 CSDN 发布接口 - 草稿保存:可对接 CSDN 草稿接口,替换本地存储逻辑
- 图片上传:将