React Native应用
本文最后更新于 764 天前,其中的信息可能已经有所发展或是发生改变。

设计一个React Native应用需要考虑很多因素,包括应用的功能、用户界面、数据管理、状态管理、路由管理、测试等。以下是一个简单的React Native应用设计方案,以及一些推荐的技术和库:

  1. 应用功能:假设我们要开发一个简单的任务管理应用,用户可以添加任务,标记任务完成,查看任务列表。
  2. 用户界面:我们可以使用React Native的内置组件来创建用户界面,如View、Text、Button等。如果需要更复杂的组件,可以使用第三方库,如React Native Elements或NativeBase。
  3. 数据管理:我们可以使用React的useState或useReducer Hook来管理应用的状态。如果应用的状态更复杂,可以使用Redux或MobX。
  4. 路由管理:我们可以使用React Navigation来管理应用的路由。React Navigation提供了一种在React Native应用中创建导航结构的方式。
  5. 测试:我们可以使用Jest来进行单元测试,使用Detox进行端到端测试。
  6. 集成框架:我们可以使用Expo作为开发和构建工具。Expo提供了一种无需配置就可以创建React Native应用的方式,同时还提供了许多方便的特性,如实时重载、错误报告、调试工具等。

项目开始

框架搭建采用原生React Native,成本低性能也不会比Flutter差很多!

  1. Expo:Expo是一个开源的React Native项目,它提供了一些工具和服务,可以帮助你更容易地使用React Native构建和部署应用。Expo提供了一些预定义的模板,可以帮助你快速开始新的项目。
  2. Ignite CLI:Ignite CLI是一个React Native应用生成器,它提供了一些预定义的模板和工具,可以帮助你快速创建和开发React Native应用。
  3. React Native Elements:React Native Elements是一个React Native的UI工具包,它提供了一些预定义的组件,可以帮助你快速创建用户界面。
  4. NativeBase:NativeBase是一个React Native的UI工具包,它提供了一些预定义的组件,可以帮助你快速创建用户界面。
  5. React Native Paper:React Native Paper是一个React Native的UI工具包,它遵循Material Design规范,提供了一些预定义的组件,可以帮助你快速创建用户界面

逛了一圈发现可以使用Expo可以快速搭建

什么是Expo?

Expo是一组工具、库和服务,可以通过编写JavaScript来构建本地的iOS和android应用程序。说人话,就是在React Native的基础上再封装了一层,让我们的开发更方便,更快速。

  • 做过移动端的同学在做跨平台之前肯定会担心一个点,就是各种原生功能(相机,相册,定位,蓝牙等等),使用expo的话,会比你开发一个裸的React Native真的会快很多,而且会少踩很多坑
  • 没有做过移动端的前端那就更需要这个了,不然移动端的一些隐藏的限制和坑,会让你很头疼

项目搭建

安装Expo

  1. 首先,你需要在你的机器上安装Node.js和npm。你可以从Node.js官网下载并安装Node.js,npm会随着Node.js一起安装。
  2. 然后,你可以使用npm全局安装Expo CLI:

 npm install -g expo-cli
  1. 创建一个新的React Native项目:

 expo init MyProject

在这个步骤中,Expo CLI会让你选择一个模板。你可以选择”blank”模板来创建一个空的项目,或者选择其他的模板来创建一个包含一些预定义功能的项目。

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

 

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 模拟器:

  1. 首先,你需要在你的电脑上安装 Android Studio 和 Android SDK。你可以从 Android Studio 的官方网站下载并安装它。
  2. 在 Android Studio 中,你可以创建并启动一个 Android 虚拟设备(AVD)。
  3. 在你的 Expo 项目的目录中打开一个命令行窗口,然后运行 expo start 命令来启动 Expo 开发服务器。
  4. 在 Expo 开发服务器的网页界面中,点击 “Run on Android device/emulator”。

iOS 模拟器(只适用于 Mac):

  1. 首先,你需要在你的 Mac 上安装 Xcode。你可以从 Mac App Store 下载并安装它。
  2. 在 Xcode 中,你可以从 “Xcode > Open Developer Tool > Simulator” 菜单中启动 iOS 模拟器。
  3. 在你的 Expo 项目的目录中打开一个命令行窗口,然后运行 expo start 命令来启动 Expo 开发服务器。
  4. 在 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\emulatorE:\Android\SDK\toolsE:\Android\SDK\tools\binE:\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页面下载。你可以按照以下步骤来下载你的应用:

  1. 打开你的浏览器,访问Expo的网站。
  2. 点击右上角的”Sign in”按钮,使用你的账户登录。
  3. 在登录后的页面,点击左侧菜单的”Projects”选项。
  4. 在项目列表中,找到你刚刚构建的项目,点击进入。
  5. 在项目的页面中,点击顶部的”Builds”选项。
  6. 在”Builds”页面中,你可以看到你的所有构建历史。找到你刚刚构建的Android应用,点击右侧的”Download”按钮。
  7. 浏览器会开始下载你的应用。下载完成后,你可以将这个APK文件安装到你的Android设备上。
  8. 由于下载下来的还是aad文件,不能直接安装
    1. 打开Android Studio:启动Android Studio,然后在欢迎界面选择”Profile or debug APK”。
    2. 选择你的.aab文件:在弹出的文件选择器中,找到并选择你的.aab文件,然后点击”OK”。
    3. 生成APK文件:在Android Studio的菜单中,选择”Build > Build Bundle(s) / APK(s) > Build APK(s)”。Android Studio会开始生成APK文件。
    4. 找到并安装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

  1. 生成密钥库文件:你可以使用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打包

这个有点麻烦了,需要申请开发者还要付费一下

需要团队支持才能上苹果商店

在这里申请

上一篇
下一篇