Expo的React Native项目, 二开记录


前言

ai-closet 项目进行二开,添加后台,部署到阿里云。

环境:
Node: 18.20.4
JDK: 17.0.9

1. 拉取代码

git clone https://github.com/zebangeth/ai-closet.git

cd ai-closet

npm i

2. 运行(web和android)

2.1 web

npm run web

web运行

2.2 android

运行android模拟器 [ 'Genymotion'、'真机'、'Android Studio' ]

npm run android

android模拟器自动安装Expo Go,然后运行项目

android运行

2.3 通用

npm run start

启动后,web通过链接访问,android手机通过Expo Go输入链接访问

3. 调试(web和android)

3.1 web

通过F12控制台即可调试

3.2 android

1. 运行起程序

2. CMD命令行输入: adb shell input keyevent 82

3. 在弹窗中点击: Open JS Debugger

4. 如果点击后程序自动停止了,可能手机没开 "开发者调试"

android调试1

android调试2

4. 开发

4.1 图片选择

使用 expo-image-picker 插件

  • 从相册选择

    import * as ImagePicker from "expo-image-picker";
    
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      quality: 1,
      legacy: true
    });
  • 拍照

    import * as ImagePicker from "expo-image-picker";
    
    const result = await ImagePicker.launchCameraAsync({
        quality: 1
    });

4.2 图片压缩

使用 expo-image-manipulator 插件

npx expo install expo-image-manipulator
import * as ImageManipulator from 'expo-image-manipulator';

/**
 * 图片压缩,android端
 * @param imageUri 文件路径,例如file:///
 */
export const imageCompress = async (imageUri) => {
  try {
    // 1. 获取图片格式
    const format = getImageFormat(imageUri);
    const outputFormat = format === 'PNG'
      ? ImageManipulator.SaveFormat.PNG
      : ImageManipulator.SaveFormat.JPEG;

    const result = await ImageManipulator.manipulateAsync(
      imageUri,
      [{ resize: { width: 700 } }], // 最大宽度为750
      {
        compress: 0.6, // 压缩质量0.7
        format: outputFormat, // 输出格式
      }
    )

    return result.uri
  } catch (e) {
    console.error('压缩图片错误', e)
    return imageUri
  }
}

// 获取图片格式(通过 URI 后缀)
const getImageFormat = (uri) => {
  try {
    // 方案1:通过 URI 后缀判断(适用于本地文件)
    const extension = uri.split('.').pop()?.toLowerCase();
    if (extension === 'png') return 'PNG';
    return 'JPEG';
  } catch (error) {
    console.warn('无法识别图片格式,默认使用 JPEG:', error);
    return 'JPEG';
  }
};

4.3 图片上传

需要转File格式,然后和web上传一样请求就行

  • fileService.ts

    /**
     * 上传图片
     * @param file 在web是File格式,在安卓是字符串file://
     */
    export const uploadApi = async (file) => {
      if (typeof file === 'string' && file.startsWith('file:')) {
        file = await imageCompress(file)
        file = uriToFile(file)
      } else if (typeof file === 'string' && file.startsWith('data:image:')) {
        file = dataURLtoFile(file, `${Date.now()}.png`)
      }
      return http.upload(API.upload, file)
    }
  • fileConver.ts

    /**
     * uri转File
     * const fileUri = 'file:///data/user/0/com.example.app/cache/image.jpg';
     */
    export function uriToFile(uri) {
      try {
        const fileName = uri.substring(uri.lastIndexOf('/') + 1);
        return { uri, type: 'multipart/form-data', name: fileName }
      } catch (error) {
        console.error('Error converting URI to File:', error);
        throw error;
      }
    }
  • http.ts

    // 封装请求
    export const http = {
      // post请求
      post(uri, body) {
        const myHeaders = new Headers();
        myHeaders.append("Content-Type", "application/json");
    
        return fetch(
          uri, {
            headers: myHeaders,
            body: JSON.stringify(body),
            method: 'POST',
            mode:'cors'
          }
        ).then(
          response => response.json()
        ).then(
          res => res.data
        )
      },
    
      // 上传文件
      upload(uri, file) {
        const formData = new FormData();
        formData.append('file', file);
    
        const myHeaders = new Headers();
        myHeaders.append("Content-Type", 'multipart/form-data');
    
        return fetch(
          uri, {
            headers: myHeaders,
            body: formData,
            method: 'POST',
            mode:'cors'
          }
        ).then((response) => {
            console.log('211', 111)
            return response.json()
          }
        ).then((res) => {
            return res.data
          }
        ).catch((e) => {
          console.log('e', e)
        })
      }
    }

4.4 alert兼容web

  • Alert只支持android和ios,不支持web,所以需要重新封装。
import { Alert, Platform } from 'react-native'

const alertPolyfill = (
  title: string,
  description: string,
  options: Array<{
    text: string,
    onPress: () => void,
    style?: string
  }> = []) => {
  const result = window.confirm([title, description].filter(Boolean).join('\n'))

  if (result) {
    const confirmOption = options.find(({ style }) => style !== 'cancel')
    confirmOption && confirmOption.onPress()
  } else {
    const cancelOption = options.find(({ style }) => style === 'cancel')
    cancelOption && cancelOption.onPress()
  }
}

const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert

export default alert
  • 使用
    import alert from '../utils/Alert'
    
    alert("权限授权", "需要访问图库的权限!");

4.5 model内容不显示

在Model标签外套一层View, 新架构的bug,详见github-issues

<View>
    <Model></Model>
</View>

5. 打包 (在线打包Apk,本地打包Web/Apk)

  • package.json 添加命令
    "scripts": {
        "start": "expo start",
        "android": "expo start --android",
        "ios": "expo start --ios",
        "web": "expo start --web",
        + "build:install-eas-cli": "npm install -g eas-cli",
        + "build:login": "expo login",
        + "build:aab": "eas build --platform android",
        + "build:apk": "eas build -p android --profile preview",
        + "build:web": "expo export"
      },

5.1 在线打包Apk

1. 注册登录Expo: https://expo.dev/

2. cd ai-closet

3. npm install -g eas-cli

4. npm run build:login

5. npm run build:apk

6. 在 https://expo.dev/ 即可查看打包进度

在线打包1

在线打包2

5.2 本地打包web

1. cd ai-closet

2. npm install react-native-web

3. npm run build:web

4. 输出dist目录

5.3 本地打包Apk

这里可能需要一些环境变量,但因为本地之前配置过Android Studio,所以直接开始打包了,有问题可以看链接1链接2

1. 打开管理员权限终端: window+x -> window终端(管理员)

2. cd ai-closet

 // 转原生代码
3. npx expo prebuild --clean

4. cd android

// 改用本地文件,防止gradle拉取太慢
5. 修改 `android/gradle/wrapper/gradle-wrapper.properties`

distributionUrl=file:///F:/cordovaItem/gradle-8.10.2-all.zip 

// 开始打包
6. ./gradlew assembleRelease

5.3.1 问题1: 下载aar、jar包太慢

解决:本地使用代理即可,powershell会自动使用代理。 ( 如果是CMD,需要手动设置 )

- CMD终端设置代理
    - cmd临时使用代理
        - set http_proxy=http://127.0.0.1:7890
        - set https_proxy=http://127.0.0.1:7890
    - 测试速度:
        - curl -L -O https://repo.maven.apache.org/maven2/com/facebook/react/hermes-android/0.76.3/hermes-android-0.76.3-release.aar

- PowerShell终端设置代理
    - 不用设置,会自动使用本地代理
    - 测试速度
        - Invoke-WebRequest -Uri "https://repo.maven.apache.org/maven2/com/facebook/react/hermes-android/0.76.3/hermes-android-0.76.3-release.aar" -OutFile "hermes-android-0.76.3-release.aar" -Verbose

无效:

  1. 下载 react-android-0.72.3-release.aar 放到 C:\Users\用户名.gradle\caches\modules-2\files-2.1\com.facebook.react\react-android\0.72.3
  2. 下载 hermes-android-0.72.3-release.aar 放到 C:\Users\用户名.gradle\caches\modules-2\files-2.1\com.facebook.react\hermes-android\0.72.3

5.3.2 问题2: react-native-reanimated打包报错:ninja: error: mkdir(xxxxxx): No such file or directory

解决:https://blog.csdn.net/qq_53372572/article/details/143107780

// 报错原因:路径太长

// 解决
1、查看报错中具体文件名,比如我的是。。。\\react-native-reanimated\\android\\.cxx\\RelWithDebInfo\\241j6e2e\\arm64-v8a
2、进入相应文件夹,比如我的是。。。\node_modules\react-native-reanimated\android
3、文件夹中有一个CMakeLists.txt文件,需要根据报错文件实际路径长度在CMakeLists.txt中设置CMAKE_OBJECT_PATH_MAX大小。
例:我的示例中告警文件为arm64-v8a,长度为9字符,在默认值250的基础上增加9,即设置set(CMAKE_OBJECT_PATH_MAX 259)

// 对应我的文件
F:\testItem\ai-closet-main\ai-closet-main\node_modules\react-native-reanimated\android
set(CMAKE_OBJECT_PATH_MAX 261)

5.3.3 问题3: 打包apk后不能通过http访问

解决:后台服务改https。

无效:

  1. android\app\src\main\AndroidManifest.xml 添加 android:networkSecurityConfig="@xml/network_security_config"
  2. android\app\src\main\AndroidManifest.xml 添加 android:usesCleartextTraffic="true"

5.3.4 问题4: 升级react-native版本,导致打包时重新拉取aar

解决: 回退版本

5.4 打包成功

输出 \android\app\build\outputs\apk\release\app-release.apk

打包1

6. 部署后台服务

记录部分问题,该部分和react-native无关。

  • 问题1: 阿里云CDN加速ECS
  • 问题2: web访问时options请求404,导致请求跨域
    server {
        # ...其他配置...
    
        location /closet/api/ {
            # 核心CORS头(需同时作用于OPTIONS和实际请求)
            add_header 'Access-Control-Allow-Origin' '*' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
            add_header 'Access-Control-Max-Age' 1728000 always;
    
            # 显式处理OPTIONS请求
            if ($request_method = 'OPTIONS') {
                # 返回204并结束处理
                return 204;
            }
    
            # 代理到后端
            proxy_pass http://backend;
        }
    }
  • 问题3: 图片没缓存
    1. 接口返回max-age
    
    2. react-native的Image使用强制缓存
    <Image source={{uri: displayImageUri,cache: 'force-cache'}}/>

文档链接

调试文档:https://reactnative.cn/docs/debugging?js-debugger=hermes

文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录