markdown-it
依赖
js
复制代码
"markdown-it": "^14.1.0",
"@types/markdown-it": "^14.1.2",
/* 瞄点 */
"markdown-it-anchor": "^9.2.0",
"markdown-it-toc-done-right": "^4.2.0",
js
复制代码
/* 代码高亮 */
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
初始化
js
复制代码
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
highlight: (code, lang) => {
// 代码高亮处理
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value
} catch (_) {}
}
return ''
},
})
md.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.linkInsideHeader({
symbol: '',
placement: 'after',
class: 'header-anchor',
}),
}).use(markdownItTocDoneRight, {
containerClass: 'toc-container', // TOC 容器的 CSS 类名
includeLevel: [1, 2, 3], // 包含 h1~h3 级标题[5,10](@ref)
})
读取文件
js
复制代码
const loadRemoteMd = async () => {
if (!props.content) return
try {
const response = await fetch(
`${import.meta.env.VITE_APP_BASE_API}${props.content}`
)
const text = await response.text()
// 渲染带 TOC 的 HTML
const htmlWithToc = md.render('[TOC]\n' + text)
const { bodyHtml: body } = extractToc(htmlWithToc)
bodyHtml.value = body
} catch (error) {
message.error('加载失败')
}
}
解析html
js
复制代码
const extractToc = (html: string) => {
// 用 DOM 解析更安全
const div = document.createElement('div')
div.innerHTML = html
const toc = div.querySelector('.toc-container')
const tocContent = toc ? toc.outerHTML : ''
if (toc) toc.remove()
stepItems.value = extractStepsFromMarkdown(tocContent)
return { bodyHtml: div.innerHTML }
}
// 解析瞄点,生成stepItems
const extractStepsFromMarkdown = (mdText: string) => {
// 匹配 # 一级标题
let match
const result = []
const regex = /<a\b[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g
while ((match = regex.exec(mdText)) !== null) {
result.push({ href: match[1], title: match[2] })
}
return result
}
自定义瞄点
html
复制代码
<template>
<div class="markdown-steps">
<ul>
<li
v-for="(item, index) in stepItems"
:key="item.href"
@click="handleStepClick(index)"
>
<div>
<span
class="step-icon"
:class="{ 'step-icon-active': selectedId === index }"
></span>
<a
:href="item.href"
class="step-title"
:class="{ 'step-title-active': selectedId === index }"
>{{ item.title }}</a
>
</div>
<div
v-if="stepItems[stepItems.length - 1] !== item"
class="step-line"
></div>
</li>
</ul>
</div>
<div
class="markdown-body"
v-html="bodyHtml"
></div>
</template>
点击瞄点
js
复制代码
const stepItems = ref<{ href: string; title: string }[]>([])
const bodyHtml = ref('')
const selectedId = ref(0)
const handleStepClick = (index: number) => {
selectedId.value = index
}
监听瞄点
js
复制代码
/* 页面滚动时 */
// 监听锚点
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const activeId = entry.target.id
// 根据锚点ID找到对应的step索引
const stepIndex = stepItems.value.findIndex(
(item) => item.href === `#${activeId}`
)
if (stepIndex !== -1) {
selectedId.value = stepIndex
}
}
})
},
{
threshold: 0.5, // 元素50%进入视口时触发
rootMargin: '0px 0px -60% 0px', // 调整检测区域,更精确地检测视口中部
}
)
// 观察所有标题元素
const observeHeadings = () => {
const markdownBody = document.querySelector('.markdown-body')
if (!markdownBody) return
const headings = markdownBody.querySelectorAll('h1, h2, h3')
headings.forEach((heading) => {
observer.observe(heading)
})
}
onMounted(() => {
loadRemoteMd().then(() => {
// 等待DOM渲染完成后观察标题
setTimeout(() => {
observeHeadings()
// 设置初始选中项
if (stepItems.value.length > 0) {
selectedId.value = 0
}
}, 200)
})
})
// 组件卸载时清理observer
onUnmounted(() => {
observer.disconnect()
})
样式
less
复制代码
<style scoped lang="less">
.markdown-steps {
position: fixed;
top: 80px;
right: 100px;
z-index: 100;
.step-icon {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
background-color: rgba(0, 0, 0, 0.3);
margin-right: 10px;
}
.step-line {
width: 1px;
height: 30px;
position: relative;
left: 5px;
background-color: rgba(0, 0, 0, 0.3);
}
.step-title {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
transition:
background 0.2s,
color 0.2s;
}
.step-title-active {
color: var(--color-primary);
}
.step-icon-active {
background-color: var(--color-primary);
}
}
.markdown-body {
padding: 20px;
border-radius: 10px;
width: 80%;
position: relative;
left: 0;
}
</style>
css
复制代码
/* 问题 */
代码块的背景色,需要到global.css中定义?
/* 代码块的背景色 */
.markdown-body pre {
background: rgba(17, 17, 17, 0.9) !important;
padding: 8px;
border-radius: 4px;
color: rgba(255, 255, 255, 0.9) !important;
}
安全
措施 |
防护目标 |
实现难度 |
禁用 HTML |
阻断脚本注入 |
⭐⭐ |
链接属性控制 |
防止钓鱼与 opener 攻击 |
⭐⭐ |
HTML 净化 (DOMPurify) |
过滤危险标签/属性 |
⭐⭐⭐ |
CSP 策略 |
限制外部资源加载 |
⭐⭐⭐⭐ |
输入预处理 |
早期拦截恶意内容 |
⭐⭐ |