H5唤起APP的方案总结:从踩坑到完美实现

前言

在移动端开发中,H5页面唤起APP是一个常见的需求场景。最近在开发跑团分享功能时,我遇到了H5页面唤起APP并传递参数到指定页面的需求。经过几天的摸索和调试,最终成功实现了这个功能。本文将详细总结H5唤起APP的各种方案、技术原理和实现细节。

一:H5唤起APP的方案和雷区总结:

1. URL Scheme方案

原理:通过自定义协议头打开APP

如:

ydapp://runGroupInfo?groupId=123&inviterId=456&joinFromInvite=1

优点:

实现简单,兼容性好

支持参数传递

跨平台支持

缺点:

在微信等屏蔽环境中无法直接使用(通俗的来说就是在微信聊天界面中点击别人分享的链接无法直接点开在微信内置的浏览器中唤醒APP)

需要用户确认是否打开APP(用户手机点击确认实现跳转)

2.Intent方案(Android系统专属方案)

原理: Android系统的Intent机制

如:

复制代码
 intent: `intent://runGroupInfo?` +

  `groupId=${params.groupId}&` +

`inviterId=${params.inviterId}&` +

      `joinFromInvite=${params.joinFromInvite}` +

      `#Intent;` +

      `scheme=ydapp;` +

      `package=com.edsd.uniedsd;` +

      `component=com.edsd.uniedsd/.pagesHome.runGroupInfo.runGroupInfo;` +

      `end`,

优点:

Android系统级支持

可以指定具体的Activity

参数传递稳定

缺点:

仅Android支持

语法复杂容易出错(常见!)

补充:

这个方案涉及雷区:

一开始我是这么写的:

注意:一开始写的这个我的Intent格式中使用了#Intent和/,这会截断后面的参数:

// ❌ 错误的写法 - 参数会被截断intent: `intent://runGroupInfo/#Intent;` + // 这里的 # 会截断后面的参数

这样直接导致了参数无法传递

也就造成了现象: 当点击唤醒App按钮跳转到App之后,App.vue文件解析不到h5传过来的参数,所以跳转过来之后只能停留在app的首页,而无法跳转到需要跳转到的指定界面。

优点:

用户体验好,无弹窗提示

在微信中可引导用户浏览器打开

缺点:

需要服务端配置

配置复杂,验证严格

在没有配置前,打开一个h5链接,就会唤起浏览器打开相应的网页,配置了App Link或者Universal Link后,如果已经安装了相应的App就能直接启动App中相应的页面,如果没有安装,才会打开浏览器跳转到H5链接。

原理: 通过Https链接唤起App Link方案

如:

https://www.dianjiasu.com/pagesHome/runGroupInfo/runGroupInfo?groupId=123&inviterId=456

Universal Links需要先在微信开放平台中为项目配置一个Universal Links,审核通过之后,在项目配置文件(manifest.json)中填写上这个Universal Links

此外,要使Universal Links生效需要在服务器的根目录和.well-known目录中增加apple-app-site-association配置文件,好了之后在网页中输入

https://{你的域名}/.well-known/apple-app-site-association和https://{你的域名}/apple-app-site-association(无后缀),看能否直接访问到上传的json文件,都能访问到即说明配置成功。果访问时浏览器是直接下载文件,看下请求地址的响应头 Content-Type,必须要是 application/json 才能直接显示。可在nginx中配置location块,配置成application/json格式,示例如下:

application/json文件配置结构如下:

复制代码
{

    "applinks": {

 

        "apps": [],

        "details": [ {

            "appID": "{TeamID}.{BundleID}",

            "paths": ["/app/*", "/applink/*"]

        }

 

        ]

    }

}

示例:

复制代码
{

    "applinks": {

        "apps": [],

        "details": [

            {

                "appID": "A4DZU75Q58.com.edsd.uniedsd",

                "paths": ["/app/*","/app/"]

            }

        ]

    }

}

其中,TeamID可去App Store Connect(苹果开发者平台)查看。此外,苹果手机app上的分享到微信好友功能和微信登录功能等同样需要配置这个Universal Links。

安卓的App Link配置方法类似ios的Universal Link,可以从外部跳转到app页面。

Android App Link是一种特殊的深层链接(Deep Link),允许用户通过 HTTP 或 HTTPS 链接直接跳转到应用内的特定页面,而无需用户确认。

App Link和普通的Deep Link的区别:

Deep Link配置后并不能直接打开App,比如打开一个配置好Deep Link的H5页面后,还需要用户点击同意打开app,才能启动App。

而App Link配置后,无需用户点击同意,就能直接打开App中相应的页面。

Android App Link配置流程

开发配置流程可以自己手写代码,也可以用Android Studio Tools下面的App Links Assistant,这里用App Links Assistant来演示:

1、配置App Link

打开App Links Assistant后,打开Create Applink

在新页面中会有4个配置的步骤,如下所示:

选中第一步Open URL Mapping Editor,配置App Link的schema、域名、后缀等:

第一个是域名地址(注意不要加www),第二个是域名后跟的链接字段,第三个是配置哪个Activity作为这个App Link的启动对象。

这里注意Path配置如果希望扩展性强,就选择pathPrefix,匹配前缀即可,如果选path,那么只有这个路径完全相同才能匹配上,path后面再添加字符就不能匹配了。

配置好后如下所示,可以看到这里autoVerify配置为true,就是代表App Link会自动验证

  1. 创建assetlinks.json文件

再点击上面的Open Digital Asset Links File Generator,打开页面如下:

1配置上一步中的域名地址,注意不要加path

2配置app的包名

3配置是否和网站共享用户登录信息,这里没有特殊要求就不勾选

4选择签名文件,这里注意如果debug包和release包的签名不一样,这里要验证哪个包,就要用哪个签名去生成对应的assetlinks.json文件。

生成好后点击右侧的保存文件,将assetlinks.json文件保存到本地。

上面是使用Android Studio Tools的配置方法(方案一),下面是我自己的手写的配置(方案二)。

示例:

复制代码
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.edsd.uniedsd",
  "sha256_cert_fingerprints": [
  "e8a504a3560f66dade8604ad20c259dfdb7c6c38df4e1565d5fa6cc3e3a90bdb"
]
  }
}]

将该文件放到域名服务器.well-known目录下,,如果没有,创建一个即可。(注意, .well-known文件为隐藏文件,ls命令查看不到,需要使用ls -a命令查看隐藏文件)

在阿里云服务器上地址即:/var/www/html/dist/.well-known

然后浏览器打开配置的域名/.well-known/assetlinks.json地址能打开就说明配置好了。

如图:

Uniapp项目也可以直接在manifest.json文件中配置intent-filter。配置好之后进行云打包,打包之后可将生成的apk安装包修改成.zip类型,解压后查看里面的AndroidManifest.xml文件,如果将上面增加的配置成功打包了进去即可生效。

补充: AndroidManifest.xml文件为二进制文件,要查看需要先将其转换成txt。可使用工具AXMLPrinter2进行转换。

转换方法:

将AXMLPrinter2.jar文件下载下来之后,将要转换的AndroidManifest.xml文件和AXMLPrinter2.jar文件放到同一个文件夹,在该文件路径打开cmd命令窗输入 java -jar AXMLPrinter2.jar AndroidManifest.xml >> AndroidManifest.txt 即可输出txt文件到目录之中。

验证方法:

点击上面的第4步,Test App Links,打开页面如下:

输入上面配置好的schema://host/pathprefix,点击Run Test如果显示正常,就会看到app被打开了。

其他验证方式

在手机上,备忘录中输入上面的链接:schema://host/pathprefix,替换成你配置的地址,点击备忘录中的这个地址,也能看到app被打开了,而不是打开的浏览器中对应网页。

发送短信同样可以验证。

如果域名服务器上没有配置assetlinks.json,那么会看到手机上打开的还是浏览器中对应的H5链接,并不会启动App。

二:核心实现代码

1.H5端唤醒逻辑

复制代码
const tryWakeUpAPP = () => {
  if (!groupId.value) {
    uni.showToast({
      title: "缺少必要参数",
      icon: "none"
    });
    return;
  }

  const params = {
    groupId: groupId.value,
    inviterId: inviterId.value || '',
    joinFromInvite: '1'
  };

  console.log('🔗 唤醒APP参数:', params);

  // 多层级唤醒方案`
  const wakeUpStrategies = {
    // 1. Intent格式 (Android优先)
    intent: `intent://runGroupInfo?` +
      `groupId=${params.groupId}&` +
      `inviterId=${params.inviterId}&` +
      `joinFromInvite=${params.joinFromInvite}` +
      `#Intent;` +
      `scheme=ydapp;` +
      `package=com.edsd.uniedsd;` +
      `component=com.edsd.uniedsd/.pagesHome.runGroupInfo.runGroupInfo;` +
      `end`,

    // 2. 标准URL Scheme
    scheme: `ydapp://runGroupInfo?groupId=${params.groupId}&inviterId=${params.inviterId}&joinFromInvite=${params.joinFromInvite}`,

    // 3. 简化的Intent格式(备选)
    simpleIntent: `intent://runGroupInfo?groupId=${params.groupId}&inviterId=${params.inviterId}&joinFromInvite=${params.joinFromInvite}#Intent;scheme=ydapp;package=com.edsd.uniedsd;end`,

    // 3. App Links (虽然验证失败,但某些设备可能工作)
    appLink: `https://www.dianjiasu.com/pagesHome/runGroupInfo/runGroupInfo?groupId=${params.groupId}&inviterId=${params.inviterId}&joinFromInvite=${params.joinFromInvite}`,

    // 4. 备用下载页面(APP下载页)
    fallback: 'https://www.dianjiasu.com/app/appcode.html'
  };

  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
  const isAndroid = /Android/.test(navigator.userAgent);
  const isWechat = /MicroMessenger/.test(navigator.userAgent);

  // 微信环境特殊处理
  if (isWechat) {
    uni.showModal({
      title: '提示',
      content: '请在浏览器中打开此链接',
      showCancel: false,
      confirmText: '知道了'
    });
    return;
  }


  if (isIOS) {
    // iOS处理
    wakeUpIOS(wakeUpStrategies);
  } else if (isAndroid) {
    // Android处理
    wakeUpAndroid(wakeUpStrategies);
  } else {
    uni.showToast({
      title: "请在移动设备上使用此功能",
      icon: "none"
    });
  }
};

2. iOS 唤醒逻辑

复制代码
const wakeUpIOS = (strategies: any) => {
  let attemptCount = 0;

  const tryNextStrategy = () => {
    attemptCount++;

    switch(attemptCount) {
      case 1:
        // 第一尝试: URL Scheme
        console.log('iOS尝试URL Scheme');
        window.location.href = strategies.scheme;
        break;
      case 2:
        // 第二尝试: Universal Link
        console.log('iOS尝试Universal Link');
        window.location.href = strategies.appLink;
        break;
      case 3:
        // 最终fallback
        console.log('跳转到下载页');
        window.location.href = strategies.fallback;
        return;
    }

    setTimeout(tryNextStrategy, 1500);
  };

  // 监听页面隐藏(跳转成功)
  const visibilityChange = () => {
    if (document.hidden) {
      console.log('iOS跳转成功');
    }
  };

  document.addEventListener('visibilitychange', visibilityChange);

  // 开始尝试
  tryNextStrategy();

  // 清理监听器
  setTimeout(() => {
    document.removeEventListener('visibilitychange', visibilityChange);
  }, 5000);
};

3. Android 分层唤醒逻辑

复制代码
const wakeUpAndroid = (strategies: any) => {
  console.log('Android唤醒开始,参数:', strategies);
  let attemptCount = 0;
  let pageHidden = false;

  // 页面可见性监听
  const visibilityChange = () => {
    if (document.hidden) {
      pageHidden = true;
      console.log('Android页面隐藏,可能跳转成功');
    }
  };

  document.addEventListener('visibilitychange', visibilityChange);

  const tryNextStrategy = () => {
    if (pageHidden) return; // 如果已经跳转成功,停止后续尝试

    attemptCount++;

    switch(attemptCount) {
      case 1:
        // 第一优先: Intent格式
        console.log('Android尝试Intent格式');
        window.location.href = strategies.intent;
        break;
      case 2:
        // 第二尝试: URL Scheme
        console.log('Android尝试URL Scheme');
        window.location.href = strategies.scheme;
        break;
      case 3:
        // 第三尝试: App Links
        console.log('Android尝试App Links');
        window.location.href = strategies.appLink;
        break;
      case 4:
        // 最终fallback
        console.log('Android跳转到下载页');
        window.location.href = strategies.fallback;

        // 显示引导提示
        setTimeout(() => {
          uni.showModal({
            title: '提示',
            content: '未检测到App,已跳转下载页面。您也可以尝试在系统浏览器中打开此链接。',
            showCancel: false
          });
        }, 1000);
        return;
    }

    // 增加间隔时间,给用户更多操作时间
    setTimeout(tryNextStrategy, 2000);
  };

  // 开始尝试
  tryNextStrategy();

  // 延长监听时间
  setTimeout(() => {
    document.removeEventListener('visibilitychange', visibilityChange);
  }, 8000);
};

三:App端的捕获与处理

此处为雷区!!!!

唤醒app之后始参数获取不到h5页面传过来的参数,试了半天了就是拿不到

这直接导致了唤醒app之后,只能停留在app首页,无法跳转到指定的界面

参数明明已经传过去了,但是app端就是接收不到参数。

经过反复排查最终发现了原因:

当通过ydapp://runGroupInfo?groupId=xxx这样的URL Scheme唤醒APP时:

APP确实被唤醒了

但是参数不会自动出现在onLaunch的options.query中

需要通过特定API来获取

获取到之后再对参数进行解析才行!!!!

解决方案:

使用uni-app的plus.runtime.arguments

  1. 修改App.vue,添加URL Scheme参数解析:
复制代码
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { memberWxLoginAPI, memberAppWxLoginAPI } from '@/services/basic'
import { useMemberStore } from '@/stores'
const memberStore = useMemberStore()
// 标记是否已经处理过URL Scheme跳转
let hasHandledUrlScheme = false;
onLaunch((options) => {
  console.log('App 启动', options);
  // 添加URL Scheme参数捕获
  captureUrlSchemeParams();
  // 添加更详细的启动参数日志
  if (options.query) {
    console.log('启动-参数', options.query);
  }
  // 处理App启动时的 deeplink 参数
  // #ifdef APP-PLUS
  handleDeepLink()
  // #endif
})
onShow((options) => {
  console.log('App 展示', options);
  // 每次显示时都检查URL Scheme参数(处理从后台唤醒的情况)
  if (!hasHandledUrlScheme) {
    captureUrlSchemeParams();
  }
})

onHide(() => {
  // APP进入后台时重置标记
  hasHandledUrlScheme = false;
})

// 专门捕获URL Scheme参数
function captureUrlSchemeParams() {
  // #ifdef APP-PLUS
  try {
    // 获取URL Scheme的参数
    const args = plus.runtime.arguments;
    console.log('🔗 URL Scheme参数:', args);

    if (args) {
      // 解析参数
      const params = parseUrlSchemeArgs(args);
      console.log('📋 解析后的参数:', params);

      if (params && params.groupId) {
        console.log('✅ 从URL Scheme找到groupId:', params.groupId);
        handleUrlSchemeParams(params);
      }
    }
  } catch (error) {
    console.error('解析URL Scheme参数失败:', error);
  }
  // #endif
}


// 解析URL Scheme参数
function parseUrlSchemeArgs(args: string) {
  if (!args) return null;

  console.log('原始参数字符串:', args);

  // 尝试不同的解析方式

  // 方式1: 直接包含参数 (ydapp://runGroupInfo?groupId=123)
  if (args.includes('?')) {
    const urlPart = args.includes('://') ? args : `ydapp://${args}`;
    try {
      const url = new URL(urlPart);
      const params: any = {};
      url.searchParams.forEach((value, key) => {
        params[key] = value;
      });
      console.log('方式1解析结果:', params);
      return params;
    } catch (e) {
      console.log('方式1解析失败:', e);
    }
  }
  // 方式2: 参数作为查询字符串 (groupId=123&inviterId=456)
  if (args.includes('?')) {
    const params: any = {};
    // 提取查询字符串部分
    const queryString = args.split('?')[1];
    // 如果查询字符串包含#Intent(Android Intent情况),先处理
    const cleanQueryString = queryString.split('#Intent')[0];
    // 解析参数
    const pairs = cleanQueryString.split('&');
    pairs.forEach(pair => {
      const [key, value] = pair.split('=');
      if (key && value !== undefined) {
        // 使用decodeURIComponent解码
        params[decodeURIComponent(key)] = decodeURIComponent(value);
      }
    });

    console.log('✅ 手动解析结果:', params);
    return params;
  }

  // 方式3: Intent格式参数
  if (args.includes('Intent')) {
    const params: any = {};
    // 提取S.开头的参数
    const paramMatches = args.match(/S\.(\w+)=([^;]+)/g);
    if (paramMatches) {
      paramMatches.forEach(match => {
        const [_, key, value] = match.match(/S\.(\w+)=([^;]+)/) || [];
        if (key && value) {
          params[key] = value;
        }
      });
    }
    console.log('方式3解析结果:', params);
    return params;
  }

  return null;
}



// 处理URL Scheme参数并跳转
function handleUrlSchemeParams(params: any) {
  console.log('处理URL Scheme参数:', params);

  //构造目标页面 URL
  const url = `/pagesHome/runGroupInfo/runGroupInfo?groupId=${params.groupId}&inviterId=${params.inviterId || ''}&joinFromInvite=${params.joinFromInvite || '1'}`;

  console.log('构造跳转URL:', url);

  // 等待APP初始化完成后再跳转
  setTimeout(() => {
    jumpToTargetPage(url);
  }, 1000);
}

// 执行页面跳转
function jumpToTargetPage(url: string) {
  console.log('🚀 开始跳转到目标页面:', url);

  // 获取当前页面栈
  const pages = getCurrentPages();
  const currentPage = pages[pages.length - 1];
  const currentRoute = currentPage ? currentPage.route : '';

  console.log('当前页面路由:', currentRoute);
  console.log('目标页面路由:', 'pagesHome/runGroupInfo/runGroupInfo');

  // 如果已经在目标页面,则不需要跳转
  if (currentRoute === 'pagesHome/runGroupInfo/runGroupInfo') {
    console.log('⚠️ 已经在目标页面,通过事件传递参数');

    // 通过全局事件传递参数给当前页面
    uni.$emit('URL_SCHEME_PARAMS', {
      groupId: url.match(/groupId=([^&]+)/)?.[1],
      inviterId: url.match(/inviterId=([^&]+)/)?.[1],
      joinFromInvite: url.match(/joinFromInvite=([^&]+)/)?.[1] || '1'
    });
    return;
  }

  // 跳转到目标页面
  uni.reLaunch({
    url: url,
    success: () => {
      console.log('🎉 URL Scheme跳转成功');
    },
    fail: (err) => {
      console.error('💥 reLaunch跳转失败:', err);

      // 备用方案:使用navigateTo
      uni.navigateTo({
        url: url,
        success: () => {
          console.log('✅ navigateTo跳转成功');
        },
        fail: (err2) => {
          console.error('❌ 所有跳转方式都失败:', err2);
          uni.showToast({
            title: '页面跳转失败',
            icon: 'none'
          });
        }
      });
    }
  });
}


// 处理 deeplink
function handleDeepLink() {
  // 获取启动参数
  const launchOptions = uni.getLaunchOptionsSync && uni.getLaunchOptionsSync()
  console.log('启动-Options:', launchOptions);
  console.log("主要的query的值", launchOptions.query);
  if (launchOptions && launchOptions.query && Object.keys(launchOptions.query).length > 0) {
    console.log('Launch Options 参数:', launchOptions.query);
    handleQueryParams(launchOptions.query)

  } else {
    console.log('没有找到启动参数.')
  }
}


function handleQueryParams(query: any) {
  console.log('处理普通启动参数:', query);
  // 原有的参数处理逻辑
  if (query && query.groupId) {
    const url = `/pagesHome/runGroupInfo/runGroupInfo?groupId=${query.groupId}&inviterId=${query.inviterId || ''}&joinFromInvite=${query.joinFromInvite || '1'}`;
    setTimeout(() => {
      uni.reLaunch({ url: url });
    }, 1000);
  }
}
</script>
  1. 在跑团页面接受参数事件
复制代码
在/pagesHome/runGroupInfo/runGroupInfo.vue中添加:
import { onLoad, onUnload } from '@dcloudio/uni-app'

// 在setup中添加
onLoad((options) => {
  console.log('页面加载参数:', options);
  // 正常的页面加载参数处理
})
// 监听URL Scheme参数事件(用于已经在跑团页面时更新参数)
uni.$on('URL_SCHEME_PARAMS', (params) => {
  console.log('收到URL Scheme参数事件:', params); 
  // 更新页面数据
  if (params.groupId) {
    groupId.value = Number(params.groupId);
    inviterId.value = params.inviterId ? Number(params.inviterId) : undefined;
    // 重新加载数据
    getRunningGroupData();
  }
});
onUnload(() => {
  // 清理事件监听
  uni.$off('URL_SCHEME_PARAMS');
});

流程总结

H5点击唤醒 → 调用URL Scheme ydapp://runGroupInfo?groupId=123...

APP被唤醒 → onLaunch/onShow触发

捕获参数 → 通过plus.runtime.arguments获取参数字符串

解析参数 → 将字符串解析为对象{groupId: 123, ...}

构造URL → 生成目标页面路径/pagesHome/runGroupInfo/runGroupInfo?groupId=123...

执行跳转 → 使用uni.reLaunch跳转到跑团详情页

页面加载 → 跑团页面通过onLoad接收参数并加载数据

关键技术难点与解决方案

  1. 参数传递丢失问题

问题: URL Scheme 参数在传递过程中丢失

解决方案:

确保参数正确编码

使用稳定的参数格式

添加参数完整性校验

  1. 多平台兼容性问题

问题: ios和Android对URL Scheme的支持差异

解决方案:

分平台使用不同策略

Android优先使用Intent

Ios使用标准URL Scheme

  1. 微信环境限制

问题:

微信中无法直接唤起APP

解决方案:

检测微信环境并提示用户在浏览器打开

提供明确的用户指引

  1. 参数解析错误

问题: APP端解析参数格式错误

解决方案:

实现多格式参数解析

添加错误处理和日志输出

支持Intent和标准URL Scheme两种格式

以上是我对这个功能的总结。H5唤起APP时一个涉及多平台、多技术的复杂功能。需要综合考虑URL Scheme、Intent 、Universal Link等多种方案,并针对不同平台和环境进行适配。

希望这篇总结能够帮助其他开发者少走弯路,顺利实现H5与APP的无缝衔接。