SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

移动端浏览器跳转外部应用时 XMLHttpRequest 状态码为 0 的问题分析与解决

问题背景

在移动端 H5 页面开发中,我们经常需要处理跳转到外部应用的场景(如跳转微信、支付宝等 APP)。在实现过程中发现了一个奇怪的现象:当用户点击跳转按钮后,无论是选择"允许跳转"还是"取消跳转",页面都会意外地跳转到错误页面,这显然不符合预期的用户体验。

问题现象

预期行为

  • 用户点击跳转按钮

  • 浏览器弹出"是否允许跳转到其他应用"的确认框

  • 用户选择"取消跳转"

  • 页面保持原状,不发生任何跳转 ✅

实际行为

  • 用户点击跳转按钮

  • 浏览器弹出"是否允许跳转到其他应用"的确认框

  • 用户选择"取消跳转"

  • 页面意外跳转到错误页面 ❌

问题调试

通过在代码中添加调试信息,发现了以下关键现象:

xhr.onerror = () => {
  alert(xhr.status); // 输出:0
};

调试结果显示:

  • xhr.onload 回调没有被触发

  • 直接进入了 xhr.onerror 回调

  • xhr.status 的值为 0

问题分析

XMLHttpRequest 状态码为 0 的含义

xhr.status = 0 通常表示以下几种情况:

  1. 网络请求被中断(最常见)

  2. 请求被主动取消

  3. 跨域问题

  4. 浏览器安全策略阻止

移动端浏览器的特殊行为

在移动端浏览器中,当页面尝试跳转到外部应用时:

  1. 浏览器安全机制:浏览器会弹出确认弹框询问用户是否允许跳转

  2. 页面状态冻结:弹框出现时,浏览器会暂停当前页面的 JavaScript 执行环境

  3. 网络请求中断:正在进行的网络请求可能被强制中断

  4. 状态码变为 0:被中断的请求会触发onerror回调,且xhr.status为 0

问题根本原因

我们的错误处理逻辑将所有xhr.onerror情况都视为"真正的网络错误",统一执行跳转到错误页面的操作:

xhr.onerror = () => {
  jumpWB(); // 跳转到错误页面
};

但实际上,应用跳转弹框导致的请求中断并不是真正的网络错误,而是浏览器的正常安全行为。

解决方案

核心思路

区分"真正的网络错误"和"应用跳转导致的请求中断":

xhr.onerror = () => {
  // 状态码为0通常表示请求被中断(如应用跳转弹框),不是真正的网络错误
  if (xhr.status === 0) {
    console.log("请求被中断,可能是应用跳转弹框导致,保持当前页面");
    return; // 不执行任何跳转,保持原页面
  }

  // 只有真正的网络错误才跳转到错误页面
  jumpWB();
};

完整的解决方案

function httpPost(url, params = { param: {}, body: {} }, headers = {}) {
  return new Promise((resolve, _reject) => {
    const { param } = params;

    const queryString = Object.keys(param)
      .map(
        (key) => `${encodeURIComponent(key)}=${encodeURIComponent(param[key])}`
      )
      .join("&");
    const fullUrl = queryString ? `${url}?${queryString}` : url;

    const xhr = new XMLHttpRequest();
    xhr.open("POST", fullUrl, true);
    xhr.setRequestHeader("Content-Type", "application/json");

    Object.keys(headers).forEach((key) => {
      xhr.setRequestHeader(key, headers[key]);
    });

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        try {
          const response = JSON.parse(xhr.responseText);
          resolve(response);
        } catch (e) {
          resolve(xhr.responseText);
        }
      } else {
        // HTTP错误状态码,跳转到错误页面
        jumpWB();
      }
    };

    xhr.onerror = () => {
      // 关键修改:区分请求中断和真正的网络错误
      if (xhr.status === 0) {
        console.log("请求被中断,可能是应用跳转弹框导致,保持当前页面");
        return;
      }

      // 其他真正的网络错误才跳转到错误页面
      jumpWB();
    };

    xhr.send();
  });
}

效果验证

修改后的行为:

  • 用户选择"取消跳转" → 请求被中断 → xhr.status = 0 → 保持当前页面 ✅

  • 真正的网络错误 → xhr.status ≠ 0 → 跳转错误页面 ✅

总结

这个问题提醒我们在移动端开发中需要注意:

  1. 异步请求并不意味着不受页面状态影响:虽然 XMLHttpRequest 是异步的,但在特定场景下(如应用跳转弹框)仍可能被浏览器中断

  2. xhr.status = 0 需要特殊处理:不能简单地将其视为普通的网络错误,需要根据具体场景判断是否需要执行错误处理逻辑

  3. 用户体验优先:技术实现要服务于用户体验,当用户明确选择"取消跳转"时,应该尊重用户的选择,保持页面原状

  4. 移动端浏览器的特殊性:移动端浏览器在处理外部应用跳转时有其特殊的安全机制,开发时需要充分考虑这些因素

这个案例也说明了在复杂的前端交互中,细致的问题分析和调试是多么重要。一个看似简单的网络请求错误,背后可能涉及浏览器安全策略、用户交互流程等多个层面的考量。

最后更新时间:
贡献者: 何风顺