
案例项目开源地址: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 而不是 TouchableHighlight 或 Pressable?
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 作为跳转链接。
在 Banner 中使用
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 是空字符串、null 或 undefined,直接提示用户,不尝试打开。
为什么要检查?因为 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 为 true 或 false。
先检查再打开,可以给用户更准确的提示。比如用户点击一个电话链接,但设备没有电话功能,可以提示"此设备不支持拨打电话"而不是"无法打开链接"。
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