设计一个React Native应用需要考虑很多因素,包括应用的功能、用户界面、数据管理、状态管理、路由管理、测试等。以下是一个简单的React Native应用设计方案,以及一些推荐的技术和库:
- 应用功能:假设我们要开发一个简单的任务管理应用,用户可以添加任务,标记任务完成,查看任务列表。
- 用户界面:我们可以使用React Native的内置组件来创建用户界面,如View、Text、Button等。如果需要更复杂的组件,可以使用第三方库,如React Native Elements或NativeBase。
- 数据管理:我们可以使用React的useState或useReducer Hook来管理应用的状态。如果应用的状态更复杂,可以使用Redux或MobX。
- 路由管理:我们可以使用React Navigation来管理应用的路由。React Navigation提供了一种在React Native应用中创建导航结构的方式。
- 测试:我们可以使用Jest来进行单元测试,使用Detox进行端到端测试。
- 集成框架:我们可以使用Expo作为开发和构建工具。Expo提供了一种无需配置就可以创建React Native应用的方式,同时还提供了许多方便的特性,如实时重载、错误报告、调试工具等。
项目开始
框架搭建采用原生React Native,成本低性能也不会比Flutter差很多!
- Expo:Expo是一个开源的React Native项目,它提供了一些工具和服务,可以帮助你更容易地使用React Native构建和部署应用。Expo提供了一些预定义的模板,可以帮助你快速开始新的项目。
- Ignite CLI:Ignite CLI是一个React Native应用生成器,它提供了一些预定义的模板和工具,可以帮助你快速创建和开发React Native应用。
- React Native Elements:React Native Elements是一个React Native的UI工具包,它提供了一些预定义的组件,可以帮助你快速创建用户界面。
- NativeBase:NativeBase是一个React Native的UI工具包,它提供了一些预定义的组件,可以帮助你快速创建用户界面。
- React Native Paper:React Native Paper是一个React Native的UI工具包,它遵循Material Design规范,提供了一些预定义的组件,可以帮助你快速创建用户界面
逛了一圈发现可以使用Expo可以快速搭建
什么是Expo?
Expo是一组工具、库和服务,可以通过编写JavaScript来构建本地的iOS和android应用程序。说人话,就是在React Native的基础上再封装了一层,让我们的开发更方便,更快速。
- 做过移动端的同学在做跨平台之前肯定会担心一个点,就是各种原生功能(相机,相册,定位,蓝牙等等),使用expo的话,会比你开发一个裸的React Native真的会快很多,而且会少踩很多坑
- 没有做过移动端的前端那就更需要这个了,不然移动端的一些隐藏的限制和坑,会让你很头疼
项目搭建
安装Expo
- 首先,你需要在你的机器上安装Node.js和npm。你可以从Node.js官网下载并安装Node.js,npm会随着Node.js一起安装。
- 然后,你可以使用npm全局安装Expo CLI:
npm install -g expo-cli
- 创建一个新的React Native项目:
expo init MyProject
在这个步骤中,Expo CLI会让你选择一个模板。你可以选择”blank”模板来创建一个空的项目,或者选择其他的模板来创建一个包含一些预定义功能的项目。

- blank:这是一个最小的应用,就像一个空白的画布。如果你想从零开始创建你的应用,或者你想完全控制你的应用的结构和代码,那么这可能是一个好的选择。
- blank (TypeScript):这和”blank”模板相同,但是它使用TypeScript进行配置。如果你喜欢使用TypeScript,那么这可能是一个好的选择。
- tabs (TypeScript):这个模板包含了几个示例屏幕和使用react-navigation和TypeScript的标签。如果你想创建一个包含多个屏幕和标签的应用,那么这可能是一个好的选择。
- minimal (Bare workflow):这是一个最小的应用,只包含了你开始需要的基本内容。这个模板使用了Bare workflow,这意味着你可以直接访问和修改原生代码。如果你需要使用一些Expo不支持的原生模块,或者你想完全控制你的应用的构建和部署过程,那么这可能是一个好的选择。
- 进入项目目录并启动项目:

cd MyProject
expo start
这个命令会启动一个开发服务器,并打开一个新的浏览器窗口,你可以在这个窗口中预览你的应用。
npx expo install –fix
- s:切换到开发构建模式
- a:在Android设备或模拟器上打开应用
- w:在Web浏览器中打开应用
- j:打开调试器
- r:重新加载应用
- m:切换菜单
- o:在你的编辑器中打开项目代码
- ?:显示所有命令
如果使用web开发需要安装依赖 npx expo install react-native-web@~0.19.6 react-dom@18.2.0 @expo/webpack-config@^19.0.0

Android 模拟器:
- 首先,你需要在你的电脑上安装 Android Studio 和 Android SDK。你可以从 Android Studio 的官方网站下载并安装它。
- 在 Android Studio 中,你可以创建并启动一个 Android 虚拟设备(AVD)。
- 在你的 Expo 项目的目录中打开一个命令行窗口,然后运行 expo start 命令来启动 Expo 开发服务器。
- 在 Expo 开发服务器的网页界面中,点击 “Run on Android device/emulator”。
iOS 模拟器(只适用于 Mac):
- 首先,你需要在你的 Mac 上安装 Xcode。你可以从 Mac App Store 下载并安装它。
- 在 Xcode 中,你可以从 “Xcode > Open Developer Tool > Simulator” 菜单中启动 iOS 模拟器。
- 在你的 Expo 项目的目录中打开一个命令行窗口,然后运行 expo start 命令来启动 Expo 开发服务器。
- 在 Expo 开发服务器的网页界面中,点击 “Run on iOS simulator”
在安卓手机上运行
启动后,连接wifi(需要与启动项目的电脑在同一个局域网下),并扫描控制台上输出的二维码来启动项目
在IPhone上运行
启动后,连接wifi(需要与启动项目的电脑在同一个局域网下),并打开系统自带的原生相机扫码,注意是自带的相机,提示前往expogo后点击二维码或按钮前往expo go启动项目

项目设计
这个项目主要是想做什么的,给谁用自己,他人还是什么!
1.自己使用朋友查看可以记录生活,记录自己学习点点滴滴
2.可以上传图片,相册这样
3.然后展示个人技术亮点项目
设计参考b站,小红书这样的
文件设计
1.设计文件目录
- /my-app
/src
components
– CustomButton.tsx
– CustomCard.tsx
– CustomHeader.tsx
screens
– HomeScreen.tsx
– PhotoUploadScreen.tsx
– TechHighlightScreen.tsx
navigation
– Router.tsx
assets
/images
/fonts
utils
– api.ts
– helpers.ts
– App.tsx
– package.json - components:存放所有的共享组件,如按钮、卡片等。
- screens:存放所有的屏幕组件,每个屏幕都是一个页面。
- navigation:存放导航相关的代码,如底部标签导航器。
- assets:存放静态资源,如图片和字体。
- utils:存放工具函数和服务,如API调用和帮助函数。
2.添加路由
Stack.Navigator是React Navigation库中的一个组件,它用于创建堆栈导航器。堆栈导航器允许你在不同的屏幕之间进行导航,每当一个新的屏幕被打开时,它就会被放到堆栈的顶部。
安装React Navigation库: cnpm install @react-navigation/native
安装所需的依赖: cnpm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
安装堆栈导航器:cnpm install @react-navigation/stack
在app.ts
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import Router from './src/navigation/Router';
export default function App() {
return (
<NavigationContainer>
<Router />
</NavigationContainer>
);
}
router
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';
const Stack = createStackNavigator();
export default function Router() {
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
);
}
HomeScreen
// src/screens/HomeScreen.tsx
import React from 'react';
import { Button, Text, View } from 'react-native';
export default function HomeScreen({ navigation }) {
return (
<View>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}
3.设置TabBar
安装tab
https://reactnavigation.org/docs/bottom-tab-navigator/
yarn add @react-navigation/bottom-tabs
AnotherScreen:
// src/screens/AnotherScreen.tsx
import React from ‘react’;
import { Text, View } from ‘react-native’;
export default function AnotherScreen() {
return (
Another Screen
);
}
router
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';
import AnotherScreen from '../screens/AnotherScreen';
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
function HomeStack() {
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerShown: false }} // 隐藏标题
/>
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
);
}
export default function Router() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Another" component={AnotherScreen} />
</Tab.Navigator>
);
}
更换TabBar图标
npm install react-native-vector-icons yarn add react-native-vector-icons
官方文档
https://oblador.github.io/react-native-vector-icons/
// src/navigation/Router.tsx
import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {Ionicons} from '@expo/vector-icons'; // 导入图标库
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';
import AnotherScreen from '../screens/AnotherScreen';
import LifeScreen from '../screens/LifeScreen';
import AlbumScreen from '../screens/AlbumScreen';
import HighlightScreen from '../screens/HighlightScreen';
import {View} from "react-native";
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
function HomeStack() {
return (
<Stack.Navigator initialRouteName="Home">
{/*隐藏标题*/}
<Stack.Screen name="Home" component={HomeScreen} options={{headerShown: false}}/>
<Stack.Screen name="Details" component={DetailsScreen}/>
</Stack.Navigator>
);
}
export default function Router() {
return (
<Tab.Navigator>
<Tab.Screen
name="首页"
component={HomeStack}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="home" color={color} size={size}/>
),
}}
/>
<Tab.Screen
name="动态"
component={LifeScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="md-pulse" color={color} size={size}/>
),
}}
/>
<Tab.Screen
name="Another"
component={AnotherScreen}
options={{
tabBarIcon: () => (
<Ionicons name="add-circle" color="#fb4a3e" size={40}/>
),
tabBarLabel: () => null, // 隐藏标签名字
}}
/>
<Tab.Screen
name="图库"
component={AlbumScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="images" color={color} size={size}/>
),
}}
/>
<Tab.Screen name="个人" component={HighlightScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="person" color={color} size={size}/>
),
}}
/>
</Tab.Navigator>
);
}
初步设计

页面设计
1. 设计上传页面
库expo install expo-image-picker
选择图片
// src/screens/AnotherScreen.tsx
import React, { useEffect, useState } from 'react';
import { Button, Image, View } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { useNavigation } from '@react-navigation/native';
export default function AnotherScreen() {
const [imageUri, setImageUri] = useState(null);
const navigation = useNavigation();
const takePhoto = async () => {
let result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
setImageUri(result.assets[0].uri);
navigation.navigate('PostScreen', { imageUri: result.assets[0].uri }); // replace 'NextScreen' with the name of your next screen
}
};
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
setImageUri(result.assets[0].uri);
navigation.navigate('PostScreen', { imageUri: result.assets[0].uri }); // replace 'NextScreen' with the name of your next screen
}
};
useEffect(() => {
pickImage();
}, []);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="选择图片" onPress={pickImage} />
<Button title="拍照" onPress={takePhoto} />
{imageUri && <Image source={{ uri: imageUri }} style={{ width: 200, height: 200 }} />}
</View>
);
}
2. 发布动态页面
配置请求api
使用axios
cnpm install axios
首先设计登录页面,没有登录的时候不能发布动态,先跳转到登录页面
清除依赖重新下载
expo start –clear
使用asiox
请求拦截器
import axios from 'axios';
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
// 创建 axios 实例
const service = axios.create({
baseURL: 'http://localhost:8080',
timeout: 10000 // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
async config => {
// 在这里可以做一些请求前的操作
/*
* 设置全局token
* */
// console.log(config,"请求拦截器config");
let token = await AsyncStorage.getItem('token');
console.log(token,"token")
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
console.log(service.defaults.baseURL + config.url, "config");
return config;
},
error => {
// 处理请求错误
console.log(error,"请求拦截器"); // for debug
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
async response => {
// 在这里可以做一些响应数据的操作
console.log(response,"响应拦截器response")
if (response.data && response.data.code === 40101) {
// 如果返回的状态码为40101,说明token过期,清除token并跳转到登录页面
await AsyncStorage.removeItem('token');
}
// 如果请求的 URL 包含 '/students/export/', 则返回整个响应对象
if (response.config.url?.includes('/students/export/')) {
return response;
}
return response.data;
},
error => {
// 处理响应错误
console.log(error,"响应拦截器"); // for debug
// console.log(error.message); // 打印错误信息
// console.log(error.request); // 打印请求对象
return Promise.reject(error);
}
);
export default service;
登录接口
import service from './api';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const userLogin = async (userAccount: string, userPassword: string) => {
let data = {
userAccount: userAccount,
userPassword: userPassword
};
try {
let response = await service.post('/api/user/login', data);
if (response.data.code === 200) {
// 请求成功
console.log('Login success!');
let token = response.data.data.token; // 获取token
let refreshToken = response.data.data.refreshToken; // 获取refreshToken
await AsyncStorage.setItem('token', token);
await AsyncStorage.setItem('refreshToken', refreshToken);
} else {
// 请求失败
console.log('Login failed!');
}
} catch (error) {
console.error(error);
}
};
export const sendSms = async (code: string, message: string, phoneNumber: string) => {
let data = {
code: code,
message: message,
phoneNumber: phoneNumber
};
try {
let response = await service.post('/api/user/sendSms', data);
if (response.data.code === 200) {
// 请求成功
console.log('SMS sent successfully!');
} else {
// 请求失败
console.log('SMS sending failed!');
}
} catch (error) {
console.error(error);
}
};
export const phoneLogin = async (code: string, message: string, phoneNumber: string) => {
let data = {
code: code,
message: message,
phoneNumber: phoneNumber
};
try {
let response = await service.post('/api/user/phoneLogin', data);
if (response.data.code === 200) {
// 请求成功
console.log('Phone login success!');
let token = response.data.data.token; // 获取token
let refreshToken = response.data.data.refreshToken; // 获取refreshToken
await AsyncStorage.setItem('token', token);
await AsyncStorage.setItem('refreshToken', refreshToken);
} else {
// 请求失败
console.log('Phone login failed!');
}
} catch (error) {
console.error(error);
}
};
export const uploadPhoto = async (imageUri: string) => {
let data = new FormData();
data.append('photo', {
uri: imageUri,
type: 'image/jpeg',
name: 'photo.jpg'
});
try {
let response = await service.post('/api/photoAlbum/upload', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (response.data.code === 200) {
// 请求成功
console.log('Upload success!');
} else {
// 请求失败
console.log('Upload failed!');
}
} catch (error) {
console.error(error);
}
};
export const savePhotoToAlbum = async (photoId: string, albumId: string) => {
let data = {
photoId: photoId,
albumId: albumId
};
try {
let response = await service.post('/api/photoAlbum/save', data);
if (response.data.code === 200) {
// 请求成功
console.log('Save success!');
} else {
// 请求失败
console.log('Save failed!');
}
} catch (error) {
console.error(error);
}
};
登录
import React, {useState} from 'react';
import {Button, Text, TextInput, View, StyleSheet,TouchableOpacity} from 'react-native';
import {phoneLogin, sendSms, userLogin} from '../utils/login';
import { useNavigation } from '@react-navigation/native';
export default function LoginScreen() {
const [userAccount, setUserAccount] = useState('');
const [userPassword, setUserPassword] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [code, setCode] = useState('');
const [error, setError] = useState<string | null>(null);
const [loginMethod, setLoginMethod] = useState('username');
const navigation = useNavigation();
const [countdown, setCountdown] = useState(0);
React.useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => setLoginMethod(loginMethod === 'username' ? 'phone' : 'username')}>
<Text style={{ marginRight: 10, color: '#000', fontSize: 16 }}>
{loginMethod === 'username' ? '手机号登录' : '用户名登录'}
</Text>
</TouchableOpacity>
),
});
}, [navigation, loginMethod]);
const handleLogin = async () => {
if (!userAccount || !userPassword) {
setError('用户名和密码不能为空');
return;
}
try {
await userLogin(userAccount, userPassword);
} catch (err) {
setError((err as Error).message);
}
};
const handleSendSms = async () => {
if (!phoneNumber) {
setError('手机号不能为空');
return;
}
try {
await sendSms(code, '登录短信', phoneNumber);
} catch (err) {
setError((err as Error).message);
}
React.useEffect(() => {
if (countdown > 0) {
const timerId = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
return () => clearTimeout(timerId); // 清除定时器
}
}, [countdown]);
};
const handlePhoneLogin = async () => {
if (!phoneNumber || !code) {
setError('手机号和验证码不能为空');
return;
}
try {
await phoneLogin(code, '', phoneNumber);
} catch (err) {
setError((err as Error).message);
}
};
return (
<View style={styles.container}>
{/* <Button title={loginMethod === 'username' ? '手机号登录' : '用户名登录'} onPress={() => setLoginMethod(loginMethod === 'username' ? 'phone' : 'username')} /> */}
{loginMethod === 'username' ? (
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="用户名"
value={userAccount}
onChangeText={(text) => setUserAccount(text)}
/>
<TextInput
style={styles.input}
placeholder="密码"
value={userPassword}
onChangeText={(text) => setUserPassword(text)}
secureTextEntry={true}
/>
<View style={styles.button}>
<Button title="登录" onPress={handleLogin} color="#FFFFFF" />
</View>
</View>
) : (
<View style={styles.inputContainer}>
<View style={styles.inputWithButton}>
<TextInput
style={styles.input}
placeholder="手机号"
value={phoneNumber}
onChangeText={(text) => setPhoneNumber(text)}
maxLength={11}
/>
<TouchableOpacity
style={styles.codeButton}
onPress={handleSendSms}
disabled={countdown > 0} // 当倒计时时,禁用按钮
>
<Text style={styles.codeButtonText}>
{countdown > 0 ? `${countdown}秒后重发` : '发送验证码'}
</Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.input}
placeholder="验证码"
value={code}
onChangeText={(text) => setCode(text)}
/>
<View style={styles.button}>
<Button title="登录" onPress={handlePhoneLogin} color="#FFFFFF" />
</View>
</View>
)}
{error && <Text style={{color: 'red'}}>{error}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
backgroundColor: '#F4F5F7',
},
inputContainer: {
marginBottom: 20,
},
input: {
height: 50,
borderColor: '#FB7299',
borderWidth: 1,
marginBottom: 10,
paddingLeft: 10,
borderRadius: 10,
backgroundColor: '#FFFFFF',
},
button: {
marginTop: 10,
backgroundColor: '#FB7299',
justifyContent: 'center',
alignItems: 'center',
padding: 10,
borderRadius: 10,
},
row: {
flexDirection: 'row',
},
flex: {
flex: 1,
},
flex2: {
flex: 2,
// width:30
},
inputWithButton: {
position: 'relative',
},
codeButton: {
position: 'absolute',
right: 10,
// top: '50%',
height:40,
transform: [{ translateY: 5 }],
backgroundColor: '#FB7299',
padding: 10,
borderRadius: 10,
},
codeContainer: {
flexDirection: 'row',
marginBottom: 10,
},
codeButtonText: {
color: '#FFFFFF',
},
});
路由
// src/navigation/Router.tsx
import React, {useEffect} from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {Ionicons} from '@expo/vector-icons'; // 导入图标库
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';
import PhotoPickerStack from '../screens/PhotoPickerStack';
import LifeScreen from '../screens/LifeScreen';
import AlbumScreen from '../screens/AlbumScreen';
import HighlightScreen from '../screens/HighlightScreen';
import PostScreen from "../screens/PostScreen";
import {HeaderBackButton} from '@react-navigation/elements';
import {StackActions, useNavigation} from '@react-navigation/native';
import LoginScreen from '../screens/LoginScreen';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {Text} from 'react-native';
type StackParamList = {
Home: undefined;
Details: undefined;
PhotoPickerStack: undefined;
PostScreen: { imageUri: string };
Login: undefined;
MainTabs: undefined;
};
const Stack = createStackNavigator<StackParamList>();
const Tab = createBottomTabNavigator();
function HomeStack() {
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerShown: false,title: '首页' }}
/>
</Stack.Navigator>
);
}
function PhotoPicker() {
const navigation = useNavigation();
useEffect(() => {
const unsubscribe = navigation.addListener('focus', async () => {
const token = await AsyncStorage.getItem('token');
if (!token) {
navigation.dispatch(StackActions.push('Login'));
}
});
return unsubscribe;
}, [navigation]);
return (
<Stack.Navigator initialRouteName="PhotoPickerStack" >
<Stack.Screen
name="PhotoPickerStack"
component={PhotoPickerStack}
options={{ headerShown: false }}
/>
<Stack.Screen
name="PostScreen"
component={PostScreen}
options={({ navigation }) => ({
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
navigation.goBack();
}}
/>
),
})}
/>
</Stack.Navigator>
);
}
function OtherScreens() {
return (
<Stack.Navigator initialRouteName="Details">
<Stack.Screen
name="Details"
component={DetailsScreen}
options={{
title: 'Details',
headerShown: false
}}
/>
</Stack.Navigator>
);
}
function MainTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="首页"
component={HomeStack}
options={({route})=>
({
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" color={color} size={size} />
),
})
}
/>
<Tab.Screen
name="动态"
component={LifeScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Ionicons name="md-pulse" color={color} size={size} />
),
}}
/>
<Tab.Screen
name="发布"
component={PhotoPicker}
options={({ route }) => ({
tabBarIcon: () => (
<Ionicons name="add-circle" color="#fb4a3e" size={40} />
),
tabBarLabel: () => null,
// title: '',
})}
/>
<Tab.Screen
name="图库"
component={AlbumScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Ionicons name="images" color={color} size={size} />
),
}}
/>
<Tab.Screen name="个人" component={HighlightScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" color={color} size={size} />
),
}}
/>
</Tab.Navigator>
);
}
function Login() {
const navigation = useNavigation();
return (
<Stack.Navigator initialRouteName="Login" >
<Stack.Screen
name="Login"
component={LoginScreen}
options={{
title: '登录',
headerLeft: (props) => (
<HeaderBackButton
{...props}
backImage={() => <Ionicons name="close" size={24} color="black" />}
onPress={async () => {
// 在登录页面的返回按钮的 onPress 事件中,获取保存的路由信息,并跳转到该路由
const lastRouteName = await AsyncStorage.getItem('lastRouteName');
navigation.navigate("Home");
}}
/>
),
headerRight: () => (
<Text style={{ marginRight: 10, color: '#000', fontSize: 16 }}>登录</Text>
),
}}
/>
</Stack.Navigator>
);
}
export default function Router() {
return (
<Stack.Navigator>
<Stack.Screen
name="MainTabs"
component={MainTabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Details"
component={OtherScreens}
/>
<Stack.Screen
name="Login"
component={Login}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
}
发布页面
需要安装库来显示输入框
expo install react-native-keyboard-aware-scroll-view
完整代码
// src/screens/PostScreen.tsx
import React, {useState} from 'react';
import {Text, TextInput, Image, TouchableOpacity, StyleSheet, Alert} from 'react-native';
import {useRoute, RouteProp, useNavigation} from '@react-navigation/native';
import { uploadPhoto, savePhotoToAlbum } from '../utils/login';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
type StackParamList = {
PostScreen: { imageUri: string };
};
type PostScreenRouteProp = RouteProp<StackParamList, 'PostScreen'>;
export default function PostScreen() {
const route = useRoute<PostScreenRouteProp>();
const imageUri = route.params?.imageUri;
const navigation = useNavigation();
const [name, setName] = useState('');
const [title, setTitle] = useState('');
const [category, setCategory] = useState('');
const handleSubmit = async () => {
try {
if (!imageUri){
return
}
// 上传图片
const uploadResponse = await uploadPhoto(imageUri);
if (uploadResponse && uploadResponse.code === 0) {
console.log('Upload success!');
// 保存到相册
const saveResponse = await savePhotoToAlbum(category, name, title, uploadResponse.data);
if (saveResponse && saveResponse.code === 0) {
console.log('Save success!');
navigation.navigate('MainTabs', { screen: '图库' });
} else {
Alert.alert('发布失败', saveResponse ? saveResponse.description : '保存失败');
}
} else {
Alert.alert('发布失败', uploadResponse ? uploadResponse.description : '上传失败');
}
} catch (error) {
console.error(error);
}
};
return (
<KeyboardAwareScrollView
style={styles.container}
resetScrollToCoords={{ x: 0, y: 0 }}
contentContainerStyle={styles.container}
scrollEnabled={true}
keyboardShouldPersistTaps='always'
keyboardDismissMode='none'
>
<Image source={{uri: imageUri}} style={styles.image}/>
<TextInput
style={styles.input}
placeholder="请输入标题"
value={name}
onChangeText={setName}
returnKeyType="done"
/>
<TextInput
style={styles.input}
placeholder="请输入描述"
value={title}
onChangeText={setTitle}
returnKeyType="done"
/>
<TextInput
style={styles.input}
placeholder="请选择分类"
value={category}
onChangeText={setCategory}
returnKeyType="done"
/>
<TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.buttonText}>发布</Text>
</TouchableOpacity>
</KeyboardAwareScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#F5F5F5',
},
image: {
width: '100%',
height: 300,
borderRadius: 10,
marginBottom: 20,
},
input: {
height: 60,
borderColor: '#ddd',
borderWidth: 1,
marginTop: 20,
paddingHorizontal: 10,
fontSize: 20,
borderRadius: 10,
backgroundColor: '#FFFFFF',
},
button: {
backgroundColor: '#4CAF50',
padding: 15,
margin: 20,
borderRadius: 10,
},
buttonText: {
color: 'white',
textAlign: 'center',
fontSize: 20,
},
});
上传接口需要
api
base64expo install expo-file-system
import * as FileSystem from 'expo-file-system';
export const uploadPhoto = async (imageUri: string) => {
try {
const fileContent = await FileSystem.readAsStringAsync(imageUri, { encoding: FileSystem.EncodingType.Base64 });
let data = new URLSearchParams({
file: fileContent,
mimeType: 'image/jpeg', // 或者 'image/png',取决于你的文件类型
});
let token = await AsyncStorage.getItem('token');
let headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
let response = await fetch('https://muxuetianyin.cn/api/photoAlbum/uploadBase64', {
method: 'POST',
headers: headers,
body: data.toString(),
});
if (response.ok) {
console.log('Upload success!');
let responseJson = await response.json();
console.log(responseJson)
return responseJson;
} else {
console.log('Upload failed!');
}
} catch (error) {
console.error('Error', error);
}
};
export const savePhotoToAlbum = async (category: string, name: string, title: string, url: string) => {
let data = {
category: category,
name: name,
title: title,
url: url
};
try {
let response:result<any> = await service.post('/api/photoAlbum/save', data);
if (response.code === 0) {
// 请求成功
console.log('Save success!');
return response
} else {
// 请求失败
console.log('Save failed!');
}
} catch (error) {
console.error(error);
}
};
后端
@ApiOperation("上传文件(Base64)")
@PostMapping("/uploadBase64")
public Result handleFileUploadBase64(@RequestParam("file") String base64File, @RequestParam("mimeType") String mimeType) {
byte[] decodedBytes;
try {
// 解码Base64文件
decodedBytes = Base64.getDecoder().decode(base64File);
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "Invalid Base64 input");
}
// 检查文件大小,这里限制为5MB
if (decodedBytes.length > 5 * 1024 * 1024) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "File size should not exceed 5MB");
}
String savePath = System.getProperty("user.dir") + "/uploads/images";
// 确保这个目录是真实存在的
File directory = new File(savePath);
if (!directory.exists()) {
directory.mkdirs(); // 创建目录,包括任何必要但不存在的父目录
}
// 根据MIME类型确定文件扩展名
String extension = mimeType.split("/")[1];
String filename = System.currentTimeMillis() + "." + extension;
File dest = new File(directory, filename);
try {
// 保存文件
Files.write(dest.toPath(), decodedBytes);
// 返回成功的url 本地设置的路径
String fileUrl = serverAddress + "uploads/images/" + filename;
return Result.success(fileUrl);
} catch (IOException e) {
e.printStackTrace();
}
throw new BusinessException(ErrorCode.PARAMS_ERROR, "Failed to upload file");
}
3.个人页面设计
安装ui库cnpm install react-native-elements yarn add react-native-elements
安装布组件yarn add react-native-super-grid
增加路由
function UserScreen() {
return (
<Stack.Navigator initialRouteName="UserScreen">
<Stack.Screen
name="UserScreen"
component={HighlightScreen}
options={{
// title: '个人',
headerShown: false,
}}
/>
<Stack.Screen
name="EditUser"
component={EditUserScreen}
/>
<Stack.Screen
name="ProjectScreen"
component={ProjectScreen}
/>
</Stack.Navigator>
);
}
优化路由导出,在screen里面新建index.ts导出所有页面
// src/screens/index.js
export { default as HomeScreen } from './HomeScreen';
export { default as DetailsScreen } from './DetailsScreen';
export { default as PhotoPickerStack } from './PhotoPickerStack';
export { default as LifeScreen } from './LifeScreen';
export { default as AlbumScreen } from './AlbumScreen';
export { default as HighlightScreen } from './HighlightScreen';
export { default as PostScreen } from './PostScreen';
export { default as LoginScreen } from './LoginScreen';
export { default as EditUserScreen } from './EditUserScreen';
export { default as ProjectScreen } from './ProjectScreen';
然后路由
import {
HomeScreen,
DetailsScreen,
PhotoPickerStack,
LifeScreen,
AlbumScreen,
HighlightScreen,
PostScreen,
LoginScreen,
EditUserScreen,
ProjectScreen,
} from ‘../screens’;
个人页面这里使用了react-native-elementsimport
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Button } from 'react-native';
import { Avatar, ListItem, Card, Text } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { getCurrentUser, updateUser } from '../utils/user';
export default function ProfileScreen() {
const [user, setUser] = useState(null);
const navigation = useNavigation();
useEffect(() => {
fetchUser();
}, []);
const fetchUser = async () => {
const userData = await getCurrentUser();
setUser(userData);
};
const handleEdit = () => {
navigation.navigate('EditUser', {
user: user,
onGoBack: () => fetchUser(),
});
};
const handleViewProjects = () => {
navigation.navigate('ProjectScreen');
};
if (!user) {
return <Card><Card.Title>Loading...</Card.Title></Card>;
}
return (
<View style={styles.container}>
<Card>
<Card.Title>{user.username}</Card.Title>
<Card.Divider/>
<View style={styles.avatarContainer}>
<Avatar
rounded
size="large"
source={{
uri: user.avatarUrl,
}}
/>
</View>
<Text style={styles.signature}>{user.signature}</Text>
<Card.Divider/>
<ListItem bottomDivider>
<ListItem.Content>
<ListItem.Title>Email</ListItem.Title>
<ListItem.Subtitle>{user.email}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
<ListItem bottomDivider>
<ListItem.Content>
<ListItem.Title>Phone</ListItem.Title>
<ListItem.Subtitle>{user.phone}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
<ListItem bottomDivider>
<ListItem.Content>
<ListItem.Title>Gender</ListItem.Title>
<ListItem.Subtitle>{user.gender}</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
<ListItem bottomDivider>
<Button title="View Projects" onPress={handleViewProjects} />
</ListItem>
<Button title="Edit" onPress={handleEdit} />
</Card>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
avatarContainer: {
alignItems: 'center',
},
signature: {
marginTop: 10,
marginBottom: 10,
textAlign: 'center',
},
});
编辑个人信页面
// src/screens/EditUserScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';
import { updateUser } from '../utils/user'; // 导入更新用户信息的函数
export default function EditUserScreen({ route, navigation }) {
const { user, onGoBack } = route.params;
const [username, setUsername] = useState(user.username);
const [email, setEmail] = useState(user.email);
const [phone, setPhone] = useState(user.phone);
const [signature, setSignature] = useState(user.signature);
const handleSave = async () => {
const updatedUser = {
...user,
username,
email,
phone,
signature,
};
await updateUser(updatedUser);
navigation.dangerouslyGetParent().options.onGoBack();
navigation.goBack();
};
return (
<View style={styles.container}>
<Text>Username</Text>
<TextInput
value={username}
onChangeText={setUsername}
style={styles.input}
/>
<Text>Email</Text>
<TextInput
value={email}
onChangeText={setEmail}
style={styles.input}
/>
<Text>Phone</Text>
<TextInput
value={phone}
onChangeText={setPhone}
style={styles.input}
/>
<Text>Signature</Text>
<TextInput
value={signature}
onChangeText={setSignature}
style={styles.input}
/>
<Button title="Save" onPress={handleSave} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
},
});
userApi
import service from './api';
export const getCurrentUser = async () => {
try {
let response:result<any> = await service.get('/api/user/current');
if (response.code === 0) {
// 请求成功
console.log('Get current user success!');
return response.data;
} else {
// 请求失败
console.log('Get current user failed!');
}
} catch (error) {
console.error(error);
}
};
export const updateUser = async (userUpdateRequest) => {
try {
let response:result<any> = await service.put('/api/user/update', userUpdateRequest);
if (response.code === 0) {
// 请求成功
console.log('Update user success!');
return response.data;
} else {
// 请求失败
console.log('Update user failed!');
}
} catch (error) {
console.error(error);
}
};
项目页面布写死
import React from 'react';
import { View, StyleSheet, Image, FlatList } from 'react-native';
import { Avatar, ListItem, Card, Text } from 'react-native-elements';
const projects = [
{
title: '项目一:muxue-user',
description: '这是一个全栈的用户管理系统,包括用户的增删改查、权限管理等功能。',
imageUrl: 'http://example.com/project1.png',
tags: ['React', 'Ant Design Pro', 'Spring Boot', 'MyBatis Plus', 'Docker'],
},
// 其他项目...
];
export default function ProjectScreen() {
return (
<View style={styles.container}>
<FlatList
data={projects}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<Card>
<Card.Title>{item.title}</Card.Title>
<Card.Divider/>
<Image
style={styles.image}
source={{
uri: item.imageUrl,
}}
/>
<Text>{item.description}</Text>
{item.tags.map((tag, index) => (
<Text key={index} style={styles.tag}>
{tag}
</Text>
))}
</Card>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
image: {
width: '100%',
height: 200,
},
tag: {
backgroundColor: '#eee',
padding: 5,
margin: 2,
},
});
4.相册页面
imagesApi
import * as FileSystem from "expo-file-system";
import AsyncStorage from "@react-native-async-storage/async-storage";
import service from "./api";
export const uploadPhoto = async (imageUri: string) => {
try {
const fileContent = await FileSystem.readAsStringAsync(imageUri, { encoding: FileSystem.EncodingType.Base64 });
let data = new URLSearchParams({
file: fileContent,
mimeType: 'image/jpeg', // 或者 'image/png',取决于你的文件类型
});
let token = await AsyncStorage.getItem('token');
let headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
let response = await fetch('https://muxuetianyin.cn/api/photoAlbum/uploadBase64', {
method: 'POST',
headers: headers,
body: data.toString(),
});
if (response.ok) {
console.log('Upload success!');
let responseJson = await response.json();
console.log(responseJson)
return responseJson;
} else {
console.log('Upload failed!');
}
} catch (error) {
console.error('Error', error);
}
};
export const savePhotoToAlbum = async (category: string, name: string, title: string, url: string) => {
let data = {
category: category,
name: name,
title: title,
url: url
};
try {
let response:result<any> = await service.post('/api/photoAlbum/save', data);
if (response.code === 0) {
// 请求成功
console.log('Save success!');
return response
} else {
// 请求失败
console.log('Save failed!');
}
} catch (error) {
console.error(error);
}
};
export const searchPhotoAlbum = async (category?: string, name?: string, page?: number, size?: number) => {
try {
let params = {};
if (category) params['category'] = category;
if (name) params['name'] = name;
if (page) params['page'] = page;
if (size) params['size'] = size;
console.log(params)
let response:RES<any> = await service.get('/api/photoAlbum/search', { params });
if (response.code === 0) {
console.log('Fetch success!');
return response.data;
} else {
console.log('Fetch failed!');
}
} catch (error) {
console.error(error);
}
};
export const getPhotoCategories = async () => {
try {
let response:result<any> = await service.get('/api/photoAlbum/categories');
if (response.code === 0) {
// 请求成功
console.log('Fetch success!');
return response.data;
} else {
// 请求失败
console.log('Fetch failed!');
}
} catch (error) {
console.error(error);
}
};
设计参考之前的网站,react-native不好实现
代码
主要设计分类和搜索
对此进行监听
useEffect(() => {
fetchPhotos();
}, [selectedIndex]);
useEffect(() => {
if (search==''){
fetchPhotos();
}
}, [search]);
使用react-native-elements进行布局
代码
import React, { useState, useEffect } from 'react';
import { View, FlatList, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
import { Card, Text, Avatar, Button, Overlay, SearchBar, Icon } from 'react-native-elements';
import { getPhotoCategories, searchPhotoAlbum } from '../utils/images';
export default function AlbumScreen() {
const [categories, setCategories] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [photos, setPhotos] = useState([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
useEffect(() => {
fetchCategories();
fetchPhotos();
}, []);
useEffect(() => {
fetchPhotos();
}, [selectedIndex]);
useEffect(() => {
if (search==''){
fetchPhotos();
}
}, [search]);
const fetchCategories = async () => {
setLoading(true);
const data = await getPhotoCategories();
setCategories(['All', ...data]);
setLoading(false);
};
const fetchPhotos = async () => {
setLoading(true);
let category = categories[selectedIndex] === 'All' ? '' : categories[selectedIndex];
const data = await searchPhotoAlbum(category, search, page, 10);
setPhotos(data.list);
setTotal(data.total);
if (data){
setTimeout(()=>{
setLoading(false);
},200)
}
};
const handleCategoryChange = (selectedIndex) => {
setSelectedIndex(selectedIndex);
if (categories[selectedIndex] === 'All') {
setSearch('');
setPage(1)
return
}
// fetchPhotos();
};
const handlePageChange = (newPage) => {
if (newPage < 1 || newPage > Math.ceil(total / 10)) {
return;
}
setPage(newPage);
fetchPhotos();
};
const handleSearchChange = (text) => {
setSearch(text);
};
const handleSearchSubmit = () => {
fetchPhotos();
};
const handleSearchClear = () => {
setSearch('');
};
return (
<View style={styles.container}>
<SearchBar
placeholder="Search Photos..."
onChangeText={handleSearchChange}
onSubmitEditing={handleSearchSubmit}
onClear={handleSearchClear}
value={search}
containerStyle={styles.searchBar}
/>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{categories.map((category, index) => (
<Button
key={index}
title={category}
onPress={() => handleCategoryChange(index)}
containerStyle={styles.button}
titleStyle={styles.buttonTitle}
/>
))}
</ScrollView>
<FlatList
data={photos}
renderItem={({ item }) => (
<Card>
<Card.Title>{item.name}</Card.Title>
<Card.Divider/>
<Avatar source={{ uri: item.url }} size="large" />
<Text>{item.title}</Text>
<Text>{item.upload_time}</Text>
</Card>
)}
keyExtractor={(item, index) => index.toString()}
/>
<View style={styles.buttonContainer}>
<Button title="Previous" onPress={() => handlePageChange(page - 1)} />
<Text>Page {page} of {Math.ceil(total / 10)}</Text>
<Button title="Next" onPress={() => handlePageChange(page + 1)} />
</View>
<Overlay isVisible={loading}>
<ActivityIndicator size="large" color="#0000ff" />
</Overlay>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
searchBar: {
marginBottom: 10,
},
button: {
marginRight: 10,
height: 40,
},
buttonTitle: {
fontSize: 14,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 10,
},
});
路由更改,设计全部放到栈里面去
默认导出
export default function Router() {
return (
<Stack.Navigator>
<Stack.Screen
name="MainTabs"
component={MainTabs}
options={{headerShown: false}}
/>
<Stack.Screen
name="Details"
component={OtherScreens}
options={{
headerLeft: (props) => (
<HeaderBackButton
{...props}
labelVisible={false}
/>
),
}}
/>
<Stack.Screen
name="Login"
component={Login}
options={{headerShown: false}}
/>
</Stack.Navigator>
);
}
MainTabs是tab导航路由,他这个会有一个标题,找了好久还是不能因此只能隐藏Stack.Screen的,然后导航里面可以配置方法设计多个Stack.Screen屏幕,然后下面两个Stack.Screen就可以摆脱导航tab
function MainTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="首页"
component={HomeStack}
options={({route}) =>
({
tabBarIcon: ({color, size}) => (
<Ionicons name="home" color={color} size={size}/>
),
})
}
/>
<Tab.Screen
name="动态"
component={LifeScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="md-pulse" color={color} size={size}/>
),
}}
/>
<Tab.Screen
name="发布"
component={PhotoPicker}
options={({route}) => ({
tabBarIcon: () => (
<Ionicons name="add-circle" color="#fb4a3e" size={40}/>
),
tabBarLabel: () => null,
// title: '',
})}
/>
<Tab.Screen
name="图库"
component={AlbumScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="images" color={color} size={size}/>
),
}}
/>
<Tab.Screen name="个人" component={UserScreen}
options={{
tabBarIcon: ({color, size}) => (
<Ionicons name="person" color={color} size={size}/>
),
}}
/>
</Tab.Navigator>
);
}
打包测试
安卓1. 安装Android SDK:如果你还没有安装Android SDK,你可以通过安装Android Studio来进行安装。在安装过程中,Android SDK会自动安装。 2. 设置ANDROID_HOME环境变量:安装完Android SDK后,你需要设置ANDROID_HOME环境变量,使其指向你的Android SDK的位置。在Windows上的默认位置是C:\Users\<你的用户名>\AppData\Local\Android\Sdk。 以下是在Windows上设置ANDROID_HOME环境变量的方法: – 在Windows搜索栏中搜索’环境变量’,然后选择’编辑系统环境变量’。 – 在出现的系统属性窗口中,点击’环境变量’。 – 在环境变量窗口中,点击’新建’,在’用户变量’部分。 – 在新用户变量窗口中,输入ANDROID_HOME作为变量名,输入你的Android SDK的路径作为变量值(例如,C:\Users\<你的用户名>\AppData\Local\Android\Sdk)。 – 在所有窗口中点击’确定’以应用更改。 3. 将Android SDK工具添加到Path:你还需要将Android SDK工具添加到你的Path变量: – 在同一个环境变量窗口中,滚动到’系统变量’部分,选择’Path’变量,然后点击’编辑’。 – 在编辑环境变量窗口中,点击’新建’,并添加以下路径(将<你的SDK路径>替换为你的Android SDK的实际路径): <你的SDK路径>\platform-tools <你的SDK路径>\tools <你的SDK路径>\tools\bin
Android Studio 安装
双击运行安装。






先不管

因为是谷歌设置代理

代理腾讯: https://mirrors.cloud.tencent.com/AndroidSDK/
阿里: https://mirrors.aliyun.com/android.googlesource.com/
Android SDK在线更新镜像服务器
1.中国科学院开源协会镜像站地址:
◦IPV4/IPV6: mirrors.opencas.cn 端口:80
◦IPV4/IPV6: mirrors.opencas.org 端口:80
◦IPV4/IPV6: mirrors.opencas.ac.cn 端口:80
2.上海GDG镜像服务器地址:
sdk.gdgshanghai.com 端口:8000
3.北京化工大学镜像服务器地址:
◦IPv4: ubuntu.buct.edu.cn/ 端口:80
◦IPv4: ubuntu.buct.cn/ 端口:80
◦IPv6: ubuntu.buct6.edu.cn/ 端口:80
4.大连东软信息学院镜像服务器地址:
mirrors.neusoft.edu.cn 端口:80
5.腾讯Bugly 镜像:
android-mirror.bugly.qq.com 端口:8080
配置完成之后点击完成



点击确定,然后点击系统变量下的path变量,添加这两条内容E:\Android\SDK\emulator,E:\Android\SDK\tools,E:\Android\SDK\tools\bin,E:\Android\SDK\platform-tools
E:\Android\SDK\platform-tools
E:\Android\SDK\tools
E:\Android\SDK\emulator
E:\Android\SDK\tools\bin


重新运行·expo start –android
连接手机调试
打包npm install -g eas-cli
配置账号密码eas build:configure
打包安卓eas build -p android
如果网络错误可能需要vpn,后面还是打包不成功,没办法这个坑没法解决,只能项目迁移了,Expo迁移到原生react-native
打包成功 eas build –platform all Loaded “env” configuration for the “production” profile: no environment variables specified. Learn more: https://docs.expo.dev/build-reference/variables/ ��� Android build √ Using remote Android credentials (Expo server) √ Using Keystore from configuration: Build Credentials WZTpmXjDyE (default) Compressing project files and uploading to EAS Build. Learn more: https://expo.fyi/eas-build-archive √ Uploaded to EAS 6s
打包成功后的安卓应用(APK文件)可以在Expo的EAS Build页面下载。你可以按照以下步骤来下载你的应用:
- 打开你的浏览器,访问Expo的网站。
- 点击右上角的”Sign in”按钮,使用你的账户登录。
- 在登录后的页面,点击左侧菜单的”Projects”选项。
- 在项目列表中,找到你刚刚构建的项目,点击进入。
- 在项目的页面中,点击顶部的”Builds”选项。
- 在”Builds”页面中,你可以看到你的所有构建历史。找到你刚刚构建的Android应用,点击右侧的”Download”按钮。
- 浏览器会开始下载你的应用。下载完成后,你可以将这个APK文件安装到你的Android设备上。
- 由于下载下来的还是aad文件,不能直接安装
- 打开Android Studio:启动Android Studio,然后在欢迎界面选择”Profile or debug APK”。
- 选择你的.aab文件:在弹出的文件选择器中,找到并选择你的.aab文件,然后点击”OK”。
- 生成APK文件:在Android Studio的菜单中,选择”Build > Build Bundle(s) / APK(s) > Build APK(s)”。Android Studio会开始生成APK文件。
- 找到并安装APK文件:生成完成后,Android Studio会显示一个通知,告诉你APK文件的位置。你可以点击这个通知来打开文件位置,然后将APK文件安装到你的设备上。
下载bundletool来进行安装https://github.com/google/bundletool/releases
1.下载bundletool:你可以从bundletool的GitHub页面下载最新的bundletool的jar文件。
2.生成APKs文件:打开终端,运行以下命令来生成APKs文件。其中,path/to/bundletool.jar是你下载的bundletool的路径,path/to/your-app.aab是你的.aab文件的路径,path/to/output.apks是输出的APKs文件的路径。 java -jar path/to/bundletool.jar build-apks –bundle=path/to/your-app.aab –output=path/to/output.apks
这里我放一起了

java -jar bundletool-all-1.15.4.jar build-apks –bundle=application-bd6e0190-4a05-4178-a1c8-7fec07da40d3.aab –output=MuXueBeauseYou.apks
这里先不要运行这个命令
在这个目录cmd
- 生成密钥库文件:你可以使用keytool命令来生成一个新的密钥库文件。keytool是Java Development Kit (JDK)的一部分,你应该已经在你的电脑上安装了它。在你的终端中运行以下命令,其中my-release-key.jks是你的密钥库文件的名称,alias_name是你的别名,password是你的密码:
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias alias_name
我这里keytool -genkey -v -keystore muxuetianyin.jks -keyalg RSA -keysize 2048 -validity 10000 -alias muxue -storepass 123456 -keypass 123456 keytool -importkeystore -srckeystore muxuetianyin.jks -destkeystore muxuetianyin.p12 -deststoretype pkcs12 -srcstorepass 123456 -deststorepass 123456
和之前的对接起来完整的命令是java -jar bundletool-all-1.15.4.jar build-apks –bundle=application-bd6e0190-4a05-4178-a1c8-7fec07da40d3.aab –output=MuXueBeauseYou.apks –ks=muxuetianyin.jks –ks-key-alias=muxue –ks-pass=pass:123456 –key-pass=pass:123456

这里签名失败了PS E:\code\project\Because-Of-You> expo credentials:manager ┌───────────────────────────────────────────────────────────────────────────┐ │ │ │ The global expo-cli package has been deprecated. │ │ │ │ The new Expo CLI is now bundled in your project in the expo package. │ │ Learn more: https://blog.expo.dev/the-new-expo-cli-f4250d8e3421. │ │ │ │ To use the local CLI instead (recommended in SDK 46 and higher), run: │ │ › npx expo <command> │ │ │ └───────────────────────────────────────────────────────────────────────────┘ Accessing credentials for muxue in project Because-Of-You √ Select platform » android √ You are currently in a directory with @muxue/Because-Of-You experience. Do you want to select it? … yes No credentials available for @muxue/Because-Of-You experience. √ What do you want to do? » Update upload Keystore √ Would you like to upload a Keystore or have us generate one for you? If you don’t know what this means, let us generate it! 🙂 » Generate new keystore Keystore updated successfully √ Do you want to quit Credential Manager » Quit Credential Manager
重新尝试
这里生成了一个签名库,重新获取到java -jar bundletool-all-1.15.4.jar build-apks –bundle=muxue.aab –output=MuXueBeauseYou.apks –ks=E:\code\project\Because-Of-You\Because-Of-You.jks –ks-key-alias=QG11eHVlL0JlY2F1c2UtT2YtWW91 –ks-pass=pass:8c931db2aeea4336a2eb83c5e0610296 –key-pass=pass:fc307b4d8034469199c3645dabb2e065
安装java -jar bundletool-all-1.15.4.jar install-apks –apks=MuXueBeauseYou.apks
安卓日志打印adb logcat | grep com.example.myapp adb logcat *:S ReactNative:V ReactNativeJS:V
另一种方法(推荐)
直接打包apk下载,这是踩了一堆坑才知道的
项目中创建一个eas.json文件(如果还没有的话),然后添加一个新的构建配置。例如:
{ "build": { "release": { "distribution": "store", "android": { "gradleCommand": ":app:assembleRelease" } } } }
配置
{ "cli": { "version": ">= 5.1.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal" }, "production": {}, "release": { "distribution": "store", "android": { "gradleCommand": ":app:assembleRelease" } } }, "submit": { "production": {} } }
还要安装几个东西
npm config set sharp_binary_host “https://npmmirror.com/mirrors/sharp”
npm config set sharp_libvips_binary_host “https://npmmirror.com/mirrors/sharp-libvips”
npm install -g sharp-cli
yarn upgrade react-native-vector-icons@latest
ios打包
这个有点麻烦了,需要申请开发者还要付费一下
需要团队支持才能上苹果商店
在这里申请