外部链接跳转:从 App 打开浏览器的正确姿势

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony

做资讯类 App,有一个绑不开的需求:点击文章,跳转到原文页面。

WanAndroid 是一个技术资讯聚合平台,文章来自各个技术博客。我们的 App 展示文章列表,用户点击后需要跳转到原文链接。这就涉及到外部链接跳转的问题。

今天来聊聊 React Native 里怎么处理外部链接,以及一些容易踩的坑。

最简单的实现

tsx 复制代码
import {Linking} from 'react-native';

const openLink = (url: string) => {
  Linking.openURL(url);
};

就这么简单?是的,Linking.openURL 一行代码就能打开外部链接。

Linking 是 React Native 提供的模块,用于处理链接相关的操作。openURL 方法接收一个 URL 字符串,会调用系统的默认浏览器打开这个链接。

但这个简单的实现有问题。

错误处理

tsx 复制代码
const openLink = (url: string) => {
  Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
};

Linking.openURL 返回一个 Promise。如果链接无法打开,Promise 会 reject。

什么情况下会打开失败?

第一,URL 格式不对。比如缺少协议(www.example.com 而不是 https://www.example.com),或者有非法字符。

第二,没有可以处理这个 URL 的应用。比如 tel:123456 需要电话应用,如果设备没有电话功能就会失败。

第三,用户拒绝了权限。某些系统可能会询问用户是否允许打开外部链接。

不处理错误的话,用户点击链接没反应,也不知道为什么。加上 .catch() 给用户一个提示,体验更好。

Alert.alert 的用法

Alert.alert('错误', '无法打开链接') 弹出一个系统对话框。

第一个参数是标题,第二个参数是内容。还可以传第三个参数,是按钮数组:

tsx 复制代码
Alert.alert('错误', '无法打开链接', [
  {text: '取消', style: 'cancel'},
  {text: '重试', onPress: () => openLink(url)}
]);

我们只需要告知用户,不需要额外操作,所以用最简单的形式。

在文章卡片中使用

tsx 复制代码
export const ArticleCard = ({item}: Props) => {
  const openLink = (url: string) => {
    Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
  };

  return (
    <TouchableOpacity
      style={[styles.card, {backgroundColor: theme.card}]}
      onPress={() => openLink(item.link)}
      activeOpacity={0.7}
    >
      {/* 卡片内容 */}
    </TouchableOpacity>
  );
};

整个卡片都是可点击的,点击后打开文章链接。

TouchableOpacity 的选择

为什么用 TouchableOpacity 而不是 TouchableHighlightPressable

TouchableOpacity 点击时会降低透明度,给用户视觉反馈。这个效果比较subtle,适合卡片这种大面积的点击区域。

TouchableHighlight 点击时会显示一个底色,效果比较明显,适合按钮。

Pressable 是新的 API,功能更强大,但需要自己处理反馈效果。

对于文章卡片,TouchableOpacity 刚刚好。

activeOpacity 的设置

activeOpacity={0.7} 设置点击时的透明度。默认值是 0.2,会变得很透明。0.7 的效果更subtle,用户能感知到点击,但不会太突兀。

这个值可以根据设计调整。如果想要更明显的反馈,可以设小一点;想要更subtle,可以设大一点(最大是 1,就是没有变化)。

item.link 的来源

item 是文章数据,link 是文章的原文链接。这个数据来自 WanAndroid 的 API。

API 返回的文章数据大概是这样:

json 复制代码
{
  "id": 12345,
  "title": "React Native 入门教程",
  "link": "https://www.example.com/article/12345",
  "author": "张三",
  ...
}

我们直接用 item.link 作为跳转链接。

tsx 复制代码
export const BannerSection = ({banners}: Props) => {
  const openLink = (url: string) => {
    Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
  };

  return (
    <TouchableOpacity
      style={[styles.container, {backgroundColor: theme.card}]}
      onPress={() => openLink(banner.url)}
    >
      <Image source={{uri: banner.imagePath}} style={styles.image} />
      {/* 其他内容 */}
    </TouchableOpacity>
  );
};

Banner 的实现和文章卡片类似,点击整个 Banner 跳转到对应链接。

Banner 数据里的链接字段是 url,和文章的 link 不同。这是 API 设计的问题,我们只能适应。

openLink 函数的复用

你可能注意到,openLink 函数在两个组件里都定义了一遍,代码重复了。

可以把它抽成一个工具函数:

tsx 复制代码
// utils/link.ts
import {Linking, Alert} from 'react-native';

export const openLink = (url: string) => {
  Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
};

然后在组件里导入使用:

tsx 复制代码
import {openLink} from '../utils/link';

// 使用
onPress={() => openLink(item.link)}

这样代码更整洁,修改也只需要改一处。

我们项目里没有这样做,是因为代码量不大,重复两次还可以接受。如果有更多地方用到,就应该抽出来了。

URL 的校验

tsx 复制代码
const openLink = (url: string) => {
  if (!url) {
    Alert.alert('错误', '链接为空');
    return;
  }
  Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
};

加一个空值检查。如果 url 是空字符串、nullundefined,直接提示用户,不尝试打开。

为什么要检查?因为 API 返回的数据不一定可靠。某些文章可能没有链接,或者链接字段是空的。不检查的话,Linking.openURL('') 会报错。

更严格的校验

如果想更严格,可以检查 URL 格式:

tsx 复制代码
const isValidUrl = (url: string) => {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
};

const openLink = (url: string) => {
  if (!url || !isValidUrl(url)) {
    Alert.alert('错误', '链接无效');
    return;
  }
  Linking.openURL(url).catch(() => Alert.alert('错误', '无法打开链接'));
};

new URL(url) 会解析 URL,如果格式不对会抛出异常。用 try-catch 捕获异常,就能判断 URL 是否有效。

不过这个检查可能过于严格。有些 URL 虽然格式不标准,但浏览器能打开。所以我们只做了空值检查,把格式问题交给 Linking.openURL 处理。

Linking 的其他用法

Linking 不只能打开网页,还能打开其他类型的链接。

打开电话

tsx 复制代码
Linking.openURL('tel:10086');

会调起电话应用,拨打 10086。

打开短信

tsx 复制代码
Linking.openURL('sms:10086');

会调起短信应用,收件人是 10086。

打开邮件

tsx 复制代码
Linking.openURL('mailto:example@example.com');

会调起邮件应用,收件人是 example@example.com

还可以带上主题和正文:

tsx 复制代码
Linking.openURL('mailto:example@example.com?subject=Hello&body=Hi there');

打开地图

tsx 复制代码
// iOS
Linking.openURL('maps://app?daddr=北京市');

// Android
Linking.openURL('geo:39.9,116.4');

不同平台的地图 URL 格式不同,需要分别处理。

打开应用商店

tsx 复制代码
// iOS App Store
Linking.openURL('itms-apps://itunes.apple.com/app/id123456');

// Android Google Play
Linking.openURL('market://details?id=com.example.app');

这些 URL scheme 是各平台定义的,需要查阅文档。

canOpenURL 的使用

tsx 复制代码
const openLink = async (url: string) => {
  const canOpen = await Linking.canOpenURL(url);
  if (canOpen) {
    await Linking.openURL(url);
  } else {
    Alert.alert('错误', '无法打开链接');
  }
};

Linking.canOpenURL 检查是否能打开某个 URL。它返回一个 Promise,resolve 为 truefalse

先检查再打开,可以给用户更准确的提示。比如用户点击一个电话链接,但设备没有电话功能,可以提示"此设备不支持拨打电话"而不是"无法打开链接"。

iOS 的限制

在 iOS 上,canOpenURL 需要在 Info.plist 里声明要查询的 URL scheme。如果没声明,canOpenURL 会返回 false,即使实际上能打开。

xml 复制代码
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>tel</string>
  <string>mailto</string>
  <string>https</string>
</array>

这是 iOS 的隐私保护措施,防止 App 探测用户安装了哪些应用。

对于 https 链接,一般不需要声明,因为所有设备都能打开网页。我们的 App 主要打开网页链接,所以没有用 canOpenURL,直接 openURL 加错误处理就够了。

完整的链接处理代码

tsx 复制代码
import React from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Linking,
  Alert
} from 'react-native';
import {Article} from '../types';
import {useTheme} from '../context/ThemeContext';

interface Props {
  item: Article;
}

export const ArticleCard = ({item}: Props) => {
  const {theme} = useTheme();

  const openLink = (url: string) => {
    if (!url) {
      Alert.alert('错误', '链接为空');
      return;
    }
    Linking.openURL(url).catch(() => {
      Alert.alert('错误', '无法打开链接');
    });
  };

  return (
    <TouchableOpacity
      style={[styles.card, {backgroundColor: theme.card, borderColor: theme.border}]}
      onPress={() => openLink(item.link)}
      activeOpacity={0.7}
    >
      <View style={styles.content}>
        <Text style={[styles.title, {color: theme.text}]} numberOfLines={2}>
          {item.title.replace(/<[^>]+>/g, '')}
        </Text>
        <View style={styles.meta}>
          <Text style={[styles.author, {color: theme.accent}]}>
            {item.author || item.shareUser || '匿名'}
          </Text>
          <Text style={[styles.chapter, {color: theme.subText}]}>
            {item.superChapterName}/{item.chapterName}
          </Text>
        </View>
        <View style={styles.footer}>
          <Text style={[styles.date, {color: theme.subText}]}>
            {item.niceDate}
          </Text>
        </View>
      </View>
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  card: {
    borderRadius: 12,
    borderWidth: 1,
    marginBottom: 12,
    overflow: 'hidden'
  },
  content: {
    padding: 12
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    lineHeight: 22,
    marginBottom: 6
  },
  meta: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
    flexWrap: 'wrap',
    gap: 8
  },
  author: {
    fontSize: 12,
    fontWeight: '500'
  },
  chapter: {
    fontSize: 11
  },
  footer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  date: {
    fontSize: 11
  },
});

外部链接跳转看起来简单,但细节不少。错误处理、空值检查、用户反馈,每一个都影响体验。

好的 App 不是功能多,而是每个功能都做得细致。用户可能不会注意到这些细节,但如果没做好,他们一定会感觉到"这个 App 不太行"。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
David凉宸16 小时前
vue2与vue3的差异在哪里?
前端·javascript·vue.js
Irene199116 小时前
JavaScript字符串转数字方法总结
javascript·隐式转换
坚持就完事了16 小时前
Java的OOP
java·开发语言
像少年啦飞驰点、16 小时前
零基础入门 Spring Boot:从“Hello World”到可部署微服务的完整学习路径
java·spring boot·微服务·编程入门·后端开发
undsky_16 小时前
【RuoYi-SpringBoot3-Pro】:将 AI 编程融入传统 java 开发
java·人工智能·spring boot·ai·ai编程
不光头强16 小时前
shiro学习要点
java·学习·spring
工一木子16 小时前
Java 的前世今生:从 Oak 到现代企业级语言
java·开发语言
css趣多多16 小时前
this.$watch
前端·javascript·vue.js
H Journey16 小时前
Linux su 命令核心用法总结
java·linux·服务器·su
Code小翊16 小时前
JS语法速查手册,一遍过JS
javascript