
本文详解如何在 React 中正确解析字符串中的 换行符,并支持基于缩进(-)和序号(#)的列表语法,实现安全、可扩展的富文本自动渲染。
本文详解如何在 react 中正确解析字符串中的 ` ` 换行符,并支持基于缩进(`-`)和序号(`#`)的列表语法,实现安全、可扩展的富文本自动渲染。
在 React 中,直接在 JSX 属性中书写带转义字符的字符串(如 <mycomponent text="
Bubble
Air
Sky"></mycomponent>)时,
不会被 JavaScript 解析为实际换行符,而是作为字面量 \n 传入组件 —— 这是因为 JSX 属性值属于 HTML 字符串上下文,而非 JS 字符串字面量。而通过 JS 变量传入(如 text={'
Bubble
Air
Sky'})则能正常解析换行符。因此,关键前提始终是:确保传入组件的 text 值已是已解析的、含真实
字符的字符串。
以下是一个生产就绪的 FormattedText 组件,它接收纯文本(含
),并按规则自动转换为结构化 HTML:
class FormattedText extends React.Component {
formatText = (text) => {
if (!text || typeof text !== 'string') return '';
const lines = text.trim().split('
');
let result = [];
const ulStack = []; // 记录当前嵌套的无序列表层级
const olStack = []; // 记录当前嵌套的有序列表层级
lines.forEach((line) => {
const trimmed = line.trim();
if (!trimmed) return; // 跳过空行
// 无序列表:以一个或多个 '-' 开头,后跟空格和内容
if (/^-+s+/.test(line)) {
const level = line.match(/^-+/)[0].length;
const content = line.replace(/^-+s+/, '').trim();
// 关闭比当前层级深的所有 <ul>
while (ulStack.length > level) {
result.push('</ul>');
ulStack.pop();
}
// 打开新层级的 <ul>(若需要)
if (ulStack.length < level) {
result.push('<ul>');
ulStack.push(level);
}
result.push(`<li>${this.escapeHtml(content)}</li>`);
// 有序列表:以一个或多个 '#' 开头,后跟空格和内容
} else if (/^#+s+/.test(line)) {
const level = line.match(/^#+/)[0].length;
const content = line.replace(/^#+s+/, '').trim();
while (olStack.length > level) {
result.push('</ol>');
olStack.pop();
}
if (olStack.length < level) {
result.push('<ol>');
olStack.push(level);
}
result.push(`<li>${this.escapeHtml(content)}</li>`);
// 普通段落:每行转为带 <br/> 的内联文本(可替换为 <p> 标签以支持块级语义)
} else {
result.push(`${this.escapeHtml(trimmed)}<br/>`);
}
});
// 清理未闭合的列表
while (ulStack.length > 0) {
result.push('</ul>');
ulStack.pop();
}
while (olStack.length > 0) {
result.push('</ol>');
olStack.pop();
}
return result.join('');
};
// 防止 XSS:对用户输入内容做基础 HTML 实体转义
escapeHtml = (str) => {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
render() {
const { text } = this.props;
return (
<div dangerouslySetInnerHTML={{ __html: this.formatText(text) }} />
);
}
}✅ 使用示例(注意:必须通过 JS 字符串变量传入,确保
已解析):
将选定的 Obsidian 笔记发布为静态站点并部署到 Cloudflare Pages。
const App = () => {
const example1 = '
Bubble
Air
SKY';
const example2 = '
-Bubble
-Air
-SKY';
const example3 = '
#Bubble
#Air
#SKY';
const example4 = '
-Bubble
--Air
---SKY';
return (
<div>
<h3>纯换行</h3>
<FormattedText text={example1} />
<h3>无序列表</h3>
<FormattedText text={example2} />
<h3>有序列表</h3>
<FormattedText text={example3} />
<h3>嵌套列表</h3>
<FormattedText text={example4} />
</div>
);
};⚠️ 重要注意事项:
-
dangerouslySetInnerHTML存在 XSS 风险,绝不可直接渲染未经清洗的用户输入。本例中已内置escapeHtml()方法对, <code>>,&等关键字符转义,但复杂场景建议使用DOMPurify进行完整 HTML 净化。 - 若需支持更多 Markdown 语法(如粗体、链接、代码块),推荐集成成熟库如
react-markdown或marked,而非手动扩展正则逻辑。 - JSX 属性中无法原生解析
,务必避免<formattedtext text=" A B"></formattedtext>写法;应统一通过变量或模板字符串传入:text={A B}。
该方案兼顾简洁性与可维护性,适用于轻量级文档展示、配置说明、后台管理文案等场景。

















