前言
对 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
2.2 android
运行android模拟器 [ 'Genymotion'、'真机'、'Android Studio' ]
npm run android
android模拟器自动安装Expo Go,然后运行项目
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. 如果点击后程序自动停止了,可能手机没开 "开发者调试"
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/ 即可查看打包进度
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
无效:
- 下载
react-android-0.72.3-release.aar
放到C:\Users\用户名.gradle\caches\modules-2\files-2.1\com.facebook.react\react-android\0.72.3
- 下载
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。
无效:
android\app\src\main\AndroidManifest.xml
添加android:networkSecurityConfig="@xml/network_security_config"
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
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