You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Preact应用中通过Ajax下载文件触发浏览器安全错误

Preact + Vite Ajax 文件下载 SecurityError 问题分析与解决

问题原因

你遇到的安全错误,根源是npm init preact创建的项目默认集成了Preact Router(依赖preact-iso),它会拦截页面中所有<a>标签的点击事件,尝试通过history.pushState实现前端路由跳转。而你创建的下载链接使用blob:协议的URL,Router的拦截逻辑会把它当作要跳转的路由路径,触发pushState的同源安全校验,最终抛出错误。

旧项目没有路由拦截,浏览器能直接处理<a>标签的download属性,所以正常工作。

错误点说明

你的代码本身逻辑没问题,但忽略了Preact Router对原生<a>标签的事件劫持,导致原生下载行为被路由逻辑干扰。

正确实现方式

方法1:阻止事件冒泡,跳过Router拦截

在动态创建的下载链接上添加点击事件,阻止事件冒泡到Router的监听逻辑,让浏览器执行原生下载:

let response = await fetch("http://localhost:8080/api/entries/template");
if (response.ok) {
  let content = await response.text();
  let type = "text/plain";
  
  let downloadLink = document.createElement('a');
  downloadLink.href = window.URL.createObjectURL(new Blob([content], { type }));
  downloadLink.download = 'filename.txt';
  
  // 阻止事件冒泡,避免Preact Router劫持点击事件
  downloadLink.addEventListener('click', (e) => {
    e.stopPropagation();
  });
  
  document.body.appendChild(downloadLink);
  downloadLink.click();
  
  // 清理Blob URL和DOM元素,避免内存泄漏
  setTimeout(() => {
    window.URL.revokeObjectURL(downloadLink.href);
    document.body.removeChild(downloadLink);
  }, 100);
}

方法2:直接使用window.open下载(无需创建a标签)

绕开<a>标签,直接通过window.open打开Blob URL,触发下载:

let response = await fetch("http://localhost:8080/api/entries/template");
if (response.ok) {
  let content = await response.text();
  let type = "text/plain";
  const blobUrl = window.URL.createObjectURL(new Blob([content], { type }));
  
  // 注意:必须在用户交互上下文内执行(比如按钮点击回调中),否则会被浏览器弹窗拦截
  const downloadWindow = window.open(blobUrl);
  
  // 清理Blob URL
  setTimeout(() => {
    window.URL.revokeObjectURL(blobUrl);
    // 可选:自动关闭下载窗口
    if (downloadWindow) downloadWindow.close();
  }, 100);
}

方法3:使用Preact组件渲染隐藏下载链接

如果是在Preact组件中实现,可以直接渲染一个隐藏的<a>标签,并阻止路由拦截:

import { useRef } from 'preact/hooks';

function DownloadButton() {
  const linkRef = useRef(null);

  const handleDownload = async () => {
    let response = await fetch("http://localhost:8080/api/entries/template");
    if (response.ok) {
      let content = await response.text();
      let type = "text/plain";
      linkRef.current.href = window.URL.createObjectURL(new Blob([content], { type }));
      linkRef.current.click();
      setTimeout(() => {
        window.URL.revokeObjectURL(linkRef.current.href);
      }, 100);
    }
  };

  return (
    <>
      <button onClick={handleDownload}>下载模板</button>
      <a 
        ref={linkRef} 
        download="filename.txt" 
        style={{display: 'none'}}
        onClick={(e) => e.stopPropagation()}
      />
    </>
  );
}

兼容性说明

以上方法完全兼容Preact Router,核心思路都是避免路由逻辑干扰原生下载行为,同时保持Ajax请求自定义头的能力(满足你的API限制需求)。

内容的提问来源于stack exchange,提问作者derpirscher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 04:12:37