搭建开发环境
- 选择
React Native CLI Quickstart - 开发平台 macOS
- 目标平台 Android
- 其他按上述链接步骤安装,注意
node 请检查其版本是否在 22.11.0 以上
mac安装Homebrew
- 安装pkg文件
- 若终端还是无法识别brew指令,说明需要配置环境变量 blog.csdn.net/m0_60508194…
Node & Watchman
brew install node@22
brew install watchman
npm install -g yarn
Java Development Kit
brew install --cask zulu@17
# 获得 JDK 安装程序的路径
brew info --cask zulu@17
# ==> zulu@17: <版本号>
# https://www.azul.com/downloads/
# Installed
# /opt/homebrew/Caskroom/zulu@17/<版本号> (185.8MB) (注意在 Intel 芯片的 Mac 上,路径可能是 /usr/local/Caskroom/zulu@17/<版本号>)
# Installed using the formulae.brew.sh API on 2024-06-06 at 10:00:00
# 导航到上面打印出来的路径
open /opt/homebrew/Caskroom/zulu@17/<版本号>
# 或者可能是 /usr/local/Caskroom/zulu@17/<版本号>
打开 Finder,双击 Double-Click to Install Azul Zulu JDK 17.pkg 包来安装 JDK。
安装 JDK 后,请更新 JAVA_HOME 环境变量。
配置Android 开发环境
1. 安装 Android Studio
首先下载和安装 Android Studio, 安装界面中选择"Custom"选项,确保选中了以下几项:
Android SDKAndroid SDK PlatformAndroid Virtual Device
2. 安装 Android SDK
你可以在 Android Studio 的欢迎界面中找到 SDK Manager。点击"Configure",然后就能看到"SDK Manager"。
SDK Manager 还可以在 Android Studio 的"Preferences"菜单中找到。具体路径是Appearance & Behavior → System Settings → Android SDK。
3. 配置ANDROID_HOME 环境变量
- 打开 macOS 环境配置文件 nano ~/.zshrc
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
- 环境变量设置立即生效 source ~/.zshrc
创建新项目
npx @react-native-community/cli init 项目名
准备 Android 设备
使用 Android 真机
你也可以使用 Android 真机来代替模拟器进行开发,只需用 usb 数据线连接到电脑,然后遵照在设备上运行这篇文档的说明操作即可。
只需用 usb 数据线连接到电脑, 提示手机授权后,打开开发者调试.
使用 Android 模拟器
你可以使用 Android Studio 打开项目下的"android"目录,然后可以使用"AVD Manager"来查看可用的虚拟设备。如果你刚刚才安装 Android Studio,那么可能需要先创建一个虚拟设备。点击"Create Virtual Device...",然后选择所需的设备类型并点击"Next".
编译并运行 React Native 应用
确保你先运行了模拟器或者连接了真机,然后在你的项目目录中运行yarn android。
使用adb devices命令 检查你的设备是否能正确连接
- 若以下指令没有显示手机设备,说明数据线没有连接设置成功,后面的手机调试就不会成功
emulator-xxxAndroid Studio软件开启的虚拟设备, 不带emulator表示物理设备offline表示虚拟或物理设备离线device表示当前设备处于激活有效状态
$ adb devices
List of devices attached
emulator-5554 device # 模拟器
14ed2fcc device # usb数据线开发者打开后显示的手机设备
- 显示以上 模拟器 + 手机设备后,启动项目,
npm run start
npm run android
注意:如果npm run android 报错 可能是上面的虚拟设备或者物理设备都没有激活。再次检测Android Studio软件开启的虚拟设备 或 数据线连接的手机设备是否允许传输文件等。另外项目启动失败,注意检测node版本,也可以全局设置node默认版本。
nvm use 22
nvm alias default 22
- 手机就会有显示上述脚手架创建的App,每次更改项目代码,手机上的App会自动刷新
r - reload app(s) // 加载刷新
d - open Dev Menu // 开启开发辅助工具 开启fast refresh
j - open DevTools // 电脑开启日志控台 就可以看到console.log日志了
RN入门基础
以下示例对应的基础包版本信息如下
"react": "19.2.3",
"react-native": "0.84.1",
"engines": {
"node": ">= 22.11.0"
}
React 基础
React Native 的基础是React, React 的核心概念:
- components 组件
- JSX
- props 属性
- state 状态 可以使用React 的
useStateHook来为组件添加状态
import React, { useState } from "react";
import { Button, Text, View } from "react-native";
const Cat = (props) => {
const [isHungry, setIsHungry] = useState(true);
return (
<View>
<Text>
I am {props.name}, and I am {isHungry ? "hungry" : "full"}!
</Text>
<Button
onPress={() => {
setIsHungry(false);
}}
disabled={!isHungry}
title={isHungry ? "Pour me some milk, please!" : "Thank you!"}
/>
</View>
);
}
const Cafe = () => {
return (
<>
<Cat name="Munkustrap" />
<Cat name="Spot" />
</>
);
}
export default Cafe;
RN常用基础组件
- View 搭建用户界面的最基础组件。容器组件 类似web的
div - Text 显示文本内容的组件。文本显示组件 类似web的
p - Image 图片显示组件
source图片源
import {Image, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
tinyLogo: {
width: 50,
height: 50,
},
});
<Image
style={styles.tinyLogo}
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
}}
/>
- TextInput 文本输入框。 类似web的
input
import React from 'react';
import {StyleSheet, TextInput} from 'react-native';
import {SafeAreaView, SafeAreaProvider} from 'react-native-safe-area-context';
const TextInputExample = () => {
const [text, onChangeText] = React.useState('Useless Text');
const [number, onChangeNumber] = React.useState('');
return (
<SafeAreaProvider>
<SafeAreaView>
<TextInput
style={styles.input}
onChangeText={onChangeText}
value={text}
/>
<TextInput
style={styles.input}
onChangeText={onChangeNumber}
value={number}
placeholder="useless placeholder"
keyboardType="numeric"
/>
</SafeAreaView>
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default TextInputExample;
- ScrollView 可滚动的容器视图。
import React from 'react';
import {
StyleSheet,
Text,
SafeAreaView,
ScrollView,
StatusBar,
} from 'react-native';
const App = () => {
return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView}>
<Text style={styles.text}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</Text>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: StatusBar.currentHeight,
},
scrollView: {
backgroundColor: 'pink',
marginHorizontal: 20,
},
text: {
fontSize: 42,
},
});
export default App;
- FlatList 高性能的简单列表组件
- 支持单独的头部组件。
- 支持单独的尾部组件。
- 支持自定义行间分隔线。
- 支持下拉刷新。
- 支持上拉加载。
- Button 安卓和ios样式不同
<Button
onPress={onPressLearnMore}
title="Learn More"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/>
- Pressable 是一个核心组件的封装, 常用来封装按钮组件
<Pressable onPress={onPressFunction}>
<Text>I'm pressable!</Text>
</Pressable>
- TouchableOpacity 本组件用于封装视图,使其可以正确响应触摸操作。当按下的时候,封装的视图的不透明度会降低
import React, { useState } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
const App = () => {
const [count, setCount] = useState(0);
const onPress = () => setCount(prevCount => prevCount + 1);
return (
<View style={styles.container}>
<View style={styles.countContainer}>
<Text>Count: {count}</Text>
</View>
<TouchableOpacity
style={styles.button}
onPress={onPress}
>
<Text>Press Here</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 10
},
button: {
alignItems: "center",
backgroundColor: "#DDDDDD",
padding: 10
},
countContainer: {
alignItems: "center",
padding: 10
}
});
export default App;
样式
- 所有的核心组件都接受名为
style的属性,这些样式名基本上是遵循了 web 上的 CSS 的命名,只是按照 JS 的语法要求使用了驼峰命名法,例如将background-color改为backgroundColor。我们建议使用StyleSheet.create来集中定义组件的样式
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
const LotsOfStyles = () => {
return (
<View style={styles.container}>
<Text style={styles.red}>just red</Text>
<Text style={styles.bigBlue}>just bigBlue</Text>
<Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
<Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 50,
},
bigBlue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});
export default LotsOfStyles;
-
React Native 中的尺寸都是无单位的
-
使用 Flexbox 布局
React Native 中的 Flexbox 的工作原理和 web 上的 CSS 基本一致,当然也存在少许差异。首先是默认值不同:
flexDirection的默认值为column(而不是row),alignContent默认值为flex-start(而不是stretch),flexShrink默认值为0(而不是1), 而flex只能指定一个数字值。组件能够撑满剩余空间的前提是其父容器的尺寸不为零。如果父容器既没有固定的
width和height,也没有设定flex,则父容器的尺寸为零。其子组件如果使用了flex,也是无法显示的。
第三方组件
除了简单的npm i xxx下载之外,必须查看github或npm文档的使用说明,可能需要在项目android和ios目录添加配置变更等,不然组件无法使用
geolocation 获取位置 '@react-native-community/geolocation'
-
- 安装包
yarn add @react-native-community/geolocation
- 安装包
"@react-native-community/geolocation": "^3.4.0",
-
安卓设备必须配置 [ios设备需要参照文档操作]
// 在 `AndroidManifest.xml`文件 添加以下
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
// 或者
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
- 3. 由于 '@react-native-community/geolocation' 已经从 从 React Native 核心模块迁移。所以需要如下代码
import Geolocation from '@react-native-community/geolocation';
Geolocation.setRNConfiguration(config);
-
- 示例
import React, { useCallback, useState, useEffect } from 'react';
import Geolocation from '@react-native-community/geolocation';
import { View, Alert, Platform, Text, Linking, Button } from 'react-native';
import {
request,
PERMISSIONS,
RESULTS,
openSettings,
} from 'react-native-permissions';
const GeoLocationExample = () => {
const [locationData, setLocationData] = useState({});
useEffect(() => {
// 在应用启动时配置 Geolocation 模块
configureGeolocation();
}, []);
const configureGeolocation = () => {
Geolocation.setRNConfiguration({
// skipPermissionRequests: false, // 如果你会自己处理权限请求,可以设为 true
locationProvider: 'playServices', // 🔴 关键配置:强制使用 Google Play 服务定位 API
// enableBackgroundLocationUpdates: true, // 如果需要后台定位可以开启
});
console.log('Geolocation configured to use playServices');
};
// 请求位置权限
const requestLocationPermission = useCallback(async () => {
if (Platform.OS === 'ios') {
const result = await request(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);
return result === RESULTS.GRANTED;
} else {
const result = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
console.log('result---', result);
return result === RESULTS.GRANTED;
}
}, []);
const onGetLocation = async () => {
const hasPermission = await requestLocationPermission();
if (hasPermission) {
const location = await getCurrentLocation();
console.log('getCurrentLocation---location', location);
setLocationData(location);
} else {
Alert.alert(
'需要位置权限',
'我们需要访问您的位置信息才能继续操作,请在设置中开启位置权限',
[
{
text: '取消',
style: 'cancel',
onPress: () => {
console.log('用户取消授权');
},
},
{
text: '去设置',
onPress: () => {
console.log('用户前往设置页面');
// 打开应用设置页面
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:');
} else {
openSettings(); // 打开安卓手机的应用权限设置界面
}
},
},
],
{ cancelable: false },
);
}
};
// 获取位置信息
const getCurrentLocation = useCallback(() => {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
position => {
console.log('getCurrentPosition---', position);
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
});
},
error => {
console.error('getCurrentPosition---error', error);
reject(error);
},
{ enableHighAccuracy: true, timeout: 15000 },
);
});
}, []);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>locationData.latitude:{locationData.latitude}</Text>
<Text>locationData.longitude:{locationData.longitude}</Text>
<Button title="获取位置" onPress={onGetLocation}></Button>
</View>
);
};
export default GeoLocationExample;
拍照+选取图片+录视频 "react-native-image-picker"
- 安装包
"react-native-image-picker": "^8.2.1",
安卓配置, 配置完成后就可以在手机设置里面看到该应用管理中看到对应的权限。打开android/app/src/main/AndroidManifest.xml,在<manifest>标签内添加以下权限:
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 存储权限(如需访问文件) 兼容 Android 12 及以下版本的旧权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 选择图片 Android 13+ 必需的新权限(按需添加) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Include this only if you are planning to use the microphone for video recording -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
- 示例代码
import React, { useCallback, useState } from 'react';
import {
View,
Alert,
Platform,
Text,
Linking,
TouchableOpacity,
Image,
ScrollView,
StyleSheet,
} from 'react-native';
import {
request,
PERMISSIONS,
RESULTS,
openSettings,
} from 'react-native-permissions';
import * as ImagePicker from 'react-native-image-picker';
const TakePhotoExample = () => {
const [imageUri, setImageUri] = useState('');
const [videoUri, setVideoUri] = useState('');
const [mediaType, setMediaType] = useState(''); // 'photo' 或 'video'
// 请求相机权限
const requestCameraPermission = useCallback(async () => {
if (Platform.OS === 'ios') {
const result = await request(PERMISSIONS.IOS.CAMERA);
return result === RESULTS.GRANTED;
} else {
const result = await request(PERMISSIONS.ANDROID.CAMERA);
return result === RESULTS.GRANTED;
}
}, []);
// 请求相册权限
const requestGalleryPermission = useCallback(async () => {
if (Platform.OS === 'ios') {
const result = await request(PERMISSIONS.IOS.PHOTO_LIBRARY);
return result === RESULTS.GRANTED;
} else {
// Android 13+ 需要新的权限
if (Platform.Version >= 33) {
const result = await request(PERMISSIONS.ANDROID.READ_MEDIA_IMAGES);
return result === RESULTS.GRANTED;
} else {
const result = await request(PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE);
return result === RESULTS.GRANTED;
}
}
}, []);
// 显示权限提示
const showPermissionAlert = permissionType => {
const messages = {
camera: {
title: '需要相机权限',
message: '我们需要访问您的相机权限才能继续操作,请在设置中开启相机权限',
},
gallery: {
title: '需要相册权限',
message: '我们需要访问您的相册权限才能继续操作,请在设置中开启相册权限',
},
};
const msg = messages[permissionType];
Alert.alert(
msg.title,
msg.message,
[
{
text: '取消',
style: 'cancel',
},
{
text: '去设置',
onPress: () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:');
} else {
openSettings();
}
},
},
],
{ cancelable: false },
);
};
// 拍照
const onTakePhoto = async () => {
const hasPermission = await requestCameraPermission();
if (hasPermission) {
const res = await ImagePicker.launchCamera({
mediaType: 'photo',
includeBase64: false,
quality: 0.8,
saveToPhotos: true, // 保存到相册
});
console.log('拍照结果:', res);
if (res.didCancel) {
console.log('用户取消了拍照');
} else if (res.error) {
console.log('拍照出错:', res.error);
Alert.alert('拍照失败', res.error);
} else {
const { assets } = res;
if (assets && assets.length > 0) {
setImageUri(assets[0].uri);
setMediaType('photo');
setVideoUri(''); // 清除视频
}
}
} else {
showPermissionAlert('camera');
}
};
// 录视频
const onRecordVideo = async () => {
const hasPermission = await requestCameraPermission();
if (hasPermission) {
const res = await ImagePicker.launchCamera({
mediaType: 'video',
videoQuality: 'high',
durationLimit: 60, // 最长录制60秒
includeBase64: false,
saveToPhotos: true,
});
console.log('录制结果:', res);
if (res.didCancel) {
console.log('用户取消了录制');
} else if (res.error) {
console.log('录制出错:', res.error);
Alert.alert('录制失败', res.error);
} else {
const { assets } = res;
if (assets && assets.length > 0) {
setVideoUri(assets[0].uri);
setMediaType('video');
setImageUri(''); // 清除图片
console.log('视频信息:', {
uri: assets[0].uri,
duration: assets[0].duration,
fileSize: assets[0].fileSize,
});
}
}
} else {
showPermissionAlert('camera');
}
};
// 从相册选择图片
const onSelectImage = async () => {
const hasPermission = await requestGalleryPermission();
if (hasPermission) {
const res = await ImagePicker.launchImageLibrary({
mediaType: 'photo',
includeBase64: false,
quality: 0.8,
selectionLimit: 1, // 限制选择数量
});
console.log('选择图片结果:', res);
if (res.didCancel) {
console.log('用户取消了选择');
} else if (res.error) {
console.log('选择出错:', res.error);
Alert.alert('选择失败', res.error);
} else {
const { assets } = res;
if (assets && assets.length > 0) {
setImageUri(assets[0].uri);
setMediaType('photo');
setVideoUri(''); // 清除视频
}
}
} else {
showPermissionAlert('gallery');
}
};
// 从相册选择视频
const onSelectVideo = async () => {
const hasPermission = await requestGalleryPermission();
if (hasPermission) {
const res = await ImagePicker.launchImageLibrary({
mediaType: 'video',
includeBase64: false,
selectionLimit: 1,
});
console.log('选择视频结果:', res);
if (res.didCancel) {
console.log('用户取消了选择');
} else if (res.error) {
console.log('选择出错:', res.error);
Alert.alert('选择失败', res.error);
} else {
const { assets } = res;
if (assets && assets.length > 0) {
setVideoUri(assets[0].uri);
setMediaType('video');
setImageUri(''); // 清除图片
console.log('视频信息:', {
uri: assets[0].uri,
duration: assets[0].duration,
fileSize: assets[0].fileSize,
});
}
}
} else {
showPermissionAlert('gallery');
}
};
// 清除当前媒体
const onClear = () => {
setImageUri('');
setVideoUri('');
setMediaType('');
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>媒体选择示例</Text>
{/* 媒体预览区域 */}
{mediaType === 'photo' && imageUri ? (
<View style={styles.previewContainer}>
<Image source={{ uri: imageUri }} style={styles.imagePreview} />
<Text style={styles.previewText}>图片预览</Text>
<TouchableOpacity style={styles.clearButton} onPress={onClear}>
<Text style={styles.clearButtonText}>清除</Text>
</TouchableOpacity>
</View>
) : mediaType === 'video' && videoUri ? (
<View style={styles.previewContainer}>
<View style={styles.videoPlaceholder}>
<Text style={styles.videoIcon}>🎥</Text>
<Text style={styles.videoText}>视频已录制</Text>
<Text style={styles.videoPath} numberOfLines={1}>
{videoUri}
</Text>
</View>
<TouchableOpacity style={styles.clearButton} onPress={onClear}>
<Text style={styles.clearButtonText}>清除</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.emptyPreview}>
<Text style={styles.emptyText}>暂无媒体</Text>
</View>
)}
{/* 操作按钮区域 */}
<View style={styles.buttonGroup}>
<Text style={styles.groupTitle}>相机操作</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={onTakePhoto}
>
<Text style={styles.buttonText}>📸 拍照</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.successButton]}
onPress={onRecordVideo}
>
<Text style={styles.buttonText}>🎥 录视频</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.buttonGroup}>
<Text style={styles.groupTitle}>相册选择</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.infoButton]}
onPress={onSelectImage}
>
<Text style={styles.buttonText}>🖼️ 选择图片</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.warningButton]}
onPress={onSelectVideo}
>
<Text style={styles.buttonText}>🎬 选择视频</Text>
</TouchableOpacity>
</View>
</View>
{/* 显示媒体信息 */}
{(imageUri || videoUri) && (
<View style={styles.infoContainer}>
<Text style={styles.infoTitle}>媒体信息:</Text>
<Text style={styles.infoText} numberOfLines={2}>
{mediaType === 'photo' ? imageUri : videoUri}
</Text>
</View>
)}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flexGrow: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
textAlign: 'center',
marginBottom: 20,
},
previewContainer: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 15,
marginBottom: 20,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
imagePreview: {
width: '100%',
height: 250,
borderRadius: 8,
marginBottom: 10,
},
videoPlaceholder: {
width: '100%',
height: 150,
backgroundColor: '#1e1e2f',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
},
videoIcon: {
fontSize: 48,
marginBottom: 10,
},
videoText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 5,
},
videoPath: {
color: '#aaa',
fontSize: 12,
paddingHorizontal: 10,
},
previewText: {
fontSize: 14,
color: '#666',
marginBottom: 10,
},
clearButton: {
backgroundColor: '#dc3545',
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 6,
},
clearButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
},
emptyPreview: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 40,
marginBottom: 20,
alignItems: 'center',
borderWidth: 2,
borderColor: '#ddd',
borderStyle: 'dashed',
},
emptyText: {
fontSize: 16,
color: '#999',
},
buttonGroup: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 15,
marginBottom: 15,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
groupTitle: {
fontSize: 16,
fontWeight: '600',
color: '#555',
marginBottom: 12,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 10,
},
button: {
flex: 1,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
},
primaryButton: {
backgroundColor: '#007AFF',
},
successButton: {
backgroundColor: '#34C759',
},
infoButton: {
backgroundColor: '#5856D6',
},
warningButton: {
backgroundColor: '#FF9500',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
infoContainer: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 15,
marginTop: 10,
},
infoTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 5,
},
infoText: {
fontSize: 12,
color: '#666',
},
});
export default TakePhotoExample;
下载文件 'react-native' Linking
本地存储 @react-native-async-storage/async-storage
web容器 'react-native-webview'
- 安装包
react-native-webview
"react-native-webview": "^13.16.1",
- Rn WebViewScreen
示例
import { useRef, useCallback, useLayoutEffect } from 'react';
import { View, StyleSheet, ActivityIndicator, Alert, Text } from 'react-native';
import { WebView } from 'react-native-webview';
import { useNavigation } from '@react-navigation/native';
const WebViewScreen = ({ route }) => {
console.log('WebViewScreen---start');
const navigation = useNavigation();
const webViewRef = useRef(null);
const { url, title } = route.params; // url: h5页面链接地址
// 设置导航标题
useLayoutEffect(() => {
navigation.setOptions({ title: title || '加载中...' });
}, [navigation, title]);
// 处理从H5发来的消息
const handleMessage = useCallback(async event => {
try {
const data = JSON.parse(event.nativeEvent.data);
console.log('data', data);
const { type, callbackId, params } = data;
console.log('type', type, 'params', params);
let result = null;
let error = null;
switch (type) {
case 'getUserInfo':
// 模拟用户信息
result = {
id: '123456',
name: '测试用户',
avatar: 'https://www.example.com/avatar.jpg',
phone: '13800138000',
};
break;
case 'getOrders':
// 模拟订单数据
result = [
{
id: '1',
orderNo: 'ORD2024001',
amount: 199,
status: 'pending',
},
{ id: '2', orderNo: 'ORD2024002', amount: 299, status: 'paid' },
];
break;
case 'getAuthToken':
// 模拟token
result = 'mock_token_123456';
break;
default:
error = `未知方法: ${type}`;
}
// 将结果回调给H5
if (webViewRef.current && callbackId) {
const callbackScript = `
window.dispatchEvent(new CustomEvent('RNCallback', {
detail: ${JSON.stringify({ callbackId, result, error })}
}));
true;
`;
webViewRef.current.injectJavaScript(callbackScript);
}
} catch (error) {
console.error('处理H5消息失败:', error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 注入到H5的JavaScript代码
const injectedJavaScript = `
(function() {
// RN Bridge 对象
window.RNBridge = {
callbacks: {},
// 通用调用方法
call(method, params = {}) {
return new Promise((resolve, reject) => {
const callbackId = Date.now() + '_' + Math.random().toString(36);
this.callbacks[callbackId] = { resolve, reject };
window.ReactNativeWebView.postMessage(JSON.stringify({
type: method,
params,
callbackId
}));
});
},
// 获取用户信息
getUserInfo(data) {
return this.call('getUserInfo', data);
},
// 获取订单列表
getOrders(data) {
return this.call('getOrders', data);
},
// 获取认证Token
getAuthToken(data) {
return this.call('getAuthToken', data);
},
};
// 监听RN回调
window.addEventListener('RNCallback', function(event) {
const { callbackId, result, error } = event.detail;
const callback = window.RNBridge.callbacks[callbackId];
if (callback) {
if (error) {
callback.reject(new Error(error));
} else {
callback.resolve(result);
}
delete window.RNBridge.callbacks[callbackId];
}
});
console.log('RNBridge 注入成功!');
console.log('可用方法:', Object.keys(window.RNBridge).filter(key => key !== 'callbacks'));
// 通知RN注入完成
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'bridgeReady',
callbackId: 'ready'
}));
})();
`;
// WebView加载状态
const renderLoading = () => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>加载中...</Text>
</View>
);
// 处理WebView错误
const handleError = syntheticEvent => {
const { nativeEvent } = syntheticEvent;
console.error('WebView错误:', nativeEvent);
Alert.alert('加载失败', '页面加载失败,请检查网络后重试', [
{ text: '确定', onPress: () => navigation.goBack() },
]);
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: url }}
onMessage={handleMessage}
injectedJavaScript={injectedJavaScript}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
renderLoading={renderLoading}
onError={handleError}
onHttpError={handleError}
style={styles.webview}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
webview: {
flex: 1,
},
loadingContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
loadingText: {
marginTop: 10,
fontSize: 14,
color: '#666',
},
});
export default WebViewScreen;
h5页面代码
const onTestGetUserInfo = async () => {
console.log('onTestGetUserInfo---start', window.RNBridge)
try {
const result = await window.RNBridge.getUserInfo({ id: 88 })
console.log('getUserInfo---result', result)
} catch (error) {
console.error('getUserInfo---error', error)
}
}
React中的路由
安装包
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/native": "^7.1.33",
"@react-navigation/native-stack": "^7.14.4",
"react-native-screens": "^4.24.0",
| React Navigation | 类比 React Router | 说明 |
|---|---|---|
NavigationContainer | BrowserRouter | 根容器,提供导航上下文 |
Stack.Navigator | Routes | 定义一组页面容器,有堆栈效果 |
Stack.Screen | Route | 单个页面配置 |
Tab.Navigator | 无直接对应 | 标签栏容器 |
Tab.Screen | 无直接对应 | 标签页配置 |
useNavigation | useNavigate | 导航 Hook |
useRoute | useParams | 获取路由参数 |
嵌套导航器的工作原理
NavigationContainer 下确实只能有一个根 Navigator,但这个根 Navigator 可以是 Stack.Navigator、Tab.Navigator 等类型,而且它本身可以嵌套其他导航器。
-
NavigationContainer的作用:是一个顶层容器组件,它负责管理整个应用的导航状态,它的角色类似于 Web 开发中的BrowserRouter。 -
根 Navigator 只能有一个,但可以嵌套:
NavigationContainer的直接子节点只能是一个Navigator(无论是Stack.Navigator还是Tab.Navigator) 。这是 React Navigation 的规则,如果尝试放置两个平行的Navigator,会抛出错误 。但这个唯一的根 Navigator 内部可以嵌套其他 Navigator。这正是 React Navigation 实现复杂路由的方式 。 -
嵌套导航器的核心机制:
Stack.Screen的component可以是另一个Navigator你观察到的现象——Stack.Screen的component可以是Tab.Navigator——这是嵌套导航器的核心用法 。 这种模式的含义是:将整个Tab.Navigator当作一个“页面”放进Stack.Navigator的一个Screen中。当导航到这个页面时,整个标签导航器就会被渲染出来。
路由示例
一个简单的路由示例
// 定义一个 Tab Navigator(这是一个独立的导航器组件)
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Messages" component={MessagesScreen} />
</Tab.Navigator>
);
}
// 根导航器
function RootStack() {
return (
<Stack.Navigator>
{/* Stack.Screen 的 component 直接使用了 HomeTabs 这个导航器组件 */}
<Stack.Screen
name="Home"
component={HomeTabs}
options={{ headerShown: false }}
/>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
// 顶层
export default function App() {
return (
<NavigationContainer>
{/* 只有一个根 Navigator */}
<RootStack />
</NavigationContainer>
);
}
一个具体的应用示例
- index.js
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
- App.js
import { SafeAreaProvider } from 'react-native-safe-area-context';
import Router from './src/router';
const App = () => {
return (
<SafeAreaProvider>
<Router />
</SafeAreaProvider>
);
};
export default App;
- src/router.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// 导入页面组件
import HomeScreen from '../screens/HomeScreen';
import OrderScreen from '../screens/OrderScreen';
import ProfileScreen from '../screens/ProfileScreen';
import WebViewScreen from '../screens/WebViewScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
// 路由配置(类似Vue Router的routes数组)
const routes = [
{
name: 'Home',
component: HomeScreen,
options: { title: '首页' },
tabBar: true, // 标记为tab页面
},
{
name: 'Orders',
component: OrderScreen,
options: { title: '订单' },
tabBar: true,
},
{
name: 'Profile',
component: ProfileScreen,
options: { title: '我的' },
tabBar: true,
},
{
name: 'WebView',
component: WebViewScreen,
options: { title: '加载中...' },
tabBar: false, // 不是tab页面
},
];
// 底部Tab导航(类似Vue的tabbar配置)
function TabNavigator() {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: 'gray',
}}
>
{routes
.filter(route => route.tabBar)
.map(route => (
<Tab.Screen
key={route.name}
name={route.name}
component={route.component}
options={route.options}
/>
))}
</Tab.Navigator>
);
}
// 根路由配置(类似Vue Router的根路由)
export default function Router() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Main"
component={TabNavigator}
options={{ headerShown: false }}
/>
{routes
.filter(route => !route.tabBar)
.map(route => (
<Stack.Screen
key={route.name}
name={route.name}
component={route.component}
options={route.options}
/>
))}
</Stack.Navigator>
</NavigationContainer>
);
}
常用路由api
import { useNavigation, useRoute } from '@react-navigation/native';
const navigation = useNavigation();
const route = useRoute();
- 导航
// 最基本的用法
navigation.navigate('Profile');
// 带参数跳转
navigation.navigate('Profile', { userId: '123' });
// 在详情页再次 push 到新的详情页(可以无限次)
//【`push` 和 `navigate` 的核心区别:`navigate` 如果已经在目标页面可能不会创建新实例,而 `push` **一定会**压入新页面。】
navigation.push('Detail', { id: '456' });
// 返回
navigation.goBack();
// 用新页面替换当前页面(用户无法返回当前页)
navigation.replace('Login');
- 获取参数
route.params
import { useNavigation, useRoute } from '@react-navigation/native';
const route = useRoute();
const { userId } = route.params;
在 React Native 中发起数据请求
Fetch API
// GET 请求示例
const getMovies = async () => {
try {
const response = await fetch('https://reactnative.dev/movies.json');
const json = await response.json();
console.log(json.movies);
} catch (error) {
console.error('请求失败:', error);
}
};
// POST 请求示例
const createPost = async (data) => {
try {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log('创建成功:', result);
} catch (error) {
console.error('请求失败:', error);
}
};
优缺点:
Axios
状态共享方案 Zustand (类似 Pinia)
- 安装
# 使用 npm 或者 yarn
npm install zustand
# 或者
yarn add zustand
"zustand": "^5.0.11"
- 创建一个 Store 模块
// `src/stores/useCounterStore.js`
import { create } from 'zustand';
// 创建一个名为 useCounterStore 的 Hook
const useCounterStore = create((set, get) => ({
// --- 1. 定义状态 (类似 Pinia 的 state) ---
count: 0,
step: 1,
// --- 2. 定义动作 (类似 Pinia 的 actions) ---
// 直接修改状态
increment: () => set((state) => ({ count: state.count + state.step })),
decrement: () => set((state) => ({ count: state.count - state.step })),
// 通过传入值修改
setStep: (newStep) => set({ step: newStep }),
// 异步动作 (原生支持,无需中间件)
incrementAsync: async () => {
// 模拟一个 API 调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 使用 get() 获取最新状态
set({ count: get().count + get().step });
},
}));
export default useCounterStore;
- 在 React Native 组件中使用
// `src/screens/CounterScreen.js`
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import useCounterStore from '../stores/useCounterStore';
const CounterScreen = () => {
// --- 核心用法:通过选择器(selector)订阅特定的状态片段 ---
// 只有这样,组件才会精确地在 count 变化时重新渲染
const count = useCounterStore((state) => state.count);
const step = useCounterStore((state) => state.step);
// 获取 actions(actions 本身是稳定的,不会导致不必要的渲染)
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const setStep = useCounterStore((state) => state.setStep);
const incrementAsync = useCounterStore((state) => state.incrementAsync);
return (
<View style={styles.container}>
<Text style={styles.text}>当前计数: {count}</Text>
<Text style={styles.text}>当前步长: {step}</Text>
<View style={styles.buttonRow}>
<Button title="-" onPress={decrement} />
<Button title="+" onPress={increment} />
</View>
<Button title="异步加" onPress={incrementAsync} />
<View style={styles.buttonRow}>
<Button title="设置步长为 2" onPress={() => setStep(2)} />
<Button title="设置步长为 5" onPress={() => setStep(5)} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
text: { fontSize: 20, marginBottom: 10 },
buttonRow: { flexDirection: 'row', marginVertical: 10 },
});
export default CounterScreen;
好用的UI库 react-native-paper
oss.callstack.com/react-nativ…
"react-native-paper": "^5.15.0",
"@react-native-vector-icons/material-design-icons": "^12.4.1",
推荐项目目录结构
my-rn-app/
├── src/
│ ├── assets/ # 静态资源 (图片、字体等)
│ │ ├── images/
│ │ └── fonts/
│ │
│ ├── components/ # **完全可复用的纯 UI 组件**
│ │ ├── Button/
│ │ │ ├── Button.js
│ │ │ └── Button.styles.js
│ │ ├── LoadingSpinner/
│ │ └── index.js # 统一导出,方便引用
│ │
│ ├── config/ # 配置文件 (环境变量、常量)
│ │ ├── constants.js # API 地址、事件名等
│ │ └── env.js # 环境判断
│ │
│ ├── hooks/ # **共享的自定义 Hooks**
│ │ ├── useDebounce.js
│ │ ├── useAppState.js
│ │ └── index.js
│ │
│ ├── navigation/ # **导航器配置 (你提到的 router)**
│ │ ├── AppNavigator.js # 根导航器 (Stack)
│ │ ├── HomeStack.js # 首页内部导航
│ │ ├── BottomTabs.js # 底部 Tab 配置
│ │ └── types.js # 导航参数类型定义 (TS)
│ │
│ ├── screens/ # **应用的主要页面 (按 Feature 划分)**
│ │ ├── Home/ # 首页模块
│ │ │ ├── HomeScreen.js # 首页主页面
│ │ │ ├── components/ # **仅首页使用的组件**
│ │ │ │ └── BannerCarousel.js
│ │ │ └── useHomeData.js # **仅首页使用的 Hook**
│ │ ├── Order/ # 订单模块
│ │ │ ├── OrderScreen.js
│ │ │ ├── OrderDetailScreen.js
│ │ │ ├── components/
│ │ │ │ └── OrderCard.js
│ │ │ └── useOrderList.js
│ │ ├── Profile/ # 我的模块
│ │ └── WebView/ # WebView 模块
│ │ ├── WebViewScreen.js
│ │ └── useWebViewBridge.js
│ │
│ ├── services/ # **API 请求层 (与后端交互)**
│ │ ├── apiClient.js # axios 实例配置
│ │ ├── authApi.js # 登录相关 API
│ │ ├── orderApi.js # 订单相关 API
│ │ └── userApi.js
│ │
│ ├── stores/ # **全局状态管理 (Zustand)**
│ │ ├── useUserStore.js # 用户信息、Token
│ │ ├── useOrderStore.js # 全局订单状态
│ │ ├── useSettingsStore.js # 主题、语言等
│ │ └── index.js # 统一导出 stores
│ │
│ ├── utils/ # **纯工具函数**
│ │ ├── storage.js # 封装 AsyncStorage/MMKV
│ │ ├── permission.js # 权限请求工具
│ │ ├── format.js # 日期、金额格式化
│ │ └── index.js
├── index.js # 入口文件
├── App.js # 根容器组件
├── android/ # 原生 Android 代码
├── ios/ # 原生 iOS 代码
├── .env # 环境变量
├── .eslintrc.js
├── .prettierrc
├── babel.config.js
├── metro.config.js
└── package.json