文章

Rn常用模块依赖

Rn常用模块依赖

Rn常用模块依赖

Rn常用模块依赖

initialRouteName={‘BaseTab’} // 启用手势返回 gestureEnabled: true, // 页面加载动画 // slide_from_right 仅限Android,在iOS上使用默认动画(从左往右),这样能保持一致 animation: ‘slide_from_right’, headerStyle: { backgroundColor: ‘#fff’, }, headerTitleAlign: ‘center’, // 自定义返回按钮 headerLeft: () => {}, />

安全区域处理

react-native-safe-area-context

动画

react-native-animatable

弹窗解决方案

react-native-root-siblings

音频

react-native-track-player


保存图片/获取手机图片 > 图片上传(常用于上传头像功能)

@react-native-camera-roll/camera-roll

import RNFS from ‘react-native-fs’; import {CameraRoll} from ‘@react-native-camera-roll/camera-roll’;

export function savePhoto(url) { let androidDownPath = ${RNFS.DocumentDirectoryPath}/${ (Math.random() * 1000) | 0 }.jpg; if (Platform.OS === ‘ios’) { //ios图片保存 let promise = CameraRoll.save(url); promise .then(function (result) { Alert.alert(‘已保存到系统相册’); }) .catch(function (error) { Alert.alert(‘保存失败!\n’ + error); }); } else { if (url.includes(‘file:’)) { let promise = CameraRoll.save(url); promise .then(function (result) { // console.error(JSON.stringify(result)) Alert.alert(‘已保存到系统相册’); }) .catch(function (error) { Alert.alert(‘保存失败!\n’ + error); }); } else { //Android 先下载到本地 let DownloadFileOptions = { fromUrl: url, //下载路径 toFile: androidDownPath, // Local filesystem path to save the file to }; let result = RNFS.downloadFile(DownloadFileOptions); // let _this = this; result.promise .then( function (val) { console.log(‘文件保存成功:’ + androidDownPath); let promise = CameraRoll.save(androidDownPath); promise .then(function (result) { // console.error(JSON.stringify(result)) Alert.alert(‘已保存到系统相册’); }) .catch(function (error) { Alert.alert(‘保存失败!\n’ + error); }); }, function (val) { console.log(‘Error Result:’ + JSON.stringify(val)); Alert.alert(‘Error Result:’ + JSON.stringify(val)); }, ) .catch(function (error) { console.log(error.message); Alert.alert(error.message); }); } } }

日期和时间选择器

react-native-modal-datetime-picker

�底层使用的是**@react-native-community/datetimepicker**


日历

react-native-calendars

Toast轻提示

react-native-root-toast

底层使用的是 react-native-root-siblings 轻提示可以用 react-native-root-siblings自行封装

本地存储

[@react-native-async-storage/async-storage](https://react-native-async-storage.github.io/async-storage/docs/install)

获取设备信息

react-native-device-info

SVG

react-native-svg

主目录下创建 assets 目录,assets 下创建 svg 目录用来存放svg文件

const path = require(‘path’); const fs = require(‘fs’);

const svgFileDir = path.resolve(__dirname, ‘@/assets/svg’);

function readSvgFile(svgFileName) { return new Promise((resolve, reject) => { fs.readFile( path.join(svgFileDir, svgFileName), ‘utf8’, (error, svgFile) => { let svgPath = svgFile.replace( /<\?xml.?\?>|<!–.?–>|<!DOCTYPE.?>/g, ‘’, ); svgPath = svgPath.replace(/[\r\n]/g, ‘’); if (error) { return reject(error); } if (svgFileName.indexOf(‘.svg’) === -1) { return resolve({}); } resolve({ [svgFileName.slice(0, svgFileName.lastIndexOf(‘.’))]: svgPath, }); }, ); }); }

function readSvgDir() { return new Promise((resolve, reject) => { fs.readdir(svgFileDir, (error, svgFiles) => { if (error) { return reject(error); } Promise.all(svgFiles.map(svgFileName => readSvgFile(svgFileName))) .then(data => resolve(data)) .catch(err => reject(err)); }); }); }

readSvgDir() .then(data => { const svgFile = export default { ${data .filter(item => Object.keys(item)[0]) .map(item => ’${Object.keys(item)[0]}’: `${Object.values(item)[0]}`)} }\n;

1
2
3
4
5
6
fs.writeFile(path.resolve(svgFileDir, 'index.js'), svgFile, err => {
  if (err) {
    throw new Error(err);
  }
});   })   .catch(error => {
throw new Error(error);   });

然后在package.json 加入一个命令:"build:svg": "node scripts/svg/index.js"

�执行命令就会在 assets/svg目录下生成 index.js 文件

然后就可以封装MyIcon组件:

import {Platform, Pressable, Text, View, ViewStyle} from ‘react-native’; import React, {Component} from ‘react’; import svgs from ‘@/assets/svg’; import SvgUri from ‘../react-native-svg-uri’; import {SvgCss, SvgXml} from ‘react-native-svg’;

export type IconProps = { // icon名字 name: String, // 颜色 color?: String, // 大小 size?: Number, // 点击回调 onPress?: Function, // style style?: ViewStyle, isBase?: Boolean, };

function MyIconView(props: IconProps) { const {name, color = ‘’, size = 30, onPress, style = {}, isBase} = props; let svgXmlData = null; if (name) { svgXmlData = svgs[name]; } else { let iconArr = Object.keys(svgs); let nn = iconArr[Math.floor(Math.random() * iconArr.length)];

1
svgXmlData = svgs[nn];   }

if (onPress) { return ( <Pressable onPress={onPress} style={style}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}>

    <SvgUri
      width={size}
      height={size}
      svgXmlData={svgXmlData}
      fill={color}
    />
  </Pressable>
);   }   if (!svgXmlData) {
// throw new Error(`No Icon Named ${name} Was Found!`);
console.log('====================================');
console.log(`No Icon Named ${name} Was Found!`);
console.log('====================================');
return null;   }

if (isBase && Platform.OS != ‘web’) { if (color) { svgXmlData = svgXmlData .replace(/fill=”#[0-9a-f]{6}”/g, fill="${color}") .replace(/stroke=”#[0-9a-f]{6}”/g, stroke="${color}"); } return ( <SvgXml width={size} height={size} xml={svgXmlData} fill={color} style={style} /> ); }

return ( <SvgUri width={size} height={size} svgXmlData={svgXmlData} fill={color} style={style} /> ); }

function areEqual(prevProps, nextProps) { if (prevProps.name === nextProps.name) { return true; } else if (prevProps.color === nextProps.color) { return true; } else if (prevProps.size === nextProps.size) { return true; } return false; }

const MyIcon = React.memo(MyIconView, areEqual);

export default MyIcon;

import React from ‘react’; import { View, Image, ImageSourcePropType, ImageResolvedAssetSource, ViewStyle } from ‘react-native’; import xmldom from ‘xmldom’; import Svg, { Circle, Ellipse, G, LinearGradient, RadialGradient, Path, Polygon, Rect, Defs, Stop, } from ‘react-native-svg’; import { camelCaseNodeName, removePixelsFromNodeValue, transformStyle, getEnabledAttributes } from ‘./utils’;

type Props = { fill: string; width: number; height: number; svgXmlData: string; source?: ImageSourcePropType; style?: ViewStyle; };

type State = { svgXmlData: string; };

const { resolveAssetSource } = Image;

const ACCEPTED_SVG_ELEMENTS = [ ‘svg’, ‘g’, ‘circle’, ‘path’, ‘rect’, ‘linearGradient’, ‘radialGradient’, ‘stop’, ‘ellipse’, ‘polygon’, ];

// Attributes from SVG elements that are mapped directly. const SVG_ATTS = [‘viewBox’]; const G_ATTS = [‘id’]; const CIRCLE_ATTS = [‘cx’, ‘cy’, ‘r’, ‘fill’, ‘stroke’]; const PATH_ATTS = [‘d’, ‘fill’, ‘stroke’]; const RECT_ATTS = [‘width’, ‘height’, ‘fill’, ‘stroke’, ‘x’, ‘y’]; const LINEARG_ATTS = [‘id’, ‘x1’, ‘y1’, ‘x2’, ‘y2’]; const RADIALG_ATTS = [‘id’, ‘cx’, ‘cy’, ‘r’]; const STOP_ATTS = [‘offset’]; const ELLIPSE_ATTS = [‘fill’, ‘cx’, ‘cy’, ‘rx’, ‘ry’]; const POLYGON_ATTS = [‘points’];

let ind = 0;

export default class SvgUri extends React.Component<Props, State> { private isComponentMounted = false; constructor(props: Props) { super(props);

1
2
3
4
5
6
7
8
9
10
11
12
this.state = { svgXmlData: props.svgXmlData };

this.createSVGElement = this.createSVGElement.bind(this);
this.obtainComponentAttrs = this.obtainComponentAttrs.bind(this);
this.inspectNode = this.inspectNode.bind(this);
this.fecthSVGData = this.fecthSVGData.bind(this);

// Gets the image data from an URL or a static file
if (props.source) {
  const source: ImageResolvedAssetSource = resolveAssetSource(props.source) || ({} as ImageResolvedAssetSource);
  this.fecthSVGData(source.uri);
}   }

UNSAFE_componentWillMount() { this.isComponentMounted = true; }

UNSAFE_componentWillUnmount() { this.isComponentMounted = false; }

UNSAFE_componentWillReceiveProps(nextProps: Props) { if (nextProps.source) { const source: ImageResolvedAssetSource = resolveAssetSource(nextProps.source) || ({} as ImageResolvedAssetSource); const oldSource: ImageResolvedAssetSource = resolveAssetSource(this.props.source as any) || ({} as ImageResolvedAssetSource); if (source.uri !== oldSource.uri) { this.fecthSVGData(source.uri); } } }

async fecthSVGData(uri: string) { let responseXML = null; try { let response = await fetch(uri); responseXML = await response.text(); } catch (e) { console.error(‘ERROR SVG’, e); } finally { if (this.isComponentMounted) { this.setState({ svgXmlData: responseXML || ‘’ }); } }

1
return responseXML;   }

createSVGElement(node: any, child: any) { let componentAttrs: any = {}; let i = ind++; switch (node.nodeName) { case ‘svg’: componentAttrs = this.obtainComponentAttrs(node, SVG_ATTS); if (this.props.width) componentAttrs.width = this.props.width; if (this.props.height) componentAttrs.height = this.props.height;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    return (
      <Svg key={i} {...componentAttrs}>
        {child}
      </Svg>
    );
  case 'g':
    componentAttrs = this.obtainComponentAttrs(node, G_ATTS);
    return (
      <G key={i} {...componentAttrs}>
        {child}
      </G>
    );
  case 'path':
    componentAttrs = this.obtainComponentAttrs(node, PATH_ATTS);
    return (
      <Path key={i} {...componentAttrs}>
        {child}
      </Path>
    );
  case 'circle':
    componentAttrs = this.obtainComponentAttrs(node, CIRCLE_ATTS);
    return (
      <Circle key={i} {...componentAttrs}>
        {child}
      </Circle>
    );
  case 'rect':
    componentAttrs = this.obtainComponentAttrs(node, RECT_ATTS);
    return (
      <Rect key={i} {...componentAttrs}>
        {child}
      </Rect>
    );
  case 'linearGradient':
    componentAttrs = this.obtainComponentAttrs(node, LINEARG_ATTS);
    return (
      <Defs key={i}>
        <LinearGradient {...componentAttrs}>{child}</LinearGradient>
      </Defs>
    );
  case 'radialGradient':
    componentAttrs = this.obtainComponentAttrs(node, RADIALG_ATTS);
    return (
      <Defs key={i}>
        <RadialGradient {...componentAttrs}>{child}</RadialGradient>
      </Defs>
    );
  case 'stop':
    componentAttrs = this.obtainComponentAttrs(node, STOP_ATTS);
    return (
      <Stop key={i} {...componentAttrs}>
        {child}
      </Stop>
    );
  case 'ellipse':
    componentAttrs = this.obtainComponentAttrs(node, ELLIPSE_ATTS);
    return (
      <Ellipse key={i} {...componentAttrs}>
        {child}
      </Ellipse>
    );
  case 'polygon':
    componentAttrs = this.obtainComponentAttrs(node, POLYGON_ATTS);
    return (
      <Polygon key={i} {...componentAttrs}>
        {child}
      </Polygon>
    );
  default:
    return null;
}   }

obtainComponentAttrs({ attributes }: any, enabledAttributes: any) { let styleAttrs = {}; Array.from(attributes).forEach(({ nodeName, nodeValue }: any) => { Object.assign(styleAttrs, transformStyle(nodeName, nodeValue, this.props.fill)); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let componentAttrs = Array.from(attributes)
  // @ts-ignore
  .map(camelCaseNodeName)
  .map(removePixelsFromNodeValue)
  .filter(getEnabledAttributes(enabledAttributes))
  .reduce(
    (acc, { nodeName, nodeValue }) => ({
      ...acc,
      [nodeName]: this.props.fill && nodeName === 'fill' ? this.props.fill : nodeValue,
    }),
    {},
  );
Object.assign(componentAttrs, styleAttrs);

return componentAttrs;   }

inspectNode(node: any) { //Process the xml node let arrayElements = [];

1
2
3
4
5
6
7
8
9
10
11
12
13
// Only process accepted elements
if (!ACCEPTED_SVG_ELEMENTS.includes(node.nodeName)) return null;
// if have children process them.

// Recursive function.
if (node.childNodes && node.childNodes.length > 0) {
  for (let i = 0; i < node.childNodes.length; i++) {
    let temp = this.inspectNode(node.childNodes[i]);
    if (temp != null) arrayElements.push(temp);
  }
}
let element = this.createSVGElement(node, arrayElements);
return element;   }

render() { try { if (this.state.svgXmlData == null) return null;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  let inputSVG = this.state.svgXmlData.substring(
    this.state.svgXmlData.indexOf('<svg '),
    this.state.svgXmlData.indexOf('</svg>') + 6,
  );

  let doc = new xmldom.DOMParser().parseFromString(inputSVG);

  let rootSVG = this.inspectNode(doc.childNodes[0]);

  return <View style={this.props.style}>{rootSVG}</View>;
} catch (e) {
  console.error('ERROR SVG', e);
  return null;
}   } }

export const camelCase = (value: string) => value.replace(/-([a-z])/g, g => g[1].toUpperCase());

export const camelCaseNodeName = ({nodeName, nodeValue}: any) => ({ nodeName: camelCase(nodeName), nodeValue, });

export const removePixelsFromNodeValue = ({nodeName, nodeValue}: any) => ({ nodeName, nodeValue: nodeValue.replace(‘px’, ‘’), });

export const transformStyle = ( nodeName: string, nodeValue: string, fillProp: string, ) => { if (nodeName === ‘style’) { return nodeValue.split(‘;’).reduce((acc, attribute) => { const [property, value] = attribute.split(‘:’); if (property == ‘’) return acc; else return { …acc, [camelCase(property)]: fillProp && property === ‘fill’ ? fillProp : value, }; }, {}); } return null; };

export const getEnabledAttributes = (enabledAttributes: string[]) => ({nodeName}: {nodeName: string}) => enabledAttributes.includes(nodeName);

webview

react-native-webview

pushy热更新

react-native-update

import {usePushy} from ‘react-native-update’; import React, {useEffect, useState} from ‘react’; import {View, Text, StyleSheet, Modal, DeviceEventEmitter, TouchableOpacity, Image, Platform, Linking} from ‘react-native’; import myConstans from ‘../src/Utils/Constants’; import updateImg from ‘./images/update.png’; import {scaleSizeH, scaleSizeW, setSpText} from ‘../src/Utils/AdapterUtil’; import Toast from ‘react-native-root-toast’;

// 检查更新 export const checkPushyUpdate = callback => { DeviceEventEmitter.emit(‘CheckPushyUpdate’, callback); };

// 获取版本信息 export const getCurrentVersionInfoEmit = callback => { DeviceEventEmitter.emit(‘GetCurrentVersionInfo’, callback); };

function HotUpdate() { const { checkUpdate, downloadUpdate, switchVersion, updateInfo, packageVersion, getCurrentVersionInfo, downloadAndInstallApk, progress: {received = 0, total} = {}, } = usePushy();

// 是否存在新的热 或 原声包过期,都需要进行更新 // 注意:后台设置原声包过期时一定要 在pushy网页管理端设置的原生版本下载地址 const visible = Boolean(updateInfo?.update || updateInfo?.expired); const [update, setUpdate] = useState(false);

useEffect(() => { const eventListener1 = DeviceEventEmitter.addListener(‘CheckPushyUpdate’, checkUpdateCallback); const eventListener2 = DeviceEventEmitter.addListener(‘GetCurrentVersionInfo’, getVersionInfoCallback); return () => { eventListener1.remove(); eventListener2.remove(); }; }, [updateInfo]);

// 检查更新回调 const checkUpdateCallback = async () => { await checkUpdate(); if (updateInfo?.upToDate) { Toast.show(‘当前已是最新’, {position: Toast.positions.CENTER}); } };

// 获取版本信息回调 const getVersionInfoCallback = async (event) => { const result = await getCurrentVersionInfo(); return event(result); };

// 安装更新 const installUpdate = async () => { setUpdate(true); if (updateInfo?.update) { await downloadUpdate(); await switchVersion(); }

1
2
3
4
5
6
7
8
9
10
// 该应用原生包已过期(三种情况:1. 主动设置为过期状态,2. 主动删除,3. 从未上传)
if (updateInfo?.expired && updateInfo?.downloadUrl && Platform.OS === 'android') {
  // Android 下载新的 apk 包并直接安装
  await downloadAndInstallApk(updateInfo?.downloadUrl);
}
if (updateInfo?.expired && Platform.OS === 'ios') {
  // iOS 跳转到 testflight
  await Linking.openURL('https://testflight.apple.com/join/6lkH73ao');
}
setUpdate(false);   };

return ( <Modal visible={visible} transparent={true} statusBarTranslucent={true} > <View style={styles.container}> <View style={styles.content}> <Image source={updateImg} resizeMode=”contain”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        style={{width: '100%', height: scaleSizeW(228)}}

      />
      <View style={styles.contentView}>
        <Text style={styles.titleText}>发现新版本</Text>
        <Text style={styles.nameText}>{updateInfo?.update ? `bundle:v${updateInfo?.name}` : `V${packageVersion}`}</Text>
        <View style={styles.infoView}>
          <Text style={styles.subTitle}>更新内容</Text>
          <Text style={styles.infoText} numberOfLines={4}>{updateInfo?.update ? updateInfo?.description : '该应用原生包已有新版本,点击“立即安装更新”下载最新版'}</Text>
        </View>
        <View style={styles.btnBox}>
          {!update ? (
            <TouchableOpacity
              style={styles.btnView}
              onPress={installUpdate}
            >
              <Text style={styles.btnText}>立即安装更新</Text>
            </TouchableOpacity>
          ) : (
            <View>
              {
                updateInfo?.update ? (
                  <Text style={styles.received}>更新下载进度:{received} / {total}</Text>
                ) : (
                  <Text style={styles.received}>请稍候,即将下载最新版本</Text>
                )
              }
            </View>
          )}
        </View>
      </View>
    </View>
  </View>
</Modal>   ); }

const styles = StyleSheet.create({ container: { flex: 1, position: ‘absolute’, left: 0, right: 0, top: 0, bottom: 0, justifyContent: ‘center’, alignItems: ‘center’, backgroundColor: ‘rgba(0,0,0,0.7)’, height: myConstans.screenH, width: myConstans.screenW, }, content: { width: ‘70%’, overflow: ‘hidden’, borderRadius: scaleSizeW(20), }, contentView: { backgroundColor: ‘#fff’, }, titleText: { color: ‘#2979FF’, fontSize: setSpText(30), fontWeight: ‘bold’, marginTop: scaleSizeW(20), textAlign: ‘center’, }, nameText: { color: ‘#878788’, fontSize: setSpText(26), textAlign: ‘center’, marginTop: scaleSizeW(5), }, subTitle: { color: ‘#000’, fontSize: setSpText(28), marginTop: 10, }, infoView: { minHeight: scaleSizeW(260), paddingHorizontal: scaleSizeH(30) }, infoText: { color: ‘#999’, fontSize: setSpText(24), marginTop: scaleSizeW(10), lineHeight: scaleSizeW(34), }, btnBox: { borderTopWidth: scaleSizeW(1), borderTopColor: ‘#eee’, height: scaleSizeW(120), alignItems: ‘center’, justifyContent: ‘center’, }, btnView: { height: scaleSizeW(70), width: ‘70%’, alignItems: ‘center’, borderRadius: 20, justifyContent: ‘center’, backgroundColor: ‘#2979FF’, }, btnText: { color: ‘#fff’, fontSize: setSpText(28) }, received: { color: ‘#313131’, fontSize: setSpText(24) } });

export default HotUpdate;

Echarts图表

react-native-echarts-proecharts

react-native-echarts-pro 底层使用的就是 react-native-webview

网络信息 API

@react-native-community/netinfo

本文由作者按照 CC BY 4.0 进行授权