基于Vue3+Vuetify3的博客自定义Markdown组件开发咨询
Vue3 + Vuetify3 博客编辑组件解决方案
问题1:图片引用方案的合理性与优化建议
你提出的用[image1]这类标记引用图片的方案是可行且易落地的,属于轻量型自定义Markdown扩展,适配博客编辑场景。若要进一步优化,可做以下调整:
- 扩展标记支持图片描述(提升可访问性):比如改成
[image1:海边日落],渲染时将描述作为图片的alt属性 - 约定连续标记自动分组:比如
[image1][image2][image3][image4]会自动分成两组,前3张一行,第4张单独一行
这个方案的核心优势:
- 文本与图片资源分离,存储时只需保存文本内容和图片ID-URL映射,便于后续修改迁移
- 用户学习成本低,无需掌握复杂Markdown语法
- 解析逻辑简单,避免富文本编辑器的沉重体积
问题2:带图片标记的文本转渲染HTML实现方案
核心思路
- 解析文本:把原始文本拆分为「纯文本段」和「图片标记组」
- 映射图片URL:将标记中的图片ID替换为实际上传后的URL
- 分组渲染:连续图片按最多3张/行分组,用Vuetify布局组件实现响应式排列
- 安全渲染:避免直接使用
v-html带来的XSS风险,优先采用组件化渲染
代码实现示例
1. 文本解析工具函数
// utils/parseBlogContent.js export function parseBlogContent(rawText, imageMap) { // 匹配图片标记:[imageX] 或 [imageX:描述] const imageRegex = /\[image(\d+)(?::([^\]]+))?\]/g; const fragments = []; let lastIndex = 0; let match; while ((match = imageRegex.exec(rawText)) !== null) { // 添加纯文本段 if (match.index > lastIndex) { fragments.push({ type: 'text', content: rawText.slice(lastIndex, match.index) }); } // 添加图片项 const imageId = `image${match[1]}`; fragments.push({ type: 'image', url: imageMap[imageId], alt: match[2] || `图片${match[1]}` }); lastIndex = match.index + match[0].length; } // 添加最后一段纯文本 if (lastIndex < rawText.length) { fragments.push({ type: 'text', content: rawText.slice(lastIndex) }); } // 将连续的图片项分组(最多3张一组) const groupedFragments = []; let currentImageGroup = []; fragments.forEach(frag => { if (frag.type === 'image') { currentImageGroup.push(frag); // 满3张或遇到文本时,结束当前分组 if (currentImageGroup.length === 3) { groupedFragments.push({ type: 'image-group', images: currentImageGroup }); currentImageGroup = []; } } else { if (currentImageGroup.length > 0) { groupedFragments.push({ type: 'image-group', images: currentImageGroup }); currentImageGroup = []; } groupedFragments.push(frag); } }); // 处理剩余的图片 if (currentImageGroup.length > 0) { groupedFragments.push({ type: 'image-group', images: currentImageGroup }); } return groupedFragments; }
2. 博客内容渲染组件
<!-- components/BlogRenderer.vue --> <template> <div class="blog-content"> <template v-for="(frag, idx) in parsedContent" :key="idx"> <!-- 纯文本段 --> <span v-if="frag.type === 'text'" class="text-block">{{ frag.content }}</span> <!-- 图片组 --> <div v-else-if="frag.type === 'image-group'" class="image-container d-flex flex-wrap justify-center gap-2"> <div v-for="(img, imgIdx) in frag.images" :key="imgIdx" class="square-image" :style="{ backgroundImage: `url(${img.url})`, backgroundSize: 'cover', backgroundPosition: 'center' }" :title="img.alt" ></div> </div> </template> </div> </template> <script setup> import { computed } from 'vue'; import { parseBlogContent } from '../utils/parseBlogContent'; const props = defineProps({ rawText: { type: String, required: true }, imageMap: { type: Object, required: true } }); const parsedContent = computed(() => { return parseBlogContent(props.rawText, props.imageMap); }); </script> <style scoped> .blog-content { line-height: 1.6; } .text-block { display: block; margin: 0.5rem 0; } .image-container { margin: 1rem 0; } .square-image { width: 180px; height: 180px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } </style>
3. 使用组件示例
<!-- 父组件中调用 --> <template> <BlogRenderer :rawText="userInputText" :imageMap="uploadedImages" /> </template> <script setup> import { ref } from 'vue'; import BlogRenderer from './components/BlogRenderer.vue'; // 用户输入的原始文本 const userInputText = ref("这是第一段文本[image1:海边][image2:山景][image3:城市]这是第二段文本[image4:夜景]"); // 上传后的图片映射(key为图片ID,value为URL) const uploadedImages = ref({ image1: 'https://example.com/image1.jpg', image2: 'https://example.com/image2.jpg', image3: 'https://example.com/image3.jpg', image4: 'https://example.com/image4.jpg' }); </script>
关键注意事项
- XSS防护:若必须用
v-html渲染带格式的文本,一定要用DOMPurify净化后再渲染 - 响应式适配:可将
.square-image宽度改为flex: 0 0 calc(33.333% - 1rem),配合flex-wrap实现移动端自动1张/行 - 上传联动:图片上传后自动生成
[imageX]标记并插入到文本光标位置,提升用户体验
内容的提问来源于stack exchange,提问作者lpares12
相关产品推荐
相关产品推荐

