React 中如何部分隐藏 external api 返回的 HTML 内容
结论
可以在渲染前把 API 返回的 HTML 转换成“预览 HTML”。达到访问次数限制后,页面只渲染前几个段落,并提示用户登录、订阅或解锁。
不要直接用 substring() 截断 HTML 字符串,否则可能从标签中间切断,导致页面结构错乱。还要注意,如果浏览器已经收到完整内容,前端隐藏只能改变显示效果,不能真正保护内容。需要访问控制时,后端应当只返回用户有权查看的部分。
推荐实现:按 HTML 节点生成预览
可以用 DOMParser 解析 HTML,并保留指定数量的顶层内容节点:
import { useEffect, useMemo, useState } from "react";
const VISIT_LIMIT = 5;
const PREVIEW_NODE_COUNT = 3;
function createPreviewHtml(html, nodeCount = PREVIEW_NODE_COUNT) {
if (!html || typeof window === "undefined") {
return "";
}
const document = new DOMParser().parseFromString(html, "text/html");
const preview = document.createElement("div");
Array.from(document.body.children)
.slice(0, nodeCount)
.forEach((element) => {
preview.appendChild(element.cloneNode(true));
});
return preview.innerHTML;
}
export default function PostContent({ postDetails }) {
const [visitCount, setVisitCount] = useState(0);
const content = postDetails?.content ?? "";
useEffect(() => {
const storageKey = "siteVisitCount";
const storedValue = Number.parseInt(
localStorage.getItem(storageKey) ?? "0",
10
);
const currentCount = Number.isFinite(storedValue)
? storedValue + 1
: 1;
localStorage.setItem(storageKey, String(currentCount));
setVisitCount(currentCount);
}, []);
const isRestricted = visitCount >= VISIT_LIMIT;
const visibleContent = useMemo(() => {
return isRestricted
? createPreviewHtml(content)
: content;
}, [content, isRestricted]);
return (
<div className="col col-sm-12 col-md-12 col-lg-12">
<div dangerouslySetInnerHTML={{ __html: visibleContent }} />
{isRestricted && (
<div className="content-gate" role="note">
你已达到免费浏览次数,请登录或订阅后继续阅读。
</div>
)}
</div>
);
}
这里按顶层 HTML 元素计算“部分内容”。如果接口返回多个 <p>、<figure> 或 <div>,达到限制后只保留前三个元素。
如果所有内容都包在一个 <div> 中,顶层可能只有一个节点。这时需要根据接口返回的实际结构,定位内部容器:
function createPreviewHtml(html, nodeCount = 3) {
const document = new DOMParser().parseFromString(html, "text/html");
const source =
document.querySelector(".article-content") ??
document.body.firstElementChild ??
document.body;
const preview = document.createElement("div");
Array.from(source.children)
.slice(0, nodeCount)
.forEach((element) => {
preview.appendChild(element.cloneNode(true));
});
return preview.innerHTML;
}
其中的 .article-content 要替换成 API 内容中实际存在且结构稳定的选择器。
只需要视觉遮挡时
如果不需要精确保留几个段落,也可以完整渲染内容,再通过固定高度和渐变遮罩形成预览效果:
export default function PostContent({ content, isRestricted }) {
return (
<div className={isRestricted ? "article-preview" : ""}>
<div dangerouslySetInnerHTML={{ __html: content }} />
{isRestricted && (
<div className="article-preview__gate">
请登录或订阅后继续阅读
</div>
)}
</div>
);
}
.article-preview {
position: relative;
max-height: 480px;
overflow: hidden;
}
.article-preview::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 180px;
content: "";
pointer-events: none;
background: linear-gradient(
to bottom,
rgb(255 255 255 / 0%),
rgb(255 255 255 / 100%)
);
}
.article-preview__gate {
position: absolute;
z-index: 1;
right: 0;
bottom: 24px;
left: 0;
text-align: center;
}
这种方式比较简单,但完整 HTML 仍在页面 DOM 中,用户可以通过开发者工具查看。
必须处理 HTML 安全问题
dangerouslySetInnerHTML 不会自动过滤恶意内容。如果 external API 返回的 HTML 不完全可信,直接渲染可能引入 XSS,包括事件属性、危险链接或其他可执行内容。
通常应该先用仍在维护的 HTML 清理库处理内容,例如 DOMPurify:
import DOMPurify from "dompurify";
const sanitizedContent = DOMPurify.sanitize(postDetails?.content ?? "");
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
处理顺序是先清理,再截取:
const sanitizedContent = DOMPurify.sanitize(content);
const visibleContent = isRestricted
? createPreviewHtml(sanitizedContent, 3)
: sanitizedContent;
如果项目使用服务端渲染,还要确认清理方案支持服务端环境,不能默认 window 和 DOMParser 一定存在。
localStorage 不能作为可靠的访问控制
localStorage 适合临时方案或风险较低的展示逻辑,但用户可以:
- 清除或修改访问次数;
- 使用无痕窗口或其他浏览器;
- 禁用浏览器存储;
- 直接查看已经下载的完整 HTML;
- 绕过前端代码调用内容接口。
如果限制涉及付费内容、会员权限或敏感信息,服务端应根据登录用户、会话和访问记录决定返回哪些内容:
{
"content": "<p>允许展示的预览内容</p>",
"restricted": true,
"remainingVisits": 0
}
React 只负责展示服务端返回的状态:
function PostContent({ postDetails }) {
return (
<div>
<div
dangerouslySetInnerHTML={{
__html: postDetails.content
}}
/>
{postDetails.restricted && (
<div className="content-gate">
请登录或订阅后继续阅读
</div>
)}
</div>
);
}
可靠的做法是由服务端统计访问次数。达到限制后,服务端不再向客户端发送完整内容,React 再根据 restricted 等字段显示预览和解锁入口。前端截取更适合原型、界面预览或不涉及安全性的软限制。
备注:内容仅供参考。
