Web 开发者构建首个 React Native 应用所需了解的一切。
对于有 React 经验并希望创建首个移动应用的 Web 开发者来说,React Native 是一个极好的选择。
虽然 React 和 React Native 在许多方面都有相似之处,但在面向 Web 和原生平台时存在显著差异。本博客旨在突出并解决开发者在从 React 过渡到 React Native 时通常遇到的最常见错误和问题。
我希望这能阐明一些进入一个全新平台时的“未知未知”,并使您从 Web 到原生的旅程更加轻松!
为什么选择 React Native
在我们讨论一些等待 React 开发者的潜在陷阱之前,让我们来看看他们应该选择 React Native 的几个原因:
可转移技能
使用 React Native,几乎所有应用代码都将用 JavaScript(或更常见的 TypeScript)编写。这意味着作为经验丰富的 React 开发者,您可以继续使用许多在 Web 上已经熟悉的编码模式和库。
真正的原生
尽管您大部分应用代码是用 JavaScript 编写的,但 React Native 应用在底层是真正的原生应用。您编写的 React 代码将映射到每个平台的实际原生基元。这对于性能以及应用的原生外观和感觉至关重要。
多平台代码共享
React Native 允许您从同一代码库构建原生 iOS 和原生 Android 应用。有了 Expo Router 和 API Routers,您还将能够针对 Web 和服务器进行开发。这意味着可以在平台之间共享大量代码。
为什么选择 Expo
Expo 是一个 React Native 框架 - 类似于 Next.js 是 React 的框架
React Native 提供的开箱即用功能是能够在原生 iOS 和 Android 平台上运行 React 代码。React Native 还附带了最基本的组件,如 Text、View 和 TextInput。但几乎所有生产应用都需要的大量功能实际上并没有包含在 React Native 中:导航、发送推送通知、使用手机相机、跨应用启动持久化数据,甚至只是为商店发布构建应用。这是有意为之,因为如果开箱即用地暴露每一个原生功能,将使 React Native 对 Meta 的小型 React 团队来说难以维护。
由于核心功能没有包括在内,React Native 开发者一直不得不依赖各种社区库,而现在 - 将近 10 年后 - 有一个大量的社区库可供选择。这在新项目开始时尤其令人不知所措,因为您还不了解足够多的信息来做出明智的决策,不知道使用哪些库以及哪些库能够很好地协同工作。
理想情况下,我们希望有一套常用且维护良好的工具集合,这样您就不必挑选和选择,这正是 Expo 所帮助实现的。
Expo 是一个 React Native 框架:一套工具和服务的集合,它们填补了 React Native 附带的功能和构建生产就绪应用所需的其他一切之间的差距。Expo SDK 是 React Native 的扩展标准库,提供了对核心中未包含的常用原生 API 的访问(如相机、视频、通知等)。Expo 提供了一个用于创建新项目的 CLI,以及一个用于学习和原型设计的沙盒应用,一个基于文件系统的导航系统 Expo Router,有 管理原生代码的工具,构建 和 交付 您的应用到应用商店,设置 空中更新,甚至 构建您自己的原生模块。除此之外,您还可以使用大多数其他开源 React Native 库,除了 Expo SDK。
Meta 也正式推荐开发者使用像 Expo 这样的 React Native 框架来创建新应用。这个建议的理由很直接:您要么 使用一个框架……要么 您自己构建一个框架。对于大多数 React Native 开发者来说,构建自己的框架并不是一个明智的选择。
2024 年 React Conf 上的 React Native 主题演讲
React vs React Native 基本元素
React Native 代码看起来非常类似于 React 代码。但在渲染组件时有一些明显的差异。
下面是一个在 Web 上渲染文本的方法:
export function MyTextComponent() {
return (
<div>Hello, web!</div>
);
}
这是在 React Native 中渲染相同内容的方法:
import { View, Text } from "react-native";
export function MyTextComponent() {
return (
<View>
<Text>Hello, native!</Text>
</View>
);
}
这些代码示例突出了 Web React 和原生 React 中非常重要的一个区别:React Native 没有 HTML 基元(div、input、form 等)。相反,等效的基元组件是从 react-native
库中导出的。因此,在 React Native 中渲染的任何内容将始终被包装在一个组件中。
在底层,React Native 将为每个平台渲染真实的原生组件,出于同样的原因,所有在 UI 中显示的文本都需要被包装在 Text
组件中。 如果不这样做,将导致您的应用崩溃。
以下是 React Native 中包含的一些关键 UI 基元:
View
最接近的 Web 等效项:div
View
是 div
在 React Native 中的等效项,使用方式相同:用于布局和样式。
ScrollView
最接近的 Web 等效项:div
在 Web 上,页面默认是可滚动的。原生应用并非如此。为了使页面可滚动,需要将其包装在 ScrollView
(或另一个虚拟列表如 FlatList
或 SectionList
)中。
Text
最接近的 Web 等效项:p
在 React Native 中渲染的所有文本都必须被包装在 Text
中。
Image
最接近的 Web 等效项:img
Image 组件允许您从 URL 和本地文件渲染图像。API 与 Web 略有不同,例如从 URL 加载图像,您会这样做:
import { Image } from "react-native";
export function MyImage() {
return <Image source={{ uri: "https://domain.com/static/my-image.png" }} />;
}
从本地文件渲染时,看起来像这样:
import { Image } from "react-native";
const imageSource = require("../assets/my-image.png");
export function MyImage() {
return <Image source={imageSource} />;
}
请注意,大多数生产应用不使用 React Native 的内置 Image 组件,而是选择使用 FastImage 或 Expo Image,因为这些库包括与样式、缓存相关的额外功能,并支持额外的图像格式。
FlatList
最接近的 Web 等效项:array.map()
许多 Web UI 依赖于渲染大量全屏项目列表 - 想想您的 Instagram 时间线或电子邮件收件箱中的电子邮件列表。在 Web 上,您可能通过映射数组来渲染这些,但这在原生平台上出于性能原因应该避免。相反,我们应该在 FlatList
中渲染这些 - 一个具有内置优化的虚拟列表,特别是在延迟渲染不需要的数据和仅在底层数据更改时重新渲染列表方面。
import { Text, FlatList, View } from "react-native";
const posts = [
{ id: "1", name: "Post 1" },
{ id: "2", name: "Post 2" },
];
export function MyList() {
return (
<FlatList
data={posts}
renderItem={({ item }) => (
<Text>{item.name}</Text>
)}
/>
);
}
TextInput
最接近的 Web 等效项:<input type="text" />
顾名思义,文本输入组件可用于任何基于文本的输入。与 Web 输入的主要区别在于,您只能将其用于文本(和数字)。虽然 TextInput 有一个 onChange
回调,但您通常使用 onChangeText
,其中回调只返回更新后的文本字符串。
import { useState } from "react";
import { StyleSheet, TextInput } from "react-native";
export function MyInput() {
const [value, setValue] = useState();
return <TextInput value={value} onChangeText={setValue} />;
}
TouchableOpacity
最接近的 Web 等效项:button
当您想让应用的某些部分响应点击时,通常的做法是将该区域包装在 TouchableOpacity
中。顾名思义,按下时该区域会自动高亮,您可以使用 activeOpacity
属性配置它被高亮的程度。
import { TouchableOpacity, Text } from "react-native";
export function MyButton() {
const onPress = () => {
console.log("Pressed!");
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Text>Button Text</Text>
</TouchableOpacity>
);
}
Pressable
Web 等效项:<button />
与 TouchableOpacity
类似,Pressable
组件用于创建按钮。它是 TouchableOpacity 的后继者,为触摸操作提供了更高级别的控制。
Switch
Web 等效项:<input type="radio" />
开关是一个切换组件,您可以将值切换为 true
或 false
。这是由于平台特定实现而在 iOS 和 Android 上看起来不同的 UI 元素的一个绝佳示例。
import { Switch } from "react-native";
export function MySwitch() {
const [value, setValue] = useState(false);
return (
<Switch
value={value}
onValueChange={(value) => setValue(value)}
trackColor={{ true: "pink" }}
/>
);
}
这是 React Native 中一些核心组件的非穷尽列表。有关详尽列表和更多详细信息,请参见官方 React Native 文档。
React Native 样式
如果您知道如何在 Web 上编写 CSS,您将能够相对容易地学习 React Native 的样式。大多数 CSS 属性都得到支持,但有一些差异:
没有全局样式
所有样式都是内联的,并通过 style
属性传递给组件。如果不使用样式库,就无法定义任何全局样式。为了在组件之间共享样式,您可以创建一个 theme
文件并将其导入到每个文件中。
import { View, Text } from "react-native";
export function MyComponent() {
return (
<View style={styles.container}>
<Text style={styles.greeting}>Set Reminder</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
padding: 24,
},
greeting: {
fontSize: 24
},
});
Flexbox,有一点不同
React Native 中的定位使用 Flexbox 完成。它的行为与 Web 上非常接近,但有一些差异:
- 所有元素默认具有
display: flex
flexDirection
默认为column
(而不是row
)alignContent
默认为flex-start
(而不是stretch
)flexShrink
默认为0
(而不是1
)flex
参数只支持单个数字
样式库
虽然内置的内联样式是为您的 React Native 应用设置样式的一个好选择,并且许多 React Native 应用根本不使用样式库,但对于那些希望获得替代样式体验的开发人员,有一些库是可用的。React Native 更受欢迎的一些样式库包括:
- NativeWind - 在 React Native 中使用 Tailwind 样式
- Styled Components - 使用 CSS 语法编写样式
- Tamagui - 用于跨平台样式和 UI 工具包
顺便说一句,我们将有一个直播系列来讨论 React Native 样式。这是第一集的链接:
React Native 样式介绍
避免常见的错误
一旦你知道了,你就知道了!但在你知道之前,你很可能会陷入这些常见的陷阱,这些通常是新接触 React Native 的开发人员经常遇到的:
不要使用 array.map 来渲染全屏项目列表
这在 Web 上是一个非常常见的模式。假设您有一个帖子列表,您想在页面上渲染它们。在 Web 上,您会这样做:
export function Posts() {
const posts = usePosts();
return (
<div>
{posts.map(post => <div key={post.id}>{post.name}</div/>)
</div>
);
}
在原生应用中,这不是推荐的方法,出于性能原因。相反,建议您使用虚拟列表组件,如 FlatList
:
import { FlatList } from "react-native";
export function Posts() {
const posts = usePosts();
return (
<FlatList
data={posts}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
</View>
)}
/>
);
}
这对原生应用有重要的性能增强,例如不渲染设备视口外部的项目。对于内置的 FlatList,一个更优化的流行替代品是 FlashList。
不要使用 React Native 的 Button 元素
React Native 提供了一个 Button 组件,它几乎不适合 99% 的用例,因为它不提供任何样式定制。最好忘记它的存在。
在 React Native 中构建按钮的方法是将按钮内容包装在 TouchableOpacity
或 Pressable
中:
import { TouchableOpacity, Text } from "react-native";
export function MyButton() {
const onPress = () => {
console.log("Pressed!");
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Text>Button Text</Text>
</TouchableOpacity>
);
}
不要在渲染中使用逻辑 AND (&&) 运算符
如前所述,任何要渲染的字符串都必须被包装在 Text
组件中。如果您不小心渲染了 NaN
或 0
,这将导致 React Native 尝试在没有文本组件的情况下渲染它。与 Web 不同,React Native 在处理 JavaScript 异常时非常不宽容:它实际上会导致您的应用崩溃。
旁注:它过去在渲染空字符串 (''
) 时也会崩溃,除非在 Text
之外,但值得庆幸的是,在较新的 React Native 版本中已经处理了这个问题。
这个老掉牙的问题!当使用逻辑 && 运算符时,请确保您的条件始终评估为布尔值,因为一些假值可能会产生意想不到的副作用!就我个人而言 - 我总是在 React Native 中使用内联 if 而不是冒着应用崩溃的风险 💥
- Kadi Kraman 💚
最好安全起见,在 React Native 中渲染时总是使用三元操作符:condition ? something : null
。
不要在应用代码中放置机密
在 Web 上,我们有前端和后端:客户端和服务器。人们理解,您永远不应该在客户端代码中拥有敏感信息,因为它可以被恶意行为者通过检查源代码或网络流量轻松访问。
React Native 应用是一个客户端应用!即使访问应用代码和网络流量并不像打开 Chrome 开发者工具那样容易,但仍然可能,所以您不应该在应用程序代码中放置任何真正敏感的秘密。像您的 Firebase 配置和其他识别您应用的各种后端服务的客户端 ID 存储在应用代码中是可以的。但任何敏感的 API 密钥和秘密都不应如此。
如果您的应用需要与需要秘密的 API 进行交互,而这些秘密不能包含在您的前端代码中,您可以以与构建网站相同的方式处理这个问题:部署一个 API 并从您的应用调用 API 端点。
使用 Expo Router,您可以在 React Native 代码库中编写 API 代码,并使用 API Routes 分开部署它们。
原生特有的新概念
以下是在构建原生应用与网站相比时需要考虑的一些新事项:
构建签名
iOS 和 Android 平台都有安全功能,防止用户从未经授权的来源安装应用。为了在物理设备上安装应用,它需要使用 签名凭据 进行签名。
构建签名在 iOS 和 Android 上的工作方式不同,但理念相同:在 Android 上,构建使用可以在本地机器上创建的 Keystore 进行签名。在 iOS 上,您需要一个配置文件和签名证书,只有在拥有付费的苹果开发者账户的情况下才能获得。如果您使用 EAS CLI 构建,签名凭据将自动创建和同步。
旁注 - 当您开始使用 Expo 时,您可能首先使用 Expo Go 进行开发。这绕过了构建签名限制,因为它已经是一个公开发布的应用。我们有这个工作流程,让您可以快速开始探索和学习,而无需繁琐的原生配置。然而,使用沙盒应用进行开发本质上是有限的,因为您将无法更改任何原生代码。因此,我们建议对于生产级应用使用 开发构建。
充分利用 Expo 的开发构建
深度链接
在 Web 上,链接到您网站的特定页面通常很简单,因为每个页面都有特定的 URL。在移动应用中,同样的想法(我们称之为深度链接)可能有点更复杂。
在移动应用中,您注册一个“方案”,这就像您的应用监听的关键字,比如 myapp://
(注意这看起来和 http://
和 email://
一样?这是因为在移动设备上它是同一种东西)。然后您有一个意图,如 myapp://my/page
,它将打开您的应用并给它 my/page
(或您添加的任何内容)作为深度链接的指南,但这取决于您监听该指南并链接到它。《您如何做到这一点》取决于您使用什么导航系统。如果您使用 Expo Router,由于基于文件系统的路由,它实际上是内置的。如果您只使用 React Navigation,它仍然是可配置的,但需要更多的工作。
手势和触摸
如果您想为移动平台构建具有原生感觉的体验,您将希望利用基于手势的交互,如滑动、长按、捏合缩放等。
React Native 的内置 Touchable 和 Pressable 元素具有诸如 onLongPress
等属性,并且 React Native Gesture Handler 通常用于为 React Native 应用构建更复杂的手势交互。
触觉反馈
触觉反馈 是指您的手机对某些交互做出响应时产生的轻微振动。重要的是要适量使用触觉反馈,但它确实增强了使用您的应用的体验,并使其感觉更加原生。例如,在 X 上点赞帖子,在 Gmail 应用上滑动帖子,或在主屏幕上长按应用图标都会触发触觉反馈。
尝试在应用的关键操作中添加一些 触觉反馈,比如在购物车中添加产品,或点赞帖子。
屏幕键盘
如果您的移动应用处理任何类型的文本输入,您将需要处理移动键盘。这里有一些事情需要考虑:
- 确保在键盘打开时文本输入保持可见,而不是被键盘覆盖 - 使用 KeyboardAvoidingView
- 当您聚焦在文本输入上时,键盘会自动打开,但您也可以 以编程方式 显示和隐藏键盘
- 在 ScrollView 上使用 keyboardShouldPersistTaps 属性来处理键盘在外部点击时应如何表现(是否应该关闭或不关闭)
- 使用 TextInput 上的 keyboardType 属性来确定显示哪种键盘,例如数字键盘。其他有用的属性,用于配置键盘布局包括:autoCapitalize, autoComplete, autoCorrect 和 returnKeyType
动画
CSS 动画在 React Native 中不存在。相反,我们有 布局动画(在 Android 上是实验性的,谨慎使用)和 Animated API。
在大多数情况下,如果您正在构建自己的动画,您会想使用 Reanimated 库。它是一个基于 React Hooks 的 React Native 动画库,API 通常比 React Native 内置的 Animated API 更容易接近。
像素密度
像素密度是衡量多少像素组成显示点的指标。您是否注意到在 React Native 中定义样式时,我们不使用 px
或 em
或 rem
?那是因为您可以使用的唯一单位是 dp
- 一个显示点。多少像素组成一个显示点在设备之间差异很大。您可以使用 PixelRatio 访问设备的像素密度。
这在渲染图像时尤其重要。您希望图像在 UI 中看起来清晰,但您也希望只渲染尽可能接近它们实际渲染区域大小的图像。因此,例如,如果您在具有像素密度为 3
的现代 iPhone 上渲染 width: 100; height: 100
的图像,图像需要是 300x300
像素。然而,低端 Android 手机可能有像素密度为 1.5
,这意味着您需要一个 150x150
像素的图像。在较小的区域上渲染较大的图像可以工作,但会占用大量内存(如果经常这样做可能会导致性能问题和崩溃)。然而,在较大的区域上显示过小的图像会看起来像素化。
因此,理想情况下,对于网络图像,您希望根据所需大小在服务器上调整图像大小,以实现尽可能接近的拟合,对于包含在应用包中的静态图像,您可以使用 @2x
和 @3x
后缀来提供 不同屏幕密度的图像。
部署到 App Store 和 Play Store
应用交付给用户的方式与 Web 非常不同。
需要牢记的一个重要事项是,当您发布应用的新版本时,它 不会 自动交付给所有用户。无论是苹果还是谷歌都没有提供一种机制来强制用户升级,因此您需要为自己构建此功能,或者确保您所做的任何更改都是向后兼容的。
要将应用发布给用户,您需要付费的 Google Play Console(一次性 $25 费用)和 Apple Developer(每年 $99 费用)账户,并设置您的商店上市页面,包括 应用截图。请注意,您的应用签名凭据将永远与特定账户绑定,所以您不能在不同的账户下发布相同的应用,除非您更改签名凭据,尽管应用转让是可能的。
您提交给应用商店的原生应用包与您用于开发的构建不同:它使用不同的签名凭据,是一个优化的构建,没有经过商店就不能直接安装在设备上。更多关于不同类型的构建的信息,请参阅我们的 EAS 视频课程 或 基于文本的教程。
然后,对于每个版本,您需要上传应用包并提交审核。您的应用每次发布都会经过各种手动和自动的指南和检查。审核可能需要从 几个小时到几天。如果您的应用需要登录,例如,您将不得不提供工作凭证供审核员使用。
您的应用可能因各种原因被拒绝(iOS 审核往往比 Android 更严格),例如,当我第一次提交 React Conf 应用到 iOS 时,它因为应用包含用于更改应用图标的原生代码而被拒绝,但审核员找不到该功能。我回复审核员,解释如何通过快捷操作访问它,应用最终在几个小时后获得批准。如果您的应用在启动时崩溃,如果审核员无法登录,如果您的使用描述不够描述性,以及许多其他原因,您的应用也会被拒绝。通常的解决方法是要么回复审核员以解释他们错过了什么,要么在应用内修复代码并重新提交新版本进行审核。
然而,一旦您的应用在商店中,您可以使用像 EAS Update 这样的空中更新来提供错误修复,而无需经过审核过程,只要任何推送的更改都遵守 商店指南。