PAYATHON 2026

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 等字段显示预览和解锁入口。前端截取更适合原型、界面预览或不涉及安全性的软限制。

备注:内容仅供参考。