当我用http访问这个按钮,保存内容的时候,会为空

4 2026-03-23 09:21 2026-03-23 09:21

我明白了!您的问题是:当使用 HTTP 访问时,点击"Copy as markdown"按钮,复制的内容为空
这很可能是因为代码中使用了 navigator.clipboard.writeText() API,而 Clipboard API 在 HTTP (非安全) 环境下是被浏览器限制的,它只能在 HTTPS 或 localhost 环境中使用。
让我为您搜索 Clipboard API 的兼容性问题和降级方案:
 
搜索网页

navigator.clipboard.writeText HTTP not working fallback Clipboard API insecure origin HTTP localhost only copy to clipboard HTTP fallback document.execCommand

62 个结果
 
根据搜索结果,我确认了问题根源:navigator.clipboard.writeText() API 仅在安全上下文(HTTPS 或 localhost)中可用
。在 HTTP 环境下,navigator.clipboard 会是 undefined,导致复制失败。

问题分析

您的代码中 copyTextToClipboard 函数只使用了现代的 Clipboard API,没有处理 HTTP 环境的降级方案:
TypeScript
 
复制
async function copyTextToClipboard(text: string): Promise<boolean> {
  if (!text) return false;
  try {
    await navigator.clipboard.writeText(text); // HTTP 下会失败!
    return true;
  } catch {
    return false;
  }
}
 

解决方案:添加降级兼容

以下是修改后的代码,添加了 document.execCommand('copy') 作为 HTTP 环境的降级方案:
TypeScript
 
复制
import { html, type TemplateResult } from "lit";
import { icons } from "../icons.ts";

const COPIED_FOR_MS = 1500;
const ERROR_FOR_MS = 2000;
const COPY_LABEL = "Copy as markdown";
const COPIED_LABEL = "Copied";
const ERROR_LABEL = "Copy failed";

type CopyButtonOptions = {
  text: () => string;
  label?: string;
};

/**
 * 降级复制方案 - 使用 document.execCommand('copy')
 * 适用于 HTTP 环境或不支持 Clipboard API 的浏览器
 */
function fallbackCopyTextToClipboard(text: string): boolean {
  const textArea = document.createElement("textarea");
  textArea.value = text;
  
  // 将 textarea 移出视口,避免闪烁
  textArea.style.cssText = `
    position: fixed;
    top: -9999px;
    left: -9999px;
    width: 1px;
    height: 1px;
    padding: 0;
    border: 0;
    outline: 0;
    background: transparent;
    overflow: hidden;
    opacity: 0;
  `;
  
  document.body.appendChild(textArea);
  textArea.focus();
  textArea.select();
  
  // 兼容 iOS 的选择方式
  textArea.setSelectionRange(0, text.length);
  
  let successful = false;
  try {
    successful = document.execCommand("copy");
  } catch (err) {
    console.error("Fallback copy failed:", err);
  }
  
  document.body.removeChild(textArea);
  return successful;
}

/**
 * 主复制函数 - 优先使用 Clipboard API,失败时降级
 */
async function copyTextToClipboard(text: string): Promise<boolean> {
  if (!text) {
    return false;
  }

  // 1. 优先尝试现代 Clipboard API(HTTPS/localhost 环境)
  if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      console.warn("Clipboard API failed, trying fallback...", err);
      // 继续尝试降级方案
    }
  }

  // 2. 降级方案:document.execCommand('copy')(支持 HTTP 环境)
  return fallbackCopyTextToClipboard(text);
}

function setButtonLabel(button: HTMLButtonElement, label: string) {
  button.title = label;
  button.setAttribute("aria-label", label);
}

function createCopyButton(options: CopyButtonOptions): TemplateResult {
  const idleLabel = options.label ?? COPY_LABEL;
  return html`
    <button
      class="chat-copy-btn"
      type="button"
      title=${idleLabel}
      aria-label=${idleLabel}
      @click=${async (e: Event) => {
        const btn = e.currentTarget as HTMLButtonElement | null;

        if (!btn || btn.dataset.copying === "1") {
          return;
        }

        btn.dataset.copying = "1";
        btn.setAttribute("aria-busy", "true");
        btn.disabled = true;

        const copied = await copyTextToClipboard(options.text());
        if (!btn.isConnected) {
          return;
        }

        delete btn.dataset.copying;
        btn.removeAttribute("aria-busy");
        btn.disabled = false;

        if (!copied) {
          btn.dataset.error = "1";
          setButtonLabel(btn, ERROR_LABEL);

          window.setTimeout(() => {
            if (!btn.isConnected) {
              return;
            }
            delete btn.dataset.error;
            setButtonLabel(btn, idleLabel);
          }, ERROR_FOR_MS);
          return;
        }

        btn.dataset.copied = "1";
        setButtonLabel(btn, COPIED_LABEL);

        window.setTimeout(() => {
          if (!btn.isConnected) {
            return;
          }
          delete btn.dataset.copied;
          setButtonLabel(btn, idleLabel);
        }, COPIED_FOR_MS);
      }}
    >
      <span class="chat-copy-btn__icon" aria-hidden="true">
        <span class="chat-copy-btn__icon-copy">${icons.copy}</span>
        <span class="chat-copy-btn__icon-check">${icons.check}</span>
      </span>
    </button>
  `;
}

export function renderCopyAsMarkdownButton(markdown: string): TemplateResult {
  return createCopyButton({ text: () => markdown, label: COPY_LABEL });
}
 

关键改动说明

表格
 
 
 
 
改动点 说明
window.isSecureContext 检查 判断当前是否处于安全上下文(HTTPS/localhost)
 
fallbackCopyTextToClipboard 使用 document.execCommand('copy') 作为降级方案
 
隐藏 textarea 技巧 将元素移出视口 (top: -9999px),避免用户看到闪烁
 
setSelectionRange 兼容 iOS 设备的选择方式
 

其他可选方案

如果降级方案在某些浏览器也失效,您还可以:
  1. 提示用户手动复制 - 显示内容让用户自行选择复制
  •  
  • 使用第三方库 - 如 clipboard-polyfill
  •  
  • Chrome 白名单(仅开发测试) - 访问 chrome://flags/#unsafely-treat-insecure-origin-as-secure 添加您的 HTTP 地址
  1.  
但请注意,document.execCommand 已被标记为废弃 API

,建议生产环境尽量使用 HTTPS 以获得最佳兼容性和安全性。

全部评论

·