likes
comments
collection
share

聊聊ReactNative的那些事儿

作者站长头像
站长
· 阅读数 17

背景:上手技术栈为RN的项目已有半年时间,之前忙于业务一直没有时间总结和思考,最近特意抽空进行了整理,希望可以带给大家一些收获,可以与大家一起交流学习 先来看看目录

聊聊ReactNative的那些事儿

React Native 介绍

React Native 是一个由 Facebook 于 2015 年 9 月发布的一款开源的 JavaScript 框架,它可以让开发者使用 JavaScript ****和 React 来开发跨平台的移动应用。它既保留了 React 的开发效率,又同时拥有 Native 应用的良好体验,加上 Virtual DOM 跨平台的优势,实现了真正意义上的:Learn Once,Write Anywhere.

ReactNative特点

跨平台

React Native 使用了 Virtual DOM(虚拟 DOM),只需编写一套代码,便可以将代码打包成不同平台的 App,极大提高了开发效率,并且相对全部原生开发的应用来说,维护成本也相对更低。

上手快

相比于原生开发,JavaScript 学习成本低、语法灵活。允许让 Web 开发者更多地基于现有经验开发 App。React Native 只需使用 JavaScript 就能编写移动原生应用,它和 React 的设计理念是一样的,因此可以毫不夸张地说:你如果会写 React,就会写 React Native!

原生体验

由于 React Native 提供的组件是对原生 API 的暴露,虽然我们使用的是 JavaScript 语言编写的代码,但是实际上是调用了原生的 API 和原生的 UI 组件。因此,体验和性能足以媲美原生应用

React Native 架构与原理

聊聊ReactNative的那些事儿

  • 绿色的是我们应用开发的部分。我们写的代码基本上都是在这一层
  • 蓝色代表公用的跨平台的代码和工具引擎,一般我们不会动蓝色部分的代码

      • 黄色代码平台相关的代码,做定制化的时候会添加修改代码。不跨平台,要针对平台写不同的代码。iOS写OC, android写java,web写js. 每个bridge都有对应的js文件,js部分是可以共享的,写一份就可以了
  • 红色部分是系统平台的东西。红色上面有一个虚线,表示所有平台相关的东西都通过bridge隔离开来了
  • 大部分情况下我们只用写绿色的部分,少部分情况下会写黄色的部分。你如果对基础架构和开源感兴趣,你可以写蓝色部分,然后尝试给那些大的开源项目提交代码。红色部分是独立于React Native的

React Native、React和JavascriptCore的关系

React

React是一个纯JS库,它封装了一套Virtual Dom的概念,实现了数据驱动编程的模式,为复杂的Web UI实现了一种无状态管理的机制, 标准的HTML/CSS之外的事情,它无能为力。调用原生控件,驱动声卡显卡,读写磁盘文件,自定义网络库等等,这是JS/React无能为力的

ReactNative

RN的原生代码(Timer和用户事件)驱动JS Engine, 然后JS Engine解析执行React或者相关的JS代码,然后把计算好的结果返回给Native code. 然后, Native code 根据JS计算出来的结果驱动设备上所有能驱动的硬件。重点,所有的硬件。也就是说,在RN这里,JS代码已经摆脱JS Engine(浏览器)的限制,可以调用所有原生接口啦

JavascriptCore

JavaScriptCore负责 bundle 产出的 JS 代码的解析和执行。

ReactNative的渲染与布局

聊聊ReactNative的那些事儿

Bridge(打通RN任督二脉的关键组件)

用来翻译ReactJS的绘制指令给原生组件进行绘制,同时把原生组件接收到的用户事件反馈给

ReactJS

Bridge的作用就是给RN内嵌的JS Engine提供原生接口的扩展供JS调用。

所有的本地存储、图片资源访问、图形图像绘制、摄像头,指纹,3D加速、网络访问、震动效果、NFC、原生控件绘制、地图、定位、通知等都是通过Bridge封装成JS接口以后注入JS Engine供JS调用。理论上,任何原生代码能实现的效果都可以通过Bridge封装成JS可以调用的组件和方法, 以JS模块的形式提供给RN使用

Bridge做了什么

  1. 创建js线程
  2. 初始化原生模块,其中包括UIManager模块
  3. js线程初始化native-js bridge,后台线程读取 js bundle,两者都完成(barrier)后才开始加载bundle

ReactNative与js的通信

React Native 中的 Native 模块如何暴露给 JS ?(暂时以 ios 为例)

// iOS端原生代码

#import <Foundation/Foundation.h>

#import <React/RCTBridgeModule.h>



@interface NativeLogModule : NSObject<RCTBridgeModule>



@end



#import "NativeLogBridge.h"



@implementation NativeLogModule

RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(nativeLog:(id)obj) {

  NSLog(@"%@",obj);

}

@end



// ---------------------ReactNative需要关注的地方!!!!!!!------------------

import { NativeModules } from 'react-native'; 

NativeModules.NativeLogModule.nativeLog('从JS侧来的消息'); 

可以看到,上面的代码中使用了RCT_EXPORT_MODULE() 宏将 Native 类以 module 的形式暴露给了 JS,然后使用了RCT_EXPORT_METHOD将 Native 的方法暴露给 JS,最后在 JS 侧直接引用一个模块,便可以直接调用暴露的方法与 Native 通信。

Native 调用 JS

在 React Native 里面,JS 的方法可以通过 global.batchedBridge.callFunctionReturnFlushedQueue 这个方法进行调用,所以在 Native 侧,只需将 React Native 里面的 global.batchedBridge 对象中的方法和 Native 侧的 JSIExecutor 方法进行绑定(本质上 Native 指针指向 JS 函数)

JSIExecutor::callFunctionReturnFlushedQueue_ = global.batchedBridge.callFunctionReturnFlushedQueue

RCTRootView

是什么?

原生项目如果想用 React Native,那么就需要用到 RCTRootView,它是 React Native 加载的地方,可以把它看作是一个容器。

RCTRootView做的事情

加载jsBundle,并且初始化JS运行环境.

对应的原生模块也被加载进来,然后js loop开始运行。 js loop的驱动来源是Timer和Event Loop(用户事件). js loop跑起来以后应用就可以持续不停地跑下去了。

初始化RootView时,会先在js线程上检查js bundle是否加载完成——加载完成就直接运行rn app了,未完成就会将“运行rn app”这个任务加到等待队列里,等加载完成后再执行等待队列中的任务,再之后才在主线程发出 js bundle 加载完成的事件。

ReactNative与React的渲染区别

在浏览器里面,JavaScript 可以调用 DOM API 去完成创建 UI 的工作

而在 React Native 里面,是通过 UI Manager 来创建视图的,基于 Virtual DOM ,React Native 把不同平台创建视图的逻辑封装了一层,不同平台通过 Bridge 调用 UI Manager 来创建不同的 Native 视图。

js中我们使用div和p标签,而在reactNative中我们使用view和text

聊聊ReactNative的那些事儿

一个<View/>标签,先被转写为ReactElement,然后得到fiber节点,再在遍历fiber时通过UIManager经由bridge创建对应的原生视图、设置视图的父子关系,并最终在布局线程中计算布局、主线程更新布局。

聊聊ReactNative的那些事儿

ReactNative的热更新

React Native 的产物 bundle 文件,本质上是 JS 的逻辑代码加上 React Native 的 Runtime 的集合,所以在应用一启动的时候就会去获取 bundle 文件,之后解析 bundle 文件,最后再由 JS Engine 去执行具体的业务代码逻辑。这就可以允许开发者在云端去更新 bundle 文件,然后应用启动的时候获取最新的 bundle 文件,这一整个流程下来就实现了热更新。

聊聊ReactNative的那些事儿

ReactNative的实践

环境搭建(可参考官网

iOS 所需环境: Node、Watchman、Xcode、CocoaPods。

Android 所需环境: Node、Watchman、JDK 和 Android Studio。

创建新项目

创建一个名为"AwesomeProject"的新项目

npx react-native init AwesomeProject


运行npm i

用 React Native 官方脚手架生成的项目目录是这样的:

├── android # Android runtime

├── iOS # iOS runtime

├── node_modules # 项目依赖

├── app.json # 描述app 信息

├── App.js # React Native 代码

├── index.js # 入口文件,这个文件里面调用了AppRegistry.registerComponent,注册React Native组件提供给Native

├── package.json # 依赖信息和版本信息

├── ...

├── ...

├── ...

React-Native 使用IOS模拟器运行本地项目

  1. 确保Mac电脑上存在 xcode,没有安装请去AppStore中下载!
  2. 启动 Xcode,并在Xcode | Preferences | Locations菜单中检查一下是否装有某个版本的Command Line Tools
  3. Xcode中安装IOS模拟器
  4. 安装 CocoaPods
  • CocoaPods是用 Ruby 编写的包管理器(可以理解为针对 iOS 的 npm)。从 0.60 版本开始 react native 的 iOS 版本需要使用 CocoaPods 来管理依赖。你可以使用下面的命令来安装 CocoaPods。CocoaPods的版本需要 1.10 以上
brew install cocoapods

或者 sudo gem install cocoapods --source http://rubygems.org
  1. 进入项目 的 ios文件夹中,执行安装命令
pod install
  1. 执行 运行命令
yarn ios

很快就应该能看到 iOS 模拟器自动启动并运行你的项目,恭喜你,你的第一个ReactNative项目创建成功了🎉🎉🎉

项目演示

聊聊ReactNative的那些事儿

尝试修改一些东西查看热更新

展示一个helloWorld(演示)

创建导航(navigation官网

npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

利用@react-navigation/native-stack中的createNativeStackNavigator初始化导航实例

import * as React from 'react';

import { NavigationContainer } from '@react-navigation/native';

import { createNativeStackNavigator } from '@react-navigation/native-stack';



const Stack = createNativeStackNavigator();



const MyStack = () => {

  return (

    <NavigationContainer>

      <Stack.Navigator>

        <Stack.Screen

          name="Home"

          component={HomeScreen}

          options={{ title: 'Welcome' }}

        />

        <Stack.Screen name="Profile" component={ProfileScreen} />

      </Stack.Navigator>

    </NavigationContainer>

  );

};

增加底部tab(reactnavigation.org/docs/tab-ba…

npm install @react-navigation/bottom-tabs

利用@react-navigation/bottom-tabs中的createBottomTabNavigator创建tab实例

import * as React from 'react';

import { Text, View } from 'react-native';

import { NavigationContainer } from '@react-navigation/native';

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';



function HomeScreen() {

  return (

    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>

      <Text>Home!</Text>

    </View>

  );

}



function SettingsScreen() {

  return (

    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>

      <Text>Settings!</Text>

    </View>

  );

}



const Tab = createBottomTabNavigator();



export default function App() {

  return (

    <NavigationContainer>

      <Tab.Navigator>

        <Tab.Screen name="Home" component={HomeScreen} />

        <Tab.Screen name="Settings" component={SettingsScreen} />

      </Tab.Navigator>

    </NavigationContainer>

  );

}

一个基本的底部tab被我们创建出来了✌️✌️✌️

聊聊ReactNative的那些事儿

使用 Android 模拟器运行项目(有兴趣的同学可以了解下)

  1. 安装 Android Studio及安装sdk官方文档
  2. 在 Android Studio中先创建一个虚拟设备。点击"Create Virtual Device...",然后选择所需的设备类型并点击"Next",完成
  3. 编译并运行 React Native 应用
yarn android

使用真机(开发中推荐此方法)

使用代理(推荐)
  • 确保Android Studio及相应的sdk版本勾选上官方文档
  • 确保手机与电脑处于同一个局域网,打开charles,使手机代理到电脑上
  • 手机点击设置,手动代理,填上电脑的ip及端口8888,电脑打开charles
  • 使用AndroidStudio打开android目录,并进行build,获取apk文件,传输到手机端

聊聊ReactNative的那些事儿

  • 编译并运行RN项目
yarn android/ios
  • 手机下载并打开编译好的apk文件,将拉取电脑端代码
usb数据线连接(不推荐)
  1. 确保手机开发者模式打开,连接电脑时,确认手机弹出连接选项,选择数据传输
  2. 编译并运行RN项目,将在手机下载可运行的apk文件(如手机禁止不明来源安装,需要取消设置)
yarn android/ios

css 相关

基础布局
  1. React Native 中的 Flexbox 的工作原理和 web 上的 CSS 基本一致,当然也存在少许差异。首先是默认值不同:flexDirection的默认值是column而不是row,而flex也只能指定一个数字值。

聊聊ReactNative的那些事儿

  1. JavaScript 来写样式

style属性可以是一个普通的 JavaScript 对象。你还可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承。

import React from 'react';

import { StyleSheet, Text, View } from 'react-native';



const LotsOfStyles = () => {

    return (

      <View style={styles.container}>

        <Text style={styles.red}>just red</Text>

        <Text style={styles.bigBlue}>just bigBlue</Text>

        <Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>

        <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>

      </View>

    );

};



const styles = StyleSheet.create({

  container: {

    marginTop: 50,

  },

  bigBlue: {

    color: 'blue',

    fontWeight: 'bold',

    fontSize: 30,

  },

  red: {

    color: 'red',

  },

});



export default LotsOfStyles;

聊聊ReactNative的那些事儿

使用渐变色,利用< LinearGradient>标签
  1. CSS 中使用渐变只需要用 linear-gradient 就可以,但是在 React-Native 项目中却不可以直接通过属性去实现,需要安装一个 react-native-linear-gradient 才可实现。
start:{ x: number, y: number }

end:{ x: number, y: number }

start 就是渐变开始的位置,x、y 范围是 0 - 1 

end同理



例如:

start: { x: 0.3, y: 0.4 } 渐变是从 左侧30%, 上部 40% 开始

end: { x: 0.7, y: 0.8 } 渐变是从 左侧70%, 上部 80% 结束
import React from 'react';

import {Text, StyleSheet, View, Dimensions} from 'react-native';

import LinearGradinet from 'react-native-linear-gradient';



export default class Home extends React.Component {

  render() {

    return (

      <View style={styles.content}>

        <Text>首页</Text>

        <LinearGradinet

          start={{x: 0, y: 0}}

          end={{x: 1, y: 0}}

          colors={['#9b63cd', '#e0708c']}

          style={{width: 200, height: 200}}

        />

      </View>

    );

  }

}



const styles = StyleSheet.create({

  content: {

    display: 'flex',

    flexDirection: 'column',

    justifyContent: 'center',

    alignItems: 'center',

    width: Dimensions.get('window').width,

    height: Dimensions.get('window').height,

  },

});

聊聊ReactNative的那些事儿

基础动画

先创建一个 Animated.Value,将它连接到动画组件的一个或多个样式属性,然后使用Animated.timing()通过动画效果展示数据的变化: 透明度、平移

import React from 'react';
import { StyleSheet, View, Text, Animated, TouchableOpacity } from 'react-native';

class App extends React.Component {
    state = {
        fadeInOpacity: new Animated.Value(0), // 透明度开始设置为0
    };

    render() {
        return (
            <>
                <View style={styles.content}>
                    <Animated.View
                        (代码选其一)
                        // 透明度
                        style={{ ...styles.square, opacity: this.state.fadeInOpacity }}
                        // 平移
                        style={{
                            ...styles.square,
                            transform: [{ translateX: this.state.translateX }],
                        }}
                    />
                    <TouchableOpacity
                        style={styles.button}
                        onPress={() => {
                         //(代码选其一)
                         // 透明度
                         Animated.timing(this.state.fadeInOpacity, {
                                toValue: 1, // 透明度开始设置为1
                                duration: 3000, // 动画时长
                                useNativeDriver: false,
                            }).start();
                           // 平移 
                           
                           Animated.timing(this.state.translateX, {
                                toValue: 100, // 最终平移的值
                                duration: 3000, // 动画时长
                                useNativeDriver: false,
                            }).start();
                        }}>
                        <Text style={{ color: '#fff' }}>执行动画</Text>
                    </TouchableOpacity>
                </View>
            </>
        );
    }
}

const styles = StyleSheet.create({
    content: {
        width: '100%',
        height: '100%',
        backgroundColor: '#fff',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
    },
    square: {
        width: 100,
        height: 100,
        backgroundColor: 'red',
    },
    button: {
        paddingLeft: 12,
        paddingRight: 12,
        paddingTop: 14,
        paddingBottom: 14,
        backgroundColor: '#409eff',
        borderRadius: 5,
        marginTop: 20,
    },
});

export default App;


插值函数

interpolate()

插值是指将一定范围的输入值映射到另一组不同的输出值,一般我们使用线性的映射,但是也可以使用 easing 函数

一个简单的将范围 0-1 转换为范围 0-100 的映射操作是:

value.interpolate({  inputRange: [0, 1],  outputRange: [0, 100]});

interpolate()还支持定义多个区间段落,常用来定义静止区间等

value.interpolate({  inputRange: [-300, -100, 0, 100, 101],  outputRange: [300, 0, 1, 0, 0]});

interpolate()还支持到字符串的映射,从而可以实现颜色以及带有单位的值的动画变换。例如你可以像下面这样实现一个旋转动画:

旋转

state = {
        degree: new Animated.Value(0), // 旋转开始的度数0
    };
 render() {
        // 用插值函数映射 0-1的值 映射 成 0 - 360 度
        const realDeg = this.state.degree.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg'],
        });
        return (
            <>
                <View style={styles.content}>
                    <Animated.View
                        style={{
                            ...styles.square,
                            transform: [{ rotate: realDeg }],
                        }}
                    />
                    <TouchableOpacity
                        style={styles.button}
                        onPress={() => {
                            Animated.timing(this.state.degree, {
                                toValue: 1, // 最终值 为1,这里表示最大旋转 360度
                                duration: 3000, // 动画时长
                                useNativeDriver: false,
                            }).start();
                        }}>
                        <Text style={{ color: '#fff' }}>执行动画</Text>
                    </TouchableOpacity>
                </View>
            </>
        );
    }
组合动画

组合动画

import React from 'react';
import {
    StyleSheet,
    View,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
} from 'react-native';

class App extends React.Component {
    state = {
        scale: new Animated.Value(0), // 缩放初始值
        degree: new Animated.Value(0), // 旋转开始的度数0
    };

    // 创建动画
    createAnimation(value, duration, easing = Easing.ease, delay = 0) {
        return Animated.timing(value, {
            toValue: 1,
            duration,
            easing, // easing: 缓动函数。 默认为Easing.inOut(Easing.ease)
            delay,
            useNativeDriver: false
        });
    }

    render() {
        // 用插值函数映射 0-1的值 映射 成 0 - 360 度
        const realDeg = this.state.degree.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg'],
        });
        return (
            <>
                <View style={styles.content}>
                    <Animated.View
                        style={{
                            ...styles.square,
                            transform: [{ scale: this.state.scale }, { rotate: realDeg }],
                        }}
                    />
                    <TouchableOpacity
                        style={styles.button}
                        onPress={() => {
                            // 同时开始动画数组中的全部动画
                            Animated.parallel([
                                this.createAnimation(this.state.scale, 3000),
                                this.createAnimation(this.state.degree, 3000),
                            ]).start();
                            // 按照顺序执行动画中的所有数组,前一个动画执行完成后,后面一个动画再开始,如果当前动画被中止,则后面的动画不会继续执行
                            // Animated.sequence([
                            //     this.createAnimation(this.state.scale, 3000),
                            //     this.createAnimation(this.state.degree, 3000),
                            // ]).start();
                            // 参数是一个延迟时间和一个动画数组,前一个动画开始之后,延迟一定时间,后一个动画开始执行
                            // Animated.stagger(9000, [
                            //     this.createAnimation(this.state.scale, 3000),
                            //     this.createAnimation(this.state.degree, 3000),
                            // ]).start();
                        }}>
                        <Text style={{ color: '#fff' }}>执行动画</Text>
                    </TouchableOpacity>
                </View>
            </>
        );
    }
}

const styles = StyleSheet.create({
    content: {
        width: '100%',
        height: '100%',
        backgroundColor: '#fff',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
    },
    square: {
        width: 100,
        height: 100,
        backgroundColor: 'red',
    },
    button: {
        paddingLeft: 12,
        paddingRight: 12,
        paddingTop: 14,
        paddingBottom: 14,
        backgroundColor: '#409eff',
        borderRadius: 5,
        marginTop: 20,
    },
});

export default App;

组件通信

父向子组件通信,通过props传值或者ref获取子组件的方法
子组件向父组件通信,回调函数
非父子组件通信
社区是以下几种方式
  • 全局时间订阅系统(EventEmitter)

  • 单向数据流框架(Flux系)

flux、reflux、alt、redux

  • 双向数据流框架

mobx

数据存储

页面间的简单参数传递我们可以使用url,但当数据太多的时候我们就要考虑其他方式

AsyncStorage是一个简单的、异步的、持久化的 Key-Value 存储系统,它对于 App 来说是全局性的。可用来代替 LocalStorage。

import AsyncStorage from '@react-native-community/async-storage';
保存数据:
_storeData = async () => {

  try {

    await AsyncStorage.setItem(

      '@MySuperStore:key',

      'I like to save it.'

    );

  } catch (error) {

    // Error saving data

  }

};
读取数据:
_retrieveData = async () => {

  try {

    const value = await AsyncStorage.getItem('@MySuperStore:key');

    if (value !== null) {

      // We have data!!

      console.log(value);

    }

  } catch (error) {

    // Error retrieving data

  }

};

可能会遇到的问题是localStorage的空间是有限的

小结

React Native 的优点与不足

优点:

  1. 开发比较友好,平滑过度,会react就会reactNative
  2. 可选择的原生功能很多,例如开启摄像头,扫描条形码,打开相机等

不足:

  1. 调试方面不如web完善,只能通过log调试页面
  2. 整体开发体验不如iOS原生开发,比如动画效率,性能是不如原生的

相关文章借鉴:juejin.cn/post/691645…