Vue3 + marked + highlight.js 打造实时Markdown编辑器(附完整代码)

张开发
2026/4/8 22:40:29 15 分钟阅读

分享文章

Vue3 + marked + highlight.js 打造实时Markdown编辑器(附完整代码)
Vue3 marked highlight.js 打造企业级Markdown编辑器实战指南在当今内容创作和技术文档编写的场景中Markdown已经成为开发者首选的轻量级标记语言。本文将带你从零开始在Vue3项目中构建一个功能完善、性能优异的实时Markdown编辑器集成marked解析器和highlight.js代码高亮功能适用于技术博客、API文档和知识管理系统等多种场景。1. 环境准备与项目初始化在开始之前确保你已经安装了Node.js建议版本16和Vue CLI或Vite。我们将使用Vite作为构建工具它能够提供更快的开发体验。首先创建一个新的Vue3项目npm create vitelatest vue-markdown-editor --template vue-ts cd vue-markdown-editor npm install接下来安装核心依赖npm install marked highlight.js types/marked types/highlight.js -D为什么选择marked和highlight.js组合marked轻量级Markdown解析器解析速度快highlight.js支持185种编程语言的高亮主题丰富两者都有活跃的社区维护和TypeScript支持2. 基础集成与配置2.1 初始化Marked解析器在src/utils/markdown.ts中创建Marked的配置import { marked } from marked import hljs from highlight.js // 配置marked marked.setOptions({ breaks: true, // 转换换行符为br gfm: true, // 启用GitHub风格的Markdown highlight(code, lang) { const language hljs.getLanguage(lang) ? lang : plaintext return hljs.highlight(code, { language }).value } }) export const parseMarkdown (content: string): string { return marked.parse(content) }2.2 创建编辑器组件新建src/components/MarkdownEditor.vuetemplate div classeditor-container div classeditor-column textarea v-modelrawText classeditor-input placeholder开始编写Markdown... / /div div classpreview-column v-htmlcompiledHtml / /div /template script setup langts import { ref, watch } from vue import { parseMarkdown } from /utils/markdown const rawText ref() const compiledHtml ref() watch(rawText, (newVal) { compiledHtml.value parseMarkdown(newVal) }, { immediate: true }) /script3. 高级功能实现3.1 优化代码高亮性能默认情况下每次输入都会重新高亮所有代码块这对性能有影响。我们可以通过以下方式优化// 在markdown.ts中添加 let lastHighlighted export const parseMarkdown (content: string): string { const html marked.parse(content) // 只在检测到代码块变化时重新高亮 const codeBlocks html.match(/precode.*?[\s\S]*?\/code\/pre/g)?.join() || if (codeBlocks ! lastHighlighted) { lastHighlighted codeBlocks const tempDiv document.createElement(div) tempDiv.innerHTML html tempDiv.querySelectorAll(pre code).forEach((block) { hljs.highlightElement(block as HTMLElement) }) return tempDiv.innerHTML } return html }3.2 添加自定义主题支持highlight.js提供了多种主题我们可以让用户切换首先在public/themes/目录下添加多个CSS主题文件创建主题切换组件template select v-modelcurrentTheme changechangeTheme option valuegithubGitHub/option option valuemonokaiMonokai/option option valuesolarized-lightSolarized Light/option /select /template script setup langts import { ref } from vue const currentTheme ref(github) const changeTheme () { const link document.getElementById(hljs-theme) as HTMLLinkElement link.href /themes/hljs/${currentTheme.value}.min.css } /script4. 安全性与XSS防护直接渲染HTML存在XSS风险我们需要加强安全措施4.1 使用DOMPurify净化HTML安装DOMPurifynpm install dompurify types/dompurify更新markdown解析逻辑import DOMPurify from dompurify export const parseMarkdown (content: string): string { const html marked.parse(content) return DOMPurify.sanitize(html, { ALLOWED_TAGS: [ h1, h2, h3, h4, h5, h6, blockquote, p, a, ul, ol, li, code, pre, em, strong, img, table, thead, tbody, tr, th, td ], ALLOWED_ATTR: [href, src, alt, title, class] }) }4.2 自定义渲染器增强安全性我们可以自定义marked的渲染器来进一步控制输出const renderer new marked.Renderer() // 安全处理链接 renderer.link (href, title, text) { if (!href.startsWith(http)) return text return a href${href} target_blank relnoopener noreferrer${text}/a } // 禁用图片 renderer.image () marked.use({ renderer })5. 编辑器功能增强5.1 添加工具栏实现一个简单的Markdown快捷工具栏template div classtoolbar button clickinsertText(**, **)B/button button clickinsertText(*, *)I/button button clickinsertText([, ](url))Link/button button clickinsertText(\n, \n)Code/button /div /template script setup langts const props defineProps{ textareaRef: HTMLTextAreaElement | null }() const insertText (prefix: string, suffix: string) { if (!props.textareaRef) return const start props.textareaRef.selectionStart const end props.textareaRef.selectionEnd const text props.textareaRef.value const selectedText text.substring(start, end) props.textareaRef.value text.substring(0, start) prefix selectedText suffix text.substring(end) // 重新聚焦并设置光标位置 props.textareaRef.focus() props.textareaRef.selectionStart start prefix.length props.textareaRef.selectionEnd end prefix.length } /script5.2 实现本地存储使用localStorage自动保存内容// 在MarkdownEditor组件中添加 const STORAGE_KEY markdown-content onMounted(() { const saved localStorage.getItem(STORAGE_KEY) if (saved) rawText.value saved }) watch(rawText, (newVal) { localStorage.setItem(STORAGE_KEY, newVal) }, { deep: true })6. 性能优化与最佳实践6.1 防抖处理频繁输入会导致性能问题添加防抖import { debounce } from lodash-es watch(rawText, debounce((newVal) { compiledHtml.value parseMarkdown(newVal) }, 300), { immediate: true })6.2 虚拟滚动优化对于长文档实现虚拟滚动提升性能template div classpreview-container refpreviewContainer div :style{ height: ${totalHeight}px } div v-forchunk in visibleChunks :keychunk.id :style{ transform: translateY(${chunk.offset}px) } v-htmlchunk.html / /div /div /template script setup langts import { computed, ref, onMounted } from vue const props defineProps{ html: string }() const previewContainer refHTMLElement() const scrollTop ref(0) const containerHeight ref(0) // 将HTML分割为多个块 const chunks computed(() { // 实现分块逻辑... }) const visibleChunks computed(() { return chunks.value.filter(chunk chunk.offset scrollTop.value containerHeight.value chunk.offset chunk.height scrollTop.value ) }) onMounted(() { containerHeight.value previewContainer.value?.clientHeight || 0 previewContainer.value?.addEventListener(scroll, () { scrollTop.value previewContainer.value?.scrollTop || 0 }) }) /script7. 部署与生产优化7.1 代码分割按需加载highlight.js语言包// 动态导入语言包 const highlightCode async (code: string, lang: string) { try { await import(highlight.js/lib/languages/${lang}) return hljs.highlight(code, { language: lang }).value } catch { return hljs.highlightAuto(code).value } }7.2 构建优化配置Vite排除不必要的highlight.js语言包// vite.config.js export default defineConfig({ optimizeDeps: { exclude: [highlight.js/lib/languages] } })在实际项目中这个Markdown编辑器已经支持了大部分常用功能包括实时预览、代码高亮、自定义主题和安全防护。根据具体需求你还可以进一步扩展如表格编辑、流程图支持或协同编辑等功能。

更多文章