Vue3 + vue-quill实战:从安装到汉化,打造一个带完整图片上传功能的管理后台编辑器
·
Vue3 + vue-quill全栈实战:从零构建企业级富文本编辑器
最近在重构公司CMS系统时,我遇到了一个典型需求:需要一个支持图片上传、样式可控且易于集成的富文本编辑器。经过技术选型,最终选择了Vue3生态下的vue-quill方案。本文将分享从安装配置到生产环境优化的完整实践路径,特别针对新手容易踩坑的环节提供解决方案。
1. 环境准备与基础配置
1.1 依赖安装与版本选择
首先需要明确的是,Vue3环境下需要使用专门适配的@vueup/vue-quill包。以下是推荐安装命令(根据你的包管理器选择):
# 使用npm
npm install @vueup/vue-quill quill-image-uploader --save
# 使用yarn
yarn add @vueup/vue-quill quill-image-uploader
# 使用pnpm
pnpm add @vueup/vue-quill quill-image-uploader
版本兼容性特别重要,我推荐以下组合:
- @vueup/vue-quill@1.0.0+
- quill@2.0.0+
- quill-image-uploader@1.0.3+
提示:安装完成后建议锁定版本,避免后续更新导致API变更
1.2 基础组件封装
创建一个可复用的编辑器组件 RichEditor.vue :
<template>
<div class="rich-editor-container">
<quill-editor
ref="editor"
v-model:content="content"
content-type="html"
:options="editorOption"
@ready="onEditorReady"
/>
</div>
</template>
<script setup>
import { QuillEditor } from '@vueup/vue-quill'
import { ref } from 'vue'
import ImageUploader from 'quill-image-uploader'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
const content = ref('')
const editorOption = ref({
modules: {
toolbar: [
// 工具栏配置将在后续章节展开
]
}
})
const onEditorReady = (quill) => {
console.log('Editor实例:', quill)
}
</script>
2. 图片上传功能深度实现
2.1 配置图片上传模块
默认情况下,vue-quill会将图片转为base64编码,这会导致数据库字段过大。我们需要通过quill-image-uploader扩展实现服务器上传:
// 在setup中添加
import { useUpload } from '@/composables/useUpload'
const { uploadImage } = useUpload()
const editorOption = ref({
modules: {
imageUploader: {
upload: async (file) => {
try {
const { url } = await uploadImage(file)
return url
} catch (error) {
console.error('上传失败:', error)
throw new Error('图片上传失败,请重试')
}
}
}
}
})
对应的上传hook( useUpload.js ):
import { ref } from 'vue'
import axios from 'axios'
export function useUpload() {
const loading = ref(false)
const error = ref(null)
const uploadImage = async (file) => {
loading.value = true
try {
const formData = new FormData()
formData.append('file', file)
const res = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
return res.data
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
return { uploadImage, loading, error }
}
2.2 上传状态反馈优化
为了提升用户体验,我们需要处理上传过程中的各种状态:
<template>
<div class="editor-wrapper">
<div v-if="uploading" class="upload-mask">
<div class="upload-progress">
<el-progress :percentage="progress" />
<p>图片上传中,请稍候...</p>
</div>
</div>
<quill-editor ... />
</div>
</template>
<script setup>
const uploading = ref(false)
const progress = ref(0)
const editorOption = ref({
modules: {
imageUploader: {
upload: async (file) => {
uploading.value = true
progress.value = 30
try {
const { url } = await uploadImage(file)
progress.value = 100
await new Promise(resolve => setTimeout(resolve, 500))
return url
} finally {
uploading.value = false
}
}
}
}
})
</script>
<style scoped>
.upload-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.7);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
</style>
3. 工具栏定制与汉化方案
3.1 完整工具栏配置
以下是电商后台常用的工具栏配置方案:
const editorOption = ref({
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // 粗体、斜体、下划线、删除线
['blockquote', 'code-block'], // 引用、代码块
[{ 'header': [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ 'list': 'ordered'}, { 'list': 'bullet' }], // 有序、无序列表
[{ 'script': 'sub'}, { 'script': 'super' }], // 上标/下标
[{ 'indent': '-1'}, { 'indent': '+1' }], // 缩进
[{ 'direction': 'rtl' }], // 文本方向
[{ 'size': ['small', false, 'large', 'huge'] }], // 字体大小
[{ 'header': [1, 2, 3, 4, 5, 6, false] }], // 标题级别
[{ 'color': [] }, { 'background': [] }], // 字体颜色、背景色
[{ 'font': [] }], // 字体家族
[{ 'align': [] }], // 对齐方式
['clean'], // 清除格式
['link', 'image', 'video'] // 链接、图片、视频
]
}
})
3.2 Vue3下的汉化方案
在Vue3中,由于废弃了 /deep/ 语法,我们需要使用 :deep() 伪类来实现样式穿透:
<style scoped>
/* 全局汉化样式 */
:deep(.ql-snow .ql-tooltip[data-mode=link]::before) {
content: "请输入链接地址:";
}
:deep(.ql-snow .ql-tooltip.ql-editing a.ql-action::after) {
content: '保存';
}
:deep(.ql-snow .ql-picker.ql-header .ql-picker-label::before),
:deep(.ql-snow .ql-picker.ql-header .ql-picker-item::before) {
content: '正文';
}
/* 标题级别汉化 */
:deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before) {
content: '标题1';
}
/* 字体大小汉化 */
:deep(.ql-snow .ql-picker.ql-size .ql-picker-label::before) {
content: '标准';
}
/* 对齐方式汉化 */
:deep(.ql-align-center) {
text-align: center;
}
</style>
4. 高级功能与性能优化
4.1 自定义图片大小限制
在图片上传前添加验证逻辑:
const editorOption = ref({
modules: {
imageUploader: {
upload: async (file) => {
// 限制图片大小不超过5MB
if (file.size > 5 * 1024 * 1024) {
throw new Error('图片大小不能超过5MB')
}
// 限制图片类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('仅支持JPEG、PNG和GIF格式')
}
return await uploadImage(file)
}
}
}
})
4.2 图片压缩方案
对于需要处理大图的场景,可以集成compressorjs进行前端压缩:
import Compressor from 'compressorjs'
const compressImage = (file) => {
return new Promise((resolve, reject) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1920,
maxHeight: 1080,
success(result) {
resolve(result)
},
error(err) {
reject(err)
}
})
})
}
// 在upload方法中使用
const upload = async (file) => {
const compressedFile = await compressImage(file)
return await uploadImage(compressedFile)
}
4.3 与UI框架集成技巧
以Element Plus为例,实现表单验证集成:
<template>
<el-form :model="form" :rules="rules">
<el-form-item label="文章内容" prop="content">
<rich-editor v-model="form.content" />
</el-form-item>
</el-form>
</template>
<script setup>
const form = ref({
content: ''
})
const rules = {
content: [
{
validator: (rule, value, callback) => {
const text = value.replace(/<[^>]+>/g, '').trim()
if (!text) {
return callback(new Error('内容不能为空'))
}
if (text.length < 30) {
return callback(new Error('内容至少30个字符'))
}
callback()
}
}
]
}
</script>
5. 生产环境最佳实践
5.1 错误处理与重试机制
完善的上传错误处理流程:
const uploadWithRetry = async (file, maxRetries = 3) => {
let lastError = null
for (let i = 0; i < maxRetries; i++) {
try {
return await uploadImage(file)
} catch (error) {
lastError = error
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
throw lastError || new Error('上传失败')
}
// 在编辑器配置中使用
const editorOption = ref({
modules: {
imageUploader: {
upload: uploadWithRetry
}
}
})
5.2 内容安全处理
防止XSS攻击的内容过滤方案:
import DOMPurify from 'dompurify'
const sanitizeContent = (html) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'strong', 'em', 'img', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'style']
})
}
// 在提交前处理
const handleSubmit = () => {
const cleanContent = sanitizeContent(form.value.content)
// 提交到服务器...
}
5.3 性能监控与优化
添加编辑器性能监控:
import { onMounted } from 'vue'
onMounted(() => {
const editor = editorRef.value
const startTime = performance.now()
editor.on('text-change', () => {
const now = performance.now()
console.log(`输入延迟: ${now - lastInputTime}ms`)
lastInputTime = now
})
const loadTime = performance.now() - startTime
console.log(`编辑器初始化耗时: ${loadTime}ms`)
// 上报性能数据
reportPerformance({
loadTime,
type: 'rich-editor'
})
})
在实际项目中,我发现将编辑器封装为独立组件后,配合Vue的keep-alive可以显著提升二次打开的性能。同时,对于内容较多的场景,建议实现懒加载策略,只在用户需要编辑时才初始化编辑器实例。
更多推荐



所有评论(0)