项目初始化 & 资源准备
初始化项目
npx react-native init 项目名
安卓端编译
打开Android Studio并打开项目下的android目录,执行gradle编译安卓原生端
改APP名
位置:app/src/main/res/values/strings.xml
<resources>
<string name="app_name">APP名称</string>
</resources>
改applicationId
位置:app/build.gradle,更改内容后,点击上方的Sync Now
defaultConfig {
applicationId "com.xxx"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
替换App图标
位置:app/src/main/res hdpi和mdpi分辨率太低,绝大多数已经不需要了,删掉
将默认ic_launcher.png图标替换成自己APP的logo
ic_launcher_round.png,如果不需要可以不配置,删掉,并在app/src/main/AndroidMainfest.xml中引用到此图标的地方替换为ic_launcher
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="false"
android:theme="@style/AppTheme">
项目其他配置安装
typescript
react-native默认安装typescript,同时默认生成了tsconfig.json,但配置内容较少,删除默认tsconfig.json,通过 tsc --init 命令生成新的 tsconfig.json
还需安装 npm i --save-dev @types/react @types/react-native
集成async-storage
npm i @react-native-async-storage/async-storage 并封装方法
import AsyncStorage from '@react-native-async-storage/async-storage';
export const save = async (key: string, value: string) => {
try {
return await AsyncStorage.setItem(key, value);
} catch (e) {
console.error('AsyncStorage save error: ', e);
}
};t
export const get = async (key: string) => {
try {
return await AsyncStorage.getItem(key);
} catch (e) {
console.error('AsyncStorage get error: ', e);
return null;
}
};
export const remove = async (key: string) => {
try {
return await AsyncStorage.removeItem(key);
} catch (e) {
console.error('AsyncStorage remove error: ', e);
}
};
export const clear = async () => {
try {
return await AsyncStorage.clear();
} catch (e) {
console.error('AsyncStorage clear error: ', e);
}
};
配置绝对路径
npm i babel-plugin-module-resolver
"baseUrl": "./src",
"paths": {
"@/assets/*": ["assets/*"],
"@/utils/*": ["utils/*"],
"@/modules/*": ["modules/*"],
"@/components/*": ["components/*"],
"@/api/*": ["api/*"],
"@/stores/*": ["stores/*"],
},
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@/utils': './src/utils',
'@/components': './src/components',
'@/assets': './src/assets',
'@/pages': './src/pages',
'@/api': './src/api',
'@/stores': './src/stores',
},
},
],
],
};
路由管理 & 集成react-navigation
react-navigation库本身
- @react-navigation/bottom-tabs
- @react-navigation/native
- @react-navigation/stack
依赖的安装
- react-native-gesture-handler
- react-native-safe-area-context
- react-native-screen
import React from 'react';
import {StatusBar} from 'react-native';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import Welcome from '@/modules/welcome/Welcome';
import Login from '@/modules/login/Login';
const Stack = createStackNavigator();
function App(): React.JSX.Element {
return (
<SafeAreaProvider>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'} />
<NavigationContainer>
<Stack.Navigator initialRouteName="Welcome">
<Stack.Screen
name="Welcome"
component={Welcome}
options={{
headerShown: false,
...TransitionPresets.SlideFromRightIOS,
}}
/>
<Stack.Screen
name="Login"
component={Login}
options={{
headerShown: false,
...TransitionPresets.SlideFromRightIOS,
}}
/>
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
export default App;
import React, {useEffect} from 'react';
import {View, StyleSheet} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
export default () => {
const navigation = useNavigation<StackNavigationProp<any>>();
useEffect(() => {
setTimeout(() => {
navigation.replace('Login');
}, 3000);
}, []);
return (
<View style={styles.root}>Welcom页面 3秒 后跳转到 Login页面</View>
);
};
const styles = StyleSheet.create({
root: {
width: '100%',
height: '100%',
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'center',
},
});
安装axios 并 封装接口请求 axios
const apiConfig = {
login: {
url: '/user/login',
method: 'get',
},
};
export default apiConfig;
import axios, {AxiosResponse} from 'axios';
import Apis from '@/api/Apis';
const instance = axios.create({
baseURL: 'http://192.168.3.20:7001/',
timeout: 10 * 1000,
});
// 错误信息分类
instance.interceptors.response.use(
response => response,
error => {
const {response} = error;
if (response) {
const {status} = response;
if (status >= 500) {
// 服务端报错
} else if (status === 400) {
// 接口参数异常
} else if (status === 401) {
// 登录信息过期,需要重新登录
} else {
// 其他错误类型,统一按照接口报错处理
}
} else {
// 网络异常
}
return Promise.reject(error);
},
);
export const request = (
name: string,
params: any,
): Promise<AxiosResponse<any, any>> => {
const api = (Apis as any)[name];
if (!api) {
return;
}
const {url, method} = api;
if (method === 'get') {
return get(url, params);
} else {
return post(url, params);
}
};
const get = (url: string, params: any): Promise<AxiosResponse<any, any>> => {
return instance.get(url, {
params,
});
};
const post = (url: string, params: any): Promise<AxiosResponse<any, any>> => {
return instance.get(url, params);
};
安装mobx: 可监听的数据 mobx mobx-react
import {request} from '@/utils/request';
import {flow} from 'mobx';
import {save} from '@/utils/Storage';
class UserStore {
userInfo: any;
requestLogin = flow(function* (
this: UserStore,
phone: string,
pwd: string,
callback: (success: boolean) => void,
) {
try {
const params = {
name: phone,
pwd,
};
const {data} = yield request('login', params);
if (data) {
save('userInfo', JSON.stringify(data));
this.userInfo = data;
callback?.(true);
} else {
this.userInfo = null;
callback?.(false);
}
} catch (e) {
console.log(e);
this.userInfo = null;
callback?.(false);
}
});
}
export default new UserStore();
ios权限 react-native-native-permissions
关于保存图片,访问相册用来拍照问题,上传图片等,需要相关权限
目前用到了camera和saveImage,其他可以不看,权限分为android和ios,需要分开处理
ios 需要在xcode的info.plist 加入以下权限
Privacy - Camera Usage Description - App需要访问您的麦克风
Privacy - Photo Library Additions Usage Description - App需要访问您的相册
Privacy - Photo Library Usage Description - Your message to user when the photo library is accessed for the first time
android 需要在AndroidMainfest.xml 加入以下权限
<!-- 相机权限,用于图片消息时拍摄图片,不使用拍照可以移除 -->
<uses-permission android:name="android.permission.CAMERA" />
```js
import {PermissionsAndroid, Platform} from 'react-native';
import {check, PERMISSIONS, request, RESULTS} from 'react-native-permissions';
import * as ImagePicker from 'react-native-image-picker';
const permissionMap = {
camera: {
ios: PERMISSIONS.IOS.CAMERA,
android: PermissionsAndroid.PERMISSIONS.CAMERA,
},
image: {
android: PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
},
video: {
android: PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
},
file: {
android: PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
},
saveImage: {
android: PermissionsAndroid.PERMISSIONS.CAMERA,
// android: PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
ios: PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY,
},
};
// 检查权限并请求相机权限
export async function requestCameraPermission() {
if (Platform.OS === 'ios') {
return await checkIOSPermission(PERMISSIONS.IOS.CAMERA);
} else if (Platform.OS === 'android') {
return await androidRequestPermission(
PermissionsAndroid.PERMISSIONS.CAMERA,
);
} else {
return null;
}
}
// 检查权限并请求相机权限
export async function requestImagePermission() {
if (Platform.OS === 'ios') {
return await checkIOSPermission(PERMISSIONS.IOS.CAMERA);
} else if (Platform.OS === 'android') {
return await androidRequestPermission(
PermissionsAndroid.PERMISSIONS.CAMERA,
);
} else {
return null;
}
}
// 检查权限并请求相机权限
export async function requestPlatFormPermission(permissionType) {
if (Platform.OS === 'ios') {
return await checkIOSPermission(permissionMap[permissionType].ios);
} else if (Platform.OS === 'android') {
return await androidRequestPermission(
permissionMap[permissionType].android,
);
} else {
return null;
}
}
// export async function requestCameraAction() {
// if (Platform.OS === 'ios') {
// await request(PERMISSIONS.IOS.CAMERA);
// } else if (Platform.OS === 'android') {
// await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
// } else {
// return null;
// }
// }
async function androidRequestPermission(PERMISSION_NAME) {
if (PERMISSION_NAME !== 'saveImage') {
const hasPermission = await PermissionsAndroid.check(PERMISSION_NAME);
console.log('androidRequestPermission-----', hasPermission);
if (hasPermission) return true;
const status = await PermissionsAndroid.request(PERMISSION_NAME);
console.log('androidRequestPermission status-----', status);
return status === PermissionsAndroid.RESULTS.GRANTED;
} else {
if (Platform.Version >= 33) {
const imageRes = await PermissionsAndroid.check('image');
const videoRes = await PermissionsAndroid.check('video');
console.log('androidRequestPermission-----', hasPermission);
if (imageRes && videoRes) return true;
const imageStatus = await PermissionsAndroid.request('image');
const videoStatus = await PermissionsAndroid.request('video');
console.log('androidRequestPermission status-----', status);
return (
imageStatus === PermissionsAndroid.RESULTS.GRANTED &&
videoStatus === PermissionsAndroid.RESULTS.GRANTED
);
} else {
const hasPermission = await PermissionsAndroid.check('camera');
console.log('androidRequestPermission-----', hasPermission);
if (hasPermission) return true;
const status = await PermissionsAndroid.request('camera');
console.log('androidRequestPermission status-----', status);
return status === PermissionsAndroid.RESULTS.GRANTED;
}
}
}
async function checkIOSPermission(PERMISSION_NAME) {
const res = await check(PERMISSION_NAME);
console.log('checkIOSPermission-----', PERMISSION_NAME, res);
if (res === RESULTS.BLOCKED) {
return false;
} else if (res === RESULTS.DENIED) {
const status = await request(PERMISSION_NAME);
return status === RESULTS.GRANTED;
}
return res === RESULTS.GRANTED;
// case RESULTS.UNAVAILABLE: // This feature is not available (on this device / in this context
// case RESULTS.DENIED: // The permission has not been requested / is denied but requestable
// case RESULTS.LIMITED: // The permission is limited: some actions are possible
// case RESULTS.GRANTED: // The permission is granted
// case RESULTS.BLOCKED: // The permission is denied and not requestable anymore
}
export function chooseImage() {
return new Promise((resolve, reject) => {
ImagePicker.launchImageLibrary(
{
mediaType: 'photo',
quality: 1,
includeBase64: true,
},
(res: ImagePicker.ImageLibraryOptions) => {
const {assets} = res;
if (!assets?.length) {
reject('选择图片失败');
return;
}
const {uri, width, height, fileName, fileSize, type} = assets[0];
console.log(`uri=${uri}, width=${width}, height=${height}`);
console.log(`fileName=${fileName}, fileSize=${fileSize}, type=${type}`);
resolve(assets[0]);
},
);
});
}
export function takePhoto() {
return new Promise((resolve, reject) => {
ImagePicker.launchCamera(
{
mediaType: 'photo',
quality: 1,
includeBase64: true,
},
res => {
if (res.didCancel) {
reject('User cancelled photo take');
} else {
const {assets} = res;
if (!assets?.length) {
reject('拍照失败');
return;
}
const {uri, width, height, fileName, fileSize, type} = assets[0];
console.log(`uri=${uri}, width=${width}, height=${height}`);
console.log(
`fileName=${fileName}, fileSize=${fileSize}, type=${type}`,
);
resolve(assets[0]);
}
},
);
});
}
// async function hasAndroidPermission() {
// const getCheckPermissionPromise = () => {
// if (Platform.Version >= 33) {
// return Promise.all([
// PermissionsAndroid.check(
// PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
// ),
// PermissionsAndroid.check(
// PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
// ),
// ]).then(
// ([hasReadMediaImagesPermission, hasReadMediaVideoPermission]) =>
// hasReadMediaImagesPermission && hasReadMediaVideoPermission,
// );
// } else {
// return PermissionsAndroid.check(
// PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
// );
// }
// };
// const hasPermission = await getCheckPermissionPromise();
// if (hasPermission) {
// return true;
// }
// const getRequestPermissionPromise = () => {
// if (Platform.Version >= 33) {
// return PermissionsAndroid.requestMultiple([
// PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
// PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
// ]).then(
// statuses =>
// statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES] ===
// PermissionsAndroid.RESULTS.GRANTED &&
// statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO] ===
// PermissionsAndroid.RESULTS.GRANTED,
// );
// } else {
// return PermissionsAndroid.request(
// PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
// ).then(status => status === PermissionsAndroid.RESULTS.GRANTED);
// }
// };
// return await getRequestPermissionPromise();
// }
集成相册选择模块,跳转系统图库 react-native-image-picker
用法
import * as ImagePicker from 'react-native-image-picker';
// 选择图库图片
export function chooseImage() {
return new Promise((resolve, reject) => {
ImagePicker.launchImageLibrary(
{
mediaType: 'photo',
quality: 1,
includeBase64: true,
},
(res: ImagePicker.ImageLibraryOptions) => {
const {assets} = res;
if (!assets?.length) {
reject('选择图片失败');
return;
}
const {uri, width, height, fileName, fileSize, type} = assets[0];
resolve(assets[0]);
},
);
});
}
export function takePhoto() {
return new Promise((resolve, reject) => {
ImagePicker.launchCamera(
{
mediaType: 'photo',
quality: 1,
includeBase64: true,
},
res => {
if (res.didCancel) {
reject('User cancelled photo take');
} else {
console.log(res, '---');
}
},
);
});
}
⚠️ 注意
IOS 使用方式
使用相机功能时,需要新增以下字段到ios目录下的info.plist,目的为开启IOS相机权限
<key>NSCameraUsageDescription</key>
<string>App需要访问您的相机以便拍照和录像</string>
获取设备信息 react-native-device-info
import DeviceInfo from 'react-native-device-info';
console.log(`${DeviceInfo.getModel()} - ${DeviceInfo.getSystemName()} - ${DeviceInfo.getSystemVersion()}`)
视频播放器 react-native-video
thewidlarzgroup.github.io/react-nativ…
"react-native-video": "^6.2.0"
安卓用法
buildscript {
ext {
kotlinVersion = "1.9.22"
useExoplayerIMA = true
useExoplayerRtsp = true
useExoplayerSmoothStreaming = true
useExoplayerDash = true
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.github.bumptech.glide:glide:4.12.0")
classpath("com.github.bumptech.glide:compiler:4.12.0")
}
}
"react-native-video": "^5.2.0" 版本存在问题,回退页面时,controls会在后一页面出现几秒
自定义数字键盘 react-native-keyboard
const onKeyboardDelete = () => {};
const onKeyboardPress = key => {
console.log(key)
};
<Keyboard
keyboardType="number-pad"
onDelete={onKeyboardDelete}
onKeyPress={onKeyboardPress}
/>
扫码 react-native-camera + react-native-qrcode-scanner-view
⚠️ 注意
使用react-native-qrcode-scanner-view时,会出现react-native-camera找不到的情况,react-native-camera 因被废弃,需看旧文档react-native-camera.github.io/react-nativ…
Mostly automatic install with autolinking (RN > 0.60)
npm install react-native-camera --save- Run
cd ios && pod install && cd ..
react-native-qrcode-scanner-view 坑点
当前最新版本运行时,会出现removeEventListener被移除,需要调整为如下形式,并将class形式改为函数式,如下代码,将一些源码中不会用到的东西有删除掉
import React, {useEffect, useRef, useState} from 'react';
import {AppState, TouchableOpacity, Text} from 'react-native';
import {RNCamera} from 'react-native-camera';
import {QRScannerRectView} from 'react-native-qrcode-scanner-view';
import {useNavigation} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
import EventBus from 'react-native-event-bus';
import Toast from '@/components/widget/Toast';
export default () => {
const navigation = useNavigation<StackNavigationProp<any>>();
// const [scanInterval] = useState(2000);
const appState = useRef(AppState.currentState);
const [scanTimes, setScanTimes] = useState(0);
const scannerRef = useRef(null);
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
const isInactive = appState.current.match(/inactive|background/);
if (isInactive && nextAppState === 'active') {
scannerRef.current?.resumePreview();
} else if (nextAppState !== 'active') {
scannerRef.current?.pausePreview();
}
appState.current = nextAppState;
});
return () => {
subscription.remove();
scannerRef.current?.pausePreview();
};
}, []);
return (
<RNCamera
ref={scannerRef}
captureAudio={false}
onBarCodeRead={e => {
setScanTimes(value => {
if (value === 0) {
return 1;
}
return 2;
});
setScanTimes(value => {
const data = e?.data || '';
if (value === 1) {
if (data.indexOf('navigateTo://OuterPay') > -1) {
const dataArr = data.split('navigateTo://').filter(item => item);
const navigateStr = dataArr[0];
const navigateArr = navigateStr.split('/');
navigation.replace(navigateArr[0], {
orderNo: navigateArr[1],
});
} else if (data.indexOf('nativeScan://') > -1) {
const dataArr = data.split('nativeScan://').filter(item => item);
const text = dataArr[0];
EventBus.getInstance().fireEvent('onScanSuccess', {
data: text,
});
navigation.goBack();
} else if (typeof data === 'string') {
// 转豆 对方地址
navigation.replace('TransferBean', {
toAddress: data,
});
}
}
return value;
});
// Toast.show(e.data);
// throttle(onScanResult(e), scanInterval, {
// maxWait: 0,
// trailing: false,
// })
}}
type={RNCamera.Constants.Type.back}
flashMode={RNCamera.Constants.FlashMode.off}
style={{flex: 1}}>
{/*绘制扫描遮罩*/}
<QRScannerRectView isShowScanBar={true} />
{/* <TouchableOpacity onPress={() => {
EventBus.getInstance().fireEvent("onScanSuccess", {
res: '123213'
})
navigation.goBack()
}}>
<Text>1212121</Text>
</TouchableOpacity> */}
</RNCamera>
);
};
// Copy from lodash
function throttle(func, wait, options) {
let leading = true;
let trailing = true;
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
leading,
trailing,
maxWait: wait,
});
}
// Copy from lodash
function debounce(func, wait, options) {
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;
let lastInvokeTime = 0;
let leading = false;
let maxing = false;
let trailing = true;
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF =
!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function';
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
wait = +wait || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId);
return root.requestAnimationFrame(pendingFunc);
}
return setTimeout(pendingFunc, wait);
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id);
}
clearTimeout(id);
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now());
}
function pending() {
return timerId !== undefined;
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}
// Copy from lodash
function isObject(value) {
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
}
Android - 额外步骤(需加,否则无法使用)
Add permissions to your app android/app/src/main/AndroidManifest.xml file:
<!-- Required -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Include this only if you are planning to use the camera roll -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Include this only if you are planning to use the microphone for video recording -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Insert the following lines in android/app/build.gradle:
android { ... defaultConfig { ... missingDimensionStrategy 'react-native-camera', 'general' // <--- insert this line } }
IOS 模拟器运行问题
ios模拟器运行报错找不到main.jsbundle
- cd 项目名/ios
- bundle install
- bundle exec pod install
unable to boot simulator
检查模拟器配置
xcrun simctl list devices
确保模拟器的状态是 "Booted"。如果状态显示为 "Shutdown" 或其他错误状态,请尝试重启模拟器。您可以使用以下命令来重启模拟器:
xcrun simctl shutdown xcrun simctl boot
unable to initiate PIF transfer session
Build service could not create build operation: unknown error while handling message: MsgHandlingError(message: "unable to initiate PIF transfer session (operation in progress?)")
解决方法: 重启 xcode
react-native xcode 打包ipa
在React Native项目中使用Xcode打包IPA文件的步骤如下:
-
通过ios目录下的
项目名.xcworkspace打开Xcode,在菜单栏选择Product>Destination>Any iOS Device。 -
确保你的Xcode工程设置中的Bundle Identifier与你的Apple Developer账户匹配。
-
在Xcode中选择你的Team(在
Product菜单下的Scheme选项旁边)。 -
点击
Product菜单中的Archive开始打包。 -
打包完成后,在
Window菜单选择Organizer。 -
在Organizer中,选择你的项目的最新Archive,然后点击
Export。 -
在Export窗口中,选择
Export as IPA选项,然后选择存储位置保存IPA文件。
以上步骤完成后,你将在指定的位置找到你的IPA文件,IOS实现分发
Android部署后,无法访问http接口的问题
因google限制请求http所以需要额外配置,配置如下,如果访问的是https不会有问题。
- 在app/src/main/res下新建文件夹xml
- 新增文件network_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 在AndroidMainfest.xml中添加
<application
...
android:networkSecurityConfig="@xml/network_config"
...
>
</application>
安卓APP版本设置
app/build.gradle 中
defaultConfig {
applicationId "com.xxx"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
h5如何调起APP,并打开某个特定的页面
h5端使用callapp-lib: github.com/suanmei/cal…
function openApp() {
const options = {
scheme: {
protocol: 'xxx',
host: '',
},
intent: {
scheme: 'xxx',
package: 'xxx',
},
universal: {
host: 'xxx',
pathKey: 'xxx',
},
fallback: '', // 打开失败跳转地址
// appstore: 'https://itunes.apple.com/cn/app/id1383186862',
// yingyongbao: '//a.app.qq.com/o/simple.jsp?pkgname=com.youku.shortvideo'
};
const callLib = new CallApp(options);
callLib.open({
param: {}, // 打开 APP 某个页面,它需要接收的参数。react-native中尝试使用监听获取参数,未获取到,不知道为何,故仅仅采用 path/:id/:name 形式
path: `testPage/123456`, // 需要打开的页面对应的值,URL Scheme 中的 path 部分,参照 H5 唤起 APP 指南 一文中的解释。只想要直接打开 app ,不需要打开特定页面,path 传空字符串 '' 就可以。
// callback: () => { // 自定义唤端失败回调函数。传递 callback 会覆盖 callapp-lib 库中默认的唤端失败处理逻辑。
// alert('失败')
// },
});
}