React-Native 最佳实践

一、将.pk8 和.pem 转换成 react-native 的 debug.keystore

Step1. 安装 openssl

参考:https://stackoverflow.com/questions/42918916/npm-install-openssl-failed-on-windows-10

Step2. 把 pkcs8 格式的私钥转换为 pkcs12 格式,生成 platform.priv.pem 文件

openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out platform.priv.pem -nocrypt

Step3. 生成 pkcs12 格式的密钥文件,生成 platform.pk12 文件,最后的 brilliance 是 keystore 的 alias,需要输入两次密码,我们这里默认为 android

openssl pkcs12 -export -in platform.x509.pem -inkey platform.priv.pem -out platform.pk12 -name brilliance

Step4. 生成 platform.keystore

keytool -importkeystore -deststorepass android -destkeypass android -destkeystore platform.keystore -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass android -alias brillianc

二、React-Native 重命名 package & 重命名 app

1. 重命名 package

Step1. 重命名文件夹

android/app/src/main/java/MY/APP/OLD_ID/ 重命名为: android/app/src/ main/java/MY/APP/NEW_ID/

这里的 NEW_ID 也可能是多级文件夹,例如: com/fungmo/a08

Step2. 配置包 ID

1.在 android/app/src/main/java/MY/APP/NEW_ID/MainActivity.java 中:

package MY.APP.NEW_ID; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08

2.在 android/app/src/main/java/MY/APP/NEW_ID/MainApplication.java 中:

package MY.APP.NEW_ID; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
import MY.APP.NEW_ID.generated.BasePackageList; //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
Class<?> aClass = Class.forName("MY.APP.NEW_ID.ReactNativeFlipper"); //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08

3.在 android/app/src/main/AndroidManifest.xml 中:

package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08

4.在 android/app/build.gradle 中:

applicationId "MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08

5.在 android/app/BUCK 中:

android_build_config(
  package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
)
android_resource(
  package="MY.APP.NEW_ID" //这里的 MY.APP.NEW_ID 项目中为例如 com.fungmo.a08
)

Step3. 最后进行 Gradle 清理(在 /android 文件夹中):

gradlew clean //cmd
//或者
./gradlew clean //powershell

2、重命名 app

生成器不会覆盖位于 android/app/src/main/res/values/ 文件夹中的 strings.xml 文件,因此必须 手动更改 app_name 变量

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
  <string name="app_name">用户手册</string>
</resources>

三、RN 启动屏

npm i react-native-splash-screen --save

配置参考

Step1.

转到app/src/main/java/[packageName]并创建一个新文件SplashActivity.java然后将以下代码复制粘贴到其中。

package com.packagename; // Replace this with your package name 替换为自己的 package 名

import android.content.Intent;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class SplashActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();
    }
}

Step2.

app/src/main/AndroidManifest.xml和修改它,如下所示使用SplashActivity: 在<application>标签内添加以下活动。

<activity
    android:name=".SplashActivity"
    android:theme="@style/SplashTheme"
    android:label="@string/app_name"
    >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

MainActivity标签中删除以下意图。

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

并添加android:exported="true"该活动。 现在,您的AndroidManifest.xml应该如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.packagename">

    <uses-permission android:name="android.permission.INTERNET" />

    <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">

      <activity
        android:name=".SplashActivity"
        android:theme="@style/SplashTheme"
        android:label="@string/app_name"
        >
        <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>

      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true"
        >
      </activity>

      <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>

Step3.

现在,我们将声明SplashThemefor SplashActivity。转到app/src/main/res/values/styles.xml并在<resources>中添加以下样式。

<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:background">@drawable/background_splash</item>
        <item name="android:statusBarColor">@color/background</item>
</style>

Step4.

转到android\app\src\main\res\values并创建一个文件(colors.xml如果尚不存在)。 我们在上面使用了背景颜色常量,因此必须将其添加到colors.xml文件中。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Insert your background color for the splash screen -->
    <color name="background">#fff</color>
</resources>

Step5.

转到android/app/src/main/res/drawable(如果尚不存在,则创建drawable文件夹)并将您的启动屏幕图像(名称应为splash_screen.png)放在此处,并background_splash.xml使用以下代码创建文件:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/background" />
    <item
        android:drawable="@drawable/splash_screen"
        android:height="300dp"
        android:width="300dp"
        android:gravity="center"
    />
</layer-list>

如果您的初始屏幕的尺寸是等于设备屏幕的尺寸,在<item>标签中删除android:heightandroid:width

Step6.

react-native-splash-screen在您的项目中安装模块,然后SplashScreen从 App.js 文件中导入它。 import SplashScreen from 'react-native-splash-screen'; 我们只需要显示初始屏幕,直到安装第一个组件,然后useEffect在 App 组件主体内(返回之前)制作一个钩子,如下所示: 不要忘了import useEffect from 'react'

useEffect(() => {
    SplashScreen.hide();
}, []);

Step7.

转到app/src/main/java/[packageName]/MainActivity.java并导入以下模块,然后导入其他模块。

import org.devio.rn.splashscreen.SplashScreen;
import android.os.Bundle;

将此方法添加到MainActivity类的顶部。

@Override
protected void onCreate(Bundle savedInstanceState) {
    SplashScreen.show(this, R.style.SplashStatusBarTheme);
    super.onCreate(savedInstanceState);
}

Step8.

android/app/src/main/res/values/styles.xml添加SplashStatusBarTheme,就像我们在第 3 步一样。

<style name="SplashStatusBarTheme" parent="SplashScreen_SplashTheme">
        <item name="android:statusBarColor">@color/background</item>
</style>

如果不这样做,则在加载应用程序的 JS 代码时,StatusBar 的颜色将变为黑色。

Step9.

转到android/app/src/main/res/并创建一个新文件夹layout(如果尚不存在)。在该文件夹中,创建一个文件launch_screen.xml(,需要此文件react-native-splash-screen library)。在该文件内,使用以前创建的背景创建布局,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/background_splash"
/>

Step10.

·android/app/src/main/res/values/colors.xml·像在步骤 4 中一样转到并添加以下标签,否则,该应用程序将崩溃。不要更改颜色值。

<color name="primary_dark">#000</color>

四、ReactNative 签名打包 apk(android)

step1. 生成一个签名密钥

C:\Program Files\Java\jdk1.8.0_271\bin 目录中,执行

keytool -genkeypair -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

生成 my-release-key.keystore 密钥库文件

Step2. 设置 gradle 变量

1.把 my-release-key.keystore 文件放到你工程中的 android/app 文件夹下。 2.编辑项目目录 /android/gradle.properties 如果没有 gradle.properties 文件你就自己创建一个,添加如下的代码(注意把其中的**替换为相应密码)

MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****

Step3. 把签名配置加入到项目的 android/app/build.gradle 配置中

...
android {
    ...
    defaultConfig { ... }
    signingConfigs {
       + release {
            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
                storeFile file(MYAPP_RELEASE_STORE_FILE)
                storePassword MYAPP_RELEASE_STORE_PASSWORD
                keyAlias MYAPP_RELEASE_KEY_ALIAS
                keyPassword MYAPP_RELEASE_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            ...
           + signingConfig signingConfigs.release
        }
    }
}
...

step4. 生成发行 APK 包

项目 android 目录中,执行

gradlew assembleRelease  //cmd 或者

./gradlew assembleRelease //PowerShell

生成的 APK 文件位于 android/app/build/outputs/apk/release/app-release.apk
如果需要重新打包,需先删除 \android\app\build\outputs\apk 中的 release 文件夹

step5. 测试发行版本

npx react-native run-android --variant=release

五、RN/Expo 渲染本地图片(资源)列表

由于 RN 中引用本地图片是用require,而 require 是运行时编译的,所以不能在 require 中添加变量。参考 Link

Step1. 将所需要的本地图片 Require 到一处

const image1 = require('../assets/Image1.png')
const image2 = require('../assets/Image2.png')

Step2. 创建一个数组对象

const data = [
  {"id":1, "url": image1},
  {"id":2, "url": image2}
]

Step3. 这样就可以使用 map 渲染 data 数据并正确使用本地图片了

const listItems = data.map(item =>

    <View key={item.id}>
        <Image source={item.url} />
    </View>
)

六、Lottie(lottie-react-native)

Lottie是以json格式导出的Adobe After Effects动画库,并在移动设备和Web上渲染。
本文只介绍RN安卓端的配置。

一. 安装 (React Native >= 0.60.0)

yarn add lottie-react-native

二. 配置文件(如果应用在Android上崩溃,则表示自动链接无效。才需要进行以下配置:)

//1. android/app/src/main/java/<AppName>/MainApplication.java
//    在文件入口(头部),添加
import com.airbnb.android.react.lottie.LottiePackage;
//    在List <ReactPackage> getPackages()中,添加
packages.add(new LottiePackage());

//2. android/app/build.gradle
//    在 dependencies 块中,添加
implementation project(':lottie-react-native')

//3. android/settings.gradle
include ':lottie-react-native'
project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android')

三. 使用

import React from 'react';
import LottieView from 'lottie-react-native';

export default class BasicExample extends React.Component {
  render() {
    return <LottieView source={require('./assets/my.json')} autoPlay loop />;
  }
}

// 其中,assets 是手动在项目根目录中创建的文件夹,这里的my.json即是lottie动画文件
动画文件:lottiefiles

四. 安装完新依赖/新添文件后需要重新编译到安卓设备中,yarn android

七、RN表单验证

这里纯手写,当然也可以使用react-hooks-form等第三方库。

注意:select 的处理方式

import React, { Component } from "react";
import { StyleSheet, KeyboardAvoidingView, TouchableWithoutFeedback, Keyboard } from "react-native";
import { Layout, Input, Button, Select, SelectItem, IndexPath } from "@ui-kitten/components";

const validEmailRegex = RegExp(
    /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
);

const validateForm = errors => {
    let valid = true;
    Object.values(errors).forEach(val => val.length > 0 && (valid = false));
    return valid;
};

const data = [//1.select初始数据
    'A',
    'B',
    'C',
];

export default class FormComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedIndex: new IndexPath(0),//2.select初始选中
            fullName: null,
            email: null,
            password: null,
            errors: {
                fullName: "",
                email: "",
                password: ""
            }
        };
    }

    handlePress = () => {
        if (this.isNotEmpty()) {
            if (validateForm(this.state.errors)) {
                Keyboard.dismiss();
                // alert("Created successfully.");
                const postData = {
                    "typeFeedBack":data[this.state.selectedIndex.row],//3.select选中数据
                }
                console.log(postData)
            }
        } else {
            validateForm(this.state.errors);
        }
    };

    handleChange = (field, value) => {
        let errors = this.state.errors;

        switch (field) {
            case "fullName":
                errors.fullName = value.length < 5 ? "Full Name must be 5 characters long!" : "";
                break;
            case "email":
                errors.email = validEmailRegex.test(value) ? "" : "Email is not valid!";
                break;
            case "password":
                errors.password = value.length < 8 ? "Password must be 8 characters long!" : "";
                break;
            default:
                break;
        }

        this.setState({ errors, [field]: value });
    };

    isNotEmpty = () => {
        const { fullName, email, password } = this.state;
        let isNoError = true;

        if (!fullName) {
            this.setState(prevState => ({
                errors: {
                    ...prevState.errors,
                    fullName: "Full Name is required."
                }
            }));
            isNoError = false;
        }
        if (!email) {
            this.setState(prevState => ({
                errors: {
                    ...prevState.errors,
                    email: "Email Address is required."
                }
            }));
            isNoError = false;
        }
        if (!password) {
            this.setState(prevState => ({
                errors: {
                    ...prevState.errors,
                    password: "Password is required."
                }
            }));
            isNoError = false;
        }

        return isNoError;
    };

    render() {
        const { selectedIndex, fullName, email, password, errors } = this.state;
        const displayValue = data[selectedIndex.row];//4.select选中的值
        const renderOption = (title: string, index: number) => (//5.select option
            <SelectItem key={index} title={title} />
        );

        return (
            <KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
                <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
                    <Layout style={styles.container}>

                        <Select //6. select组件渲染
                            selectedIndex={selectedIndex}
                            onSelect={(index) => this.setState({ selectedIndex: index })}
                            value={displayValue}
                        >
                            {data.map(renderOption)}
                        </Select>

                        <Input
                            value={fullName}
                            label="Full Name"
                            captionTextStyle={styles.captionTextStyle}
                            caption={errors.fullName.length > 0 && errors.fullName}
                            status={errors.fullName.length > 0 ? "danger" : ""}
                            onChangeText={value => this.handleChange("fullName", value)}
                        />
                        <Input
                            value={email}
                            label="Email Address"
                            keyboardType="email-address"
                            autoCapitalize="none"
                            captionTextStyle={styles.captionTextStyle}
                            caption={errors.email.length > 0 && errors.email}
                            status={errors.email.length > 0 ? "danger" : ""}
                            onChangeText={value => this.handleChange("email", value)}
                        />
                        <Input
                            value={password}
                            label="Password"
                            secureTextEntry
                            captionTextStyle={styles.captionTextStyle}
                            caption={errors.password.length > 0 && errors.password}
                            status={errors.password.length > 0 ? "danger" : ""}
                            onChangeText={value => this.handleChange("password", value)}
                        />
                        <Button style={styles.btn} onPress={this.handlePress}>
                            Submit
                        </Button>

                    </Layout>
                </TouchableWithoutFeedback>
            </KeyboardAvoidingView>
        );
    }
}


const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: "center",
        paddingHorizontal: 10,
        width: 600
    },
    captionTextStyle: {
        color: "red"
    },
    btn: {
        marginVertical: 5
    }
});

react-hooks-form 使用示例(包含select)

import React from "react";
import { StyleSheet } from "react-native";
import { Layout, Input, Button, Select, SelectItem, IndexPath, Text } from "@ui-kitten/components";
import { useForm, Controller } from "react-hook-form";

const selectData = [
    'A',
    'B',
    'C',
];

export default function FormComponent() {
    const { control, handleSubmit, errors } = useForm();
    const onSubmit = (data: any) => {
        const postData = {
            "typeFeedBack": displayValue,//select选的值
            "firstName": data.firstName,
            "lastName": data.lastName
        }
        console.log(postData)
    };
    const [selectedIndex, setSelectedIndex] = React.useState(new IndexPath(0));
    const displayValue = selectData[selectedIndex.row];
    const renderOption = (title: string, index: number) => (
        <SelectItem key={index} title={title} />
    );

    return (
        <Layout style={styles.container} level='4'>

            <Controller
                control={control}
                render={() => (
                    <Select
                        style={styles.input}
                        placeholder='Default'
                        value={displayValue}
                        selectedIndex={selectedIndex}
                        onSelect={index => setSelectedIndex(index)}>
                        {selectData.map(renderOption)}
                    </Select>
                )}
                name="typeFeedBack"//这里这个名字不重要,只要随便取一个没有的就可以。因为select我要单独处理
                defaultValue=""//必须,可以为空
            />

            <Controller
                control={control}
                render={({ onChange, onBlur, value }) => (
                    <Input
                        style={styles.input}
                        label='姓氏'
                        onBlur={onBlur}
                        onChangeText={value => onChange(value)}
                        value={value}
                    />
                )}
                name="firstName"
                rules={{ required: true }}
                defaultValue=""
            />
            {errors.firstName && <Text>This is required.</Text>}

            <Controller
                control={control}
                render={({ onChange, onBlur, value }) => (
                    <Input
                        style={styles.input}
                        label='名字'
                        onBlur={onBlur}
                        onChangeText={value => onChange(value)}
                        value={value}
                    />
                )}
                name="lastName"
                rules={{ required: true }}
                defaultValue=""
            />
            {errors.lastName && <Text>This is required.</Text>}

            <Button style={styles.button} onPress={handleSubmit(onSubmit)}>提交</Button>
        </Layout>
    );
}

const styles = StyleSheet.create({
    container: {
        paddingLeft: 20,
        paddingRight: 20,
        paddingBottom: 20
    },
    input: {
        width: 500,
        marginTop: 20
    },
    button: {
        marginTop: 20
    }
})