实现的效果:
遇到过的问题:当我禁止获取缓存时,会发生下面的效果:页面图片抖动、闪动
核心设计原则
四个要点:Web安全、高可用、易拓展和性能
站得高才飞得远,怎么在众多markdown渲染库和清理恶意 HTML的库中技术选型呢?
清理恶意 HTML:选择 DOMPurify。
- 高安全性:DOMPurify 专注于防止 XSS 攻击,能有效清理恶意 HTML。
- 跨平台支持:同时支持浏览器和 Node.js,适合前后端一致的 Markdown 渲染需求。
markdown渲染库:选择markdownIt
- 生态丰富,各种插件都有,拓展性极强。
- start数最多...也说明认可度...
markdownIt基本使用
通过包管理工具安装:
npm install markdown-it --save
markdown-it 的核心流程是:创建解析实例 → 配置选项 → 解析 Markdown 文本 → 生成 HTML。
基础示例
javascript
// 引入 markdown-it
import MarkdownIt from 'markdown-it';
// 创建实例
const md = new MarkdownIt();
// Markdown 文本
const markdownText = `
# 标题 1
## 标题 2
这是一段普通文本,包含 **加粗**、*斜体* 和 \`代码\`。
- 列表项 1
- 列表项 2
[链接](https://example.com)
`;
// 解析为 HTML
const html = md.render(markdownText);
console.log(html);
// 输出:包含对应标签的 HTML 字符串(如 <h1>、<strong>、<ul> 等)
核心配置项
示例:启用 HTML 解析 / 自动换行 / 自动识别链接
const md = MarkdownIt({
html: true, // markdown格式里内嵌html标签,可以进行解析 (默认 false)
breaks: true, // 是否将换行符(\n)解析为 <br> 标签(默认 false)
linkify: true // 自动识别链接(如 www.example.com 转为 <a>)(默认 false)
});
插件拓展
示例:使用 emoji 插件
- 安装插件:
npm install markdown-it-emoji --save
- 使用插件:
import MarkdownIt from 'markdown-it';
import emoji from 'markdown-it-emoji';
const md = MarkdownIt().use(emoji); // 集成 emoji 插件
// 解析包含 emoji 的 Markdown
const html = md.render('Hello :smile:!');
// 输出:<p>Hello 😊!</p>
其他常用插件:
<font style="color:rgb(0, 0, 0);background-color:rgb(249, 250, 251);">markdown-it-table-of-contents</font>:生成目录<font style="color:rgb(0, 0, 0);background-color:rgb(249, 250, 251);">markdown-it-task-lists</font>:支持任务列表(<font style="color:rgb(0, 0, 0);background-color:rgb(249, 250, 251);">- [x] 完成</font>)<font style="color:rgb(0, 0, 0);background-color:rgb(249, 250, 251);">markdown-it-prism</font>:代码块语法高亮(基于 Prism)
自定义渲染规则
自定义代码块(``` 包裹的内容)
例如:为代码块添加自定义类名和容器:
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const token = tokens[idx];
const code = token.content; // 代码内容
const lang = token.info || 'plaintext'; // 代码语言(如 js、html)
// 自定义渲染结构
return `<div class="code-block lang-${lang}">
<pre><code>${md.utils.escapeHtml(code)}</code></pre>
</div>`;
};
DOMPurify基本使用
- 安装 DOMPurify:
npm install dompurify --save
- 渲染后净化 HTML:
import DOMPurify from 'dompurify';
const md = MarkdownIt({ html: true });
const dirtyHtml = md.render(markdownText);
const safeHtml = DOMPurify.sanitize(dirtyHtml); // 净化恶意代码
markdownIt和DOMPurify配合使用
- 给一个很简单的示例吧,主要如何拓展和自定义后续的内容,可以进一步自己去封装
import MarkdownIt from 'markdown-it';
import DOMPurify from 'dompurify';
import { full as emoji } from 'markdown-it-emoji';
export default function useToMarkdownHtml(initialMarkdown) {
let error = null;
// 初始化 MarkdownIt 实例
const md = MarkdownIt({
html: false,
breaks: true,
}).use(emoji);
// 默认的渲染方法
const defaultRender = function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
// 自定义表情符号渲染规则
md.renderer.rules.emoji = function (tokens, idx, options, env, self) {
return defaultRender(tokens, idx, options, env, self);
};
// 自定义链接渲染规则
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const token = tokens[idx];
const hrefIndex = token.attrIndex('href');
token.attrs.splice(hrefIndex, 1); // 删除 href 属性
if (hrefIndex >= 0) {
token.attrPush(['id', currentHref]); // 添加 id 属性
}
return defaultRender(tokens, idx, options, env, self);
};
// 获取渲染后的 HTML
const getHtml = function () {
try {
let htmlContent = md.render(initialMarkdown);
return DOMPurify.sanitize(htmlContent);
} catch (err) {
error = err;
return useInitMarkdown(initialMarkdown);
}
};
return {
html: getHtml(),
error,
};
}
// Markdown 初始化处理
const MARKDOWN_CLEAN_PATTERNS = [
[/\[([^\]]+)\]\(.*?\)/g, '$1'], // 保留链接文字
[/!\[.*?\]\(.*?\)/g, ''], // 移除图片
[/`/g, ''], // 移除代码标记
[/\*/g, ''], // 移除星号
[/#/g, ''], // 移除井号
[/:[a-zA-Z0-9_]+:/g, ''] // 移除表情符号
];
export const useInitMarkdown = (markdown) =>
MARKDOWN_CLEAN_PATTERNS.reduce(
(acc, [pattern, replacement]) => acc.replace(pattern, replacement),
markdown
);
- 关注几个细节:当遇上 a标签的时候,我们对其原本的
href属性是进行了剔除的,增加一个id属性给a标签上,那么跳转的时候就可以自定义跳转的方法了。
jumpLink(e) {
if (e.target.localName.toLowerCase() === 'a') {
const url = e.target.id;
// 自定义跳转方法
}
},
- 实际上
html:false时,是可以不使用DOMPurify.sanitize的,原因是markdownIt此时不会渲染markdown格式内容里面的标签的 - 兜底策略:我们现在采用简单的去除标签来展示,当然你可以看业务要求选择别的更优雅的方式,实际上基本是不会触发这个
catch的,markdownIt解析的时候遇到非标准的也会解析为字符串,那么你就可以根据业务要求来自定义这个错误的捕获
一直在全量更新渲染,你知道吗?
就拿 vue3来进行举例吧,使用以上进行解析之后得到的是一个完整的 html结构,使用 v-html进行插入,然后 v-html实际上就是直接操作 DOM 的 innerHTML属性,将字符串解析为 HTML 并插入到目标元素中。
当渲染更新时,vue 在 diff的时候是无法顾及 v-html插入的内容的,也就是不会转为 vNode。
当我禁止获取缓存时,会发生下面的效果:页面图片抖动、闪动
在使用缓存情况下,如果markdown只有纯文字和图片的话问题不大。
但是如果有echarts图、视频的话,那这些元素就会被重复渲染,以至于甚至会导致闪烁和卡顿。
局部更新具体方案
- 既然
vue不会将其转为 vnode,那么我们进行手动转,流程如下 - markdown -> html -> ast -> vnode
Vue3 现成的组件方案:github.com/ttLeslie/vu…