react-native 实践

900 阅读10分钟

项目初始化 & 资源准备

初始化项目

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

www.npmjs.com/package/rea…

关于保存图片,访问相册用来拍照问题,上传图片等,需要相关权限

目前用到了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)

  1. npm install react-native-camera --save
  2. Run cd ios && pod install && cd ..

react-native-qrcode-scanner-view 坑点

gitcode.com/MarnoDev/re…

当前最新版本运行时,会出现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文件的步骤如下:

  1. 通过ios目录下的 项目名.xcworkspace 打开Xcode,在菜单栏选择 Product > Destination > Any iOS Device

  2. 确保你的Xcode工程设置中的Bundle Identifier与你的Apple Developer账户匹配。

  3. 在Xcode中选择你的Team(在 Product 菜单下的 Scheme 选项旁边)。

  4. 点击 Product 菜单中的 Archive 开始打包。

  5. 打包完成后,在 Window 菜单选择 Organizer

  6. 在Organizer中,选择你的项目的最新Archive,然后点击 Export

  7. 在Export窗口中,选择 Export as IPA 选项,然后选择存储位置保存IPA文件。

以上步骤完成后,你将在指定的位置找到你的IPA文件,IOS实现分发

Android部署后,无法访问http接口的问题

因google限制请求http所以需要额外配置,配置如下,如果访问的是https不会有问题。

  1. 在app/src/main/res下新建文件夹xml
  2. 新增文件network_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
  1. 在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('失败')
        // },
      });
    }

如何使用第三方字体

www.jianshu.com/p/65c6e7bc1…