前言
最近接到一个小的需求:根据后端新返回的数据结构,处理后展示文本内容。
返回的数据结构如下:包含了文本内容和对应的样式信息
{
"result": {
"answer": "[{\"text\":\"\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【查询用户信息】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"您有带身份证吗?我先看看您现在的套餐。\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【介绍副卡】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"副卡10块钱一个月,副卡的共享要到\"},{\"style\":\"font-weight: bold;color: #9FC524\",\"text\":\"次月才生效\"},{\"text\":\",如果现在用要额外收费。超出打电话要\"},{\"style\":\"font-weight: bold;color: #9FC524\",\"text\":\"1毛1分钟\"},{\"text\":\",流量要5块1G。\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【了解使用场景】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"您是打算自己用,还是家人用?\"}]"
}
}
这种结构将文本内容拆分为多个片段,每个片段可以有自己的样式定义。
如果简单地将其作为字符串处理,会丢失所有样式信息和格式。
一、传统的处理方式
一般可能会尝试直接处理这个 JSON 字符串,比如下面这种方式:
if (item.result && item.result.answer) {
const recommendDiv = document.createElement("div");
recommendDiv.style.marginTop = "10px";
recommendDiv.innerHTML = `<div><span style="font-weight:bold;">建议:</span>“${item.result.answer.replace(/\n/g, "<br>")}”</div>`;
div.appendChild(recommendDiv);
}
这种方式存在明显问题:
- 无法解析和应用每个文本片段的样式
- 简单替换换行符的方式不够优雅
- 直接将 JSON 字符串展示出来,用户体验差
二、优雅的解决方案
正确的处理方式应该是先解析 JSON 数据,然后逐个处理每个文本片段,应用对应的样式。
下面是一个完整的页面实现,展示了如何正确解析和展示结构化文本内容:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>结构化文本展示示例</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#51B0E8',
secondary: '#9FC524',
neutral: '#f0f2f5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-card {
@apply bg-white rounded-lg shadow-md p-6 mb-6 transition-all duration-300 hover:shadow-lg;
}
.section-title {
@apply text-xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200;
}
}
</style>
</head>
<body class="bg-gray-50 font-sans text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-3xl">
<header class="mb-8 text-center">
<h1 class="text-3xl font-bold text-gray-800 mb-2">结构化文本展示示例</h1>
<p class="text-gray-600">演示如何解析并显示包含样式和换行的结构化文本内容</p>
</header>
<div class="content-card">
<h2 class="section-title">
<i class="fa fa-file-text-o mr-2 text-primary"></i>后端数据展示
</h2>
<div id="raw-data" class="bg-gray-50 p-4 rounded text-sm font-mono overflow-x-auto text-gray-700"></div>
</div>
<div class="content-card">
<h2 class="section-title">
<i class="fa fa-eye mr-2 text-primary"></i>解析后的展示效果
</h2>
<div id="parsed-content" class="min-h-[200px]"></div>
</div>
<div class="content-card">
<h2 class="section-title">
<i class="fa fa-info-circle mr-2 text-primary"></i>说明
</h2>
<ul class="list-disc pl-5 space-y-2 text-gray-700">
<li>蓝色粗体文本为操作提示</li>
<li>绿色粗体文本为重要说明</li>
<li>自动保留原始数据中的换行效果</li>
<li>所有样式均按照原始数据中的定义进行展示</li>
</ul>
</div>
</div>
<script>
// 模拟后端返回的数据
const mockData = {
result: {
answer: "[{\"text\":\"\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【查询用户信息】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"您有带身份证吗?我先看看您现在的套餐。\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【介绍副卡】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"副卡10块钱一个月,副卡的共享要到\"},{\"style\":\"font-weight: bold;color: #9FC524\",\"text\":\"次月才生效\"},{\"text\":\",如果现在用要额外收费。超出打电话要\"},{\"style\":\"font-weight: bold;color: #9FC524\",\"text\":\"1毛1分钟\"},{\"text\":\",流量要5块1G。\\n\"},{\"style\":\"font-weight: bold;color: #51B0E8\",\"text\":\"【了解使用场景】请说\"},{\"style\":\"color: #51B0E8\",\"text\":\":\"},{\"text\":\"您是打算自己用,还是家人用?\"}]"
}
};
// 显示原始数据
document.getElementById('raw-data').textContent = JSON.stringify(mockData, null, 2);
// 解析并展示结构化内容
function renderStructuredContent(data) {
const container = document.getElementById('parsed-content');
if (data.result && data.result.answer) {
const recommendDiv = document.createElement("div");
recommendDiv.className = "p-4 bg-neutral rounded-lg";
// 解析JSON字符串为对象数组
const answerParts = JSON.parse(data.result.answer);
// 创建容器并添加"建议:"标签
const contentContainer = document.createElement("div");
const labelSpan = document.createElement("span");
labelSpan.className = "font-bold text-gray-800";
labelSpan.textContent = "建议:";
contentContainer.appendChild(labelSpan);
// 遍历每个部分并应用样式
answerParts.forEach(part => {
const span = document.createElement("span");
// 应用样式(如果有的话)
if (part.style) {
const stylePairs = part.style.split(';');
stylePairs.forEach(pair => {
if (pair.trim()) {
const [key, value] = pair.split(':');
// 将CSS属性名转换为驼峰式(如font-weight -> fontWeight)
const styleKey = key.trim().replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
span.style[styleKey] = value.trim();
}
});
}
// 设置文本内容(保留换行效果)
span.textContent = part.text;
contentContainer.appendChild(span);
});
recommendDiv.appendChild(contentContainer);
container.appendChild(recommendDiv);
} else {
container.innerHTML = '<div class="text-gray-500 italic">没有可显示的内容</div>';
}
}
// 页面加载完成后渲染内容
document.addEventListener('DOMContentLoaded', () => {
renderStructuredContent(mockData);
});
</script>
</body>
</html>
效果如下:
三、核心解析逻辑
解析和展示结构化文本的核心逻辑在renderStructuredContent
函数中,主要包含以下步骤:
1、解析 JSON 数据:
使用JSON.parse()
将字符串转换为对象数组
const answerParts = JSON.parse(data.result.answer);
2、创建容器元素:
为整个内容和 "建议:" 标签创建容器
const contentContainer = document.createElement("div");
const labelSpan = document.createElement("span");
labelSpan.className = "font-bold text-gray-800";
labelSpan.textContent = "建议:";
contentContainer.appendChild(labelSpan);
3、遍历文本片段:
为每个文本片段创建 span 元素并应用样式
answerParts.forEach(part => {
const span = document.createElement("span");
// 应用样式
if (part.style) {
const stylePairs = part.style.split(';');
stylePairs.forEach(pair => {
if (pair.trim()) {
const [key, value] = pair.split(':');
// CSS属性名驼峰式转换
const styleKey = key.trim().replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
span.style[styleKey] = value.trim();
}
});
}
// 设置文本内容
span.textContent = part.text;
contentContainer.appendChild(span);
});
4、实现要点解析
- CSS 属性名转换:由于 CSS 中使用连字符(如
font-weight
),而 JavaScript 中需要使用驼峰式(如fontWeight
),因此需要进行转换:
const styleKey = key.trim().replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
- 使用 textContent:使用
textContent
而非innerHTML
设置文本内容,这样可以:
- 自动保留换行符的效果
- 避免 XSS 安全问题
- 确保文本内容被正确转义
- 样式应用:逐个解析样式字符串中的键值对,并应用到对应的元素上,确保每个文本片段都能展示其应有的样式。
四、总结
通过这种方式处理结构化文本内容,我们能够准确地还原后端数据中包含的所有样式信息和格式要求。
关键:对于结构化数据,应该先解析再处理,而不是简单地当作字符串来处理。这种思路可以应用于许多类似的场景,帮助我们写出更优雅、更健壮的代码。