深入浅出React Native

978-7-115-57242-4
作者: 陈陆扬
译者:
编辑: 赵轩

图书目录:

详情

适合iOS和Android原生平台应用开发者,以及有兴趣加入移动平台开发的JavaScript开发者阅读。适合iOS和Android原生平台应用开发者,以及有兴趣加入移动平台开发的JavaScript开发者阅读。适合iOS和Android原生平台应用开发者,以及有兴趣加入移动平台开发的JavaScript开发者阅读。适合iOS和Android原生平台应用开发者,以及有兴趣加入移动平台开发的JavaScript开发者阅读。

图书摘要

版权信息

书名:深入浅出React Native

ISBN:978-7-115-57242-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


版  权

著     陈陆扬

责任编辑 赵 

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内 容 提 要

本书主要介绍了React Native在iOS/Android系统中的实际运行机制,包含启动原理、基础组件解析、扩展原生能力以及常见场景方案的优化和探索;其中不仅包含JavaScript,也会从iOS/Android的角度去解释实现的机制及原理,以便读者更好地理解移动端开发和选择适合自身场景的方案。

本书适用于对React Native有一定了解的iOS、Android或JavaScript开发者。

前  言

跨平台方案一直是端设备应用开发的热点。一直以来,设计者和开发者都在追求更好的产品体验与降低开发成本、提高响应速度之间寻求平衡。而React Native也是诸多方案之一,面世之初,它以基于React框架的高效开发方式,和优于Hybrid Webview方案的用户体验受到了广大开发者的关注。但随之而来的是人们对其质量、稳定性和性能方面的质疑。甚至在一些场景中,JavaScript开发者并不能完全依赖React Native提供的方式,依旧需要iOS/Android工程师的支持,并不能做到完全屏蔽平台差异。

除了技术难点之外,跨端配合也是实际开发中经常遇到的问题。React Native主要的作用是让JavaScript开发者直接开发原生应用,这对原生开发者来说或多或少挤占了一部分开发空间,双方目标不一致也就很难协同。但整体来看,没有一项技术能够脱离产品和业务单独存活,一项能够降低开发成本的技术一定有其生存的空间。对于大多数的开发者而言,技术价值的体现基本都包括在产品的实现以及对业务结构的提升上。但如果新技术在节省某些日常基础开发时间的同时,增加了自己的学习成本,我们又该持什么样的态度去面对这种变化和挑战呢?

本书面向JavaScript开发者深度分析了React Native实现的原理,包括iOS/Android中常见的开发概念及实现方式,帮助JavaScript开发者更好地理解原生端开发的特点;也面向iOS/Android开发者,解释React Native利用了哪些原生特性,两端之间如何配合及通信,并且说明了从操作系统的角度看待自身平台,对于上层的实现能有更好的支持。本书的所有代码均可在GitHub上的react-native-explorer项目中找到。

最后,感谢杨攀、赵延达、付晓龙、吴泞含4位iOS/Android工程师的大力支持,书中涉及的原生能力的扩展和探索方案的具体实现,均由大家共同完成,没有不同平台开发者的协作,本书也就不可能完成。

第1章 走进React Native

自2013年Facebook团队(后文简称为Facebook)发布React框架后,这种新型的Web开发技术得到了广泛的应用和支持,极大程度上提升了Web开发的效率,并且降低了开发复杂Web App(应用或应用程序)的难度。不过,Web应用毕竟只是互联网开发的一部分,随着移动互联网的发展,越来越多的App开发需求如雨后春笋般冒出,对研发的效率也提出了更高的挑战。那么在Web开发中大放异彩的React框架,是否能够应用到移动端开发中,从而也提升移动应用的开发效率呢?2015年,Facebook发布了React Native。React Native利用React组件化及虚拟DOM的特性,建立了JavaScript到原生视图的映射关系,可以将JavaScript开发者编写的React工程转化成包含iOS及Android的原生应用,并尽可能保留原生应用的体验,同时降低了开发成本。

由于目前React Native仍在不断升级版本,为确保一致性,本书采用的是0.60版本,下文所介绍的React Native均默认为该版本。

1.1 React Native给我们带来了什么

在React Native诞生之前,为了节省成本,开发者在开发App时会选择用WebView封装H5。如果项目需要额外原生能力的话,则可以采用Cordova(PhoneGap)这类框架和工具,也可以达到一定的效果,但这种混合方式始终在体验和性能方面与原生应用存在着一定的差距。而使用React Native开发的项目,其最终生成的代码及项目仍是一个原生应用,只不过将页面渲染、事件交互等工作交给了JavaScript端处理,与纯粹的原生应用相比多了一层与JavaScript端通信的成本。但与使用WebView相比较,使用React Native开发的应用在体验和性能上已经有了很大的提升,并且再也不会受到WebView的限制。

那么,React Native具体给我们提供了哪些开发方式和现成的工具呢?

(1)使用React的方式开发原生应用,包括完整的构建及打包机制。

(2)超过20个基础UI组件,包括基础视图、文字、图片、输入框等,并且支持Flex布局。

(3)手势事件响应体系,用于处理用户交互。

(4)可调用的原生API,包括设备属性、动画和本地存储等。

(5)丰富的社区资源和插件。

其中,React Native官方资源列表(官网more-resources目录下的Awesome React Native)里面罗列了社区生态中关于React Native的资料、工具和组件等一系列开源项目,方便开发者选择适用于当前自身需求的方案。

1.2 React Native的适用场景

那么,在什么场景中适合采用 React Native方案呢?这估计是被讨论次数最多,且永远不会有结论的问题;或者说最后的答案始终会是“根据实际情况”。随着2018年6月Airbnb宣布放弃使用React Native,就有不少言论不看好React Native,大家纷纷觉得应该回归原生开发或者探求新的高性价比开发方式。或许我们始终不会找到能够覆盖所有范围、适用所有场景的跨平台技术方案,但对这些技术方案了解得越多、越深入,你就越需要根据实际场景来选择技术方案。

通常而言,以下几种场景更适合使用React Native开发方案。

(1)需求变动频繁:原生开发流程较长,但诸如营销类的需求则需要对节庆、热点进行快速响应,并且流程环节可能会经常发生变化,React Native的热更新、跨平台机制则可满足这种需求。

(2)内部OA:越来越多公司的OA系统有着移动化的趋势,对于一些交互简单但流程较长的工作场景,例如审批、请假,大家会更偏向于在移动端处理。这类系统对体验的要求相对低一点,使用者又大部分是内部员工,使用React Native开发方案则能极大地降低开发成本。

1.3 搭建React Native环境

搭建环境通常是学习一门新技术时最先遇到的难题,特别是在跨平台开发中,对刚入门的开发者来说是不小的挑战。在搭建环境之前,我们先大致解释一下各平台上的常见概念及其作用,以减少学习者的陌生感。

1.3.1 iOS开发常见概念

iOS提供了从开发语言、IDE到App发布及付费的闭合生态,其中包含了大量平台独有的概念及开发步骤。

1. Xcode

Xcode是开发iOS App的IDE,用于开发、打包及发布iOS App。你可以通过App Store下载最新的Release版本,也可以通过Apple Develop下载beta版。需要注意的是,beta版的Xcode无法提交构建好的App到iTunes。

2. CocoaPods

CocoaPods是开发者必备的iOS依赖管理工具。在安装CocoaPods之前,首先要在本地安装好Ruby环境,之后只需要执行sudo gem install cocoapods命令就可以直接安装了。

3. 模拟器(Simulator)

使用模拟器(Simulator)可以在mac OS上模拟iPhone、iPad。下载Xcode时,最新的模拟器会自动一起安装到本地。如果需要增加机型,可以选择addDevice自行添加。如果需要使用旧版本的iOS,则需要在Xcode > Preferences > Components中自行下载,之后所有支持该系统版本的机型都可以使用该系统。

4. 打包发布

关于iOS App打包发布需要了解两个概念——开发者账号和开发者证书。如果你想把自己的App上传到App Store,需要申请个人开发者账号或企业开发者账号,大致步骤如下。

(1)开发者账号注册:使用Apple ID登录Developer网站,在页面中找到“Join the Apple Developer Program”并按照提示付费和申请,如图1.1所示。

图1.1 Apple开发者首页

(2)证书配置:如果持有开发者账号,可以选择Xcode > Preferences > Accounts,在Accounts界面中直接登录该账号,如图1.2所示,并且在TARGETS选项的Signing & Capabilities标签下勾选Automatically manage signing(自动管理证书,见图1.3),Xcode会自动使用TARGETS选项中配置的Bundle Identifier去申请本机可使用证书。如果未持有开发者账号,则需要在Apple Develop中创建App并在配置证书的计算机上导出p12文件以及描述文件,安装到当前非开发者账号的设备上,并且在TARGETS选项的Signing & Capabilities标签下选择该证书和描述文件。

图1.2 Xcode Accounts配置

图1.3 Xcode Signing & Capabilities

(3)打包:将device选择到非模拟器条目上(真机或空都可以),选择Product > Archive命令进行打包编译,如图1.4所示。打包完成后会自动弹出Organizer(也可以在Window菜单中找到打开该功能的命令);选中刚才打包成功的Archive并单击右侧的Distribute App按钮,按照提示选择打包类型,如图1.5所示。如需上传App Store,则可以选择App Store Connect,直接打包成ipa文件并上传到iTunes。ADHoc和企业证书的打包选项,会影响可安装设备以及证书的选择。

图1.4 打包编译

图1.5 发布

iOS的证书体系相对繁琐,在这里我们简单介绍申请过程中会遇到的一些概念。

Certificates:iOS证书是用来证明iOS App内容合法性和完整性的数字证书。想安装到真机或发布到App Store的App必须经过签名验证,并且其内容是完整、未经篡改的。

Identifiers:App ID,用于标识一个或者一组App,App ID和Xcode中的Bundle ID是一致或匹配的。

Devices:iOS设备。Devices中包含了该账户中所有可用于开发和测试的设备,每台设备使用UDID(设备唯一识别符)来唯一标识。

Profiles:也称Provisioning Profile。一个Provisioning Profile文件包含了上述的所有内容,如证书、App ID、设备,是一个综合描述文件。如果我们要打包或者在真机上运行一个App,首先需要证书来进行签名,用来标识这个App的合法性;其次,需要指明它的App ID,并且验证Bundle ID是否与其一致;最后,在真机调试环节,确认这台设备是否能够运行该App。我们只需选择对应的profile文件就可以进行正常的打包和真机调试,并且这个Provisioning Profile文件会在打包时嵌入.ipa。

上述相关文件均可在Apple开发者网站进行管理,如图1.6和图1.7所示。

图1.6 证书管理入口

图1.7 证书管理首页

1.3.2 Android开发常见概念

Android是基于Linux的开源操作系统,也是当今最主流的智能手机系统之一。

1. Android Studio

Android Studio集成了Android开发所需要的各种工具,并为Android项目优化了目录文件结构的展示。如果开发者之前没有配置过Android开发环境,建议下载集成了Android SDK的Android Studio安装包,可以极大地减少第一次打开Android Studio时下载相关开发工具所需的时间。

2. Gradle

Gradle是一款支持依赖管理、自动化构建、打包、发布和部署的通用性构建工具,主要使用Groovy语言。开发者可以通过应用不同的Gradle插件来构建不同的工程项目。对Android项目来说,用户必须要在工程根目录的build.gradle文件中首先指明适用于Android项目的构建插件——Android Plugin for Gradle,然后添加配置项,所以在Android工程的build.gradle中经常会看见以下代码。

    apply plugin: 'com.android.application' // 指明适用于Android项目的构建插件
      
      android { // 为Android添加配置项
        ......
      }

Android视图是Android Studio的默认文件目录视图模式,如图1.8所示,通过聚合Gradle相关文件,可以减少开发者花费在探索目录上的时间。

图1.8 Gradle相关文件的作用

3. 模拟器

如果开发Android App时没有真机设备,可以通过Android Virtual Device Manager(AVD Manager)添加模拟器,如图1.9所示。

添加不同配置和规格的模拟器需要再次下载相关文件,可能需要一些时间。由于许多Android机型采用了经过各厂商二次开发的深度定制系统,因此最好使用真机进行开发调试,避免出现兼容性问题。

图1.9 Android Studio模拟器位置

4. 打包

使用Android Studio打包项目生成APK(App包),需要先在左边的Build Variants选项中选择打包环境和debug/release版本,然后单击工具栏中的Run按钮,即可将安装包安装至连接的真机或模拟器。打包生成的APK在/app/build/outputs/apk下,如图1.10所示。

图1.10 APK的位置

1.3.3 命令行构建

和大部分框架一样,React Native也具有自己的命令行工具react-native-cli,用于初始化项目。开发者可以通过npm或yarn命令来安装react-native-cli。

    npm install -g react-native-cli

安装好之后,可以直接通过命令来构建React Native项目。

    react-native init FirstRN

也可以指定对应的版本号。

    react-native init FirstRN --version 0.60.5 // 注意版本号需要精确到补丁版本号

执行完成后,你将收获一个最基础的React Native项目,包含了JavaScript所需的依赖以及iOS工程和Android工程。虽然官方文档上说直接进入项目文件夹运行react-native run-ios即可,但根据自身环境不同,所装依赖的版本也有可能不同,会有一定概率无法成功。建议直接使用Xcode和Android Studio来运行项目,里面有比较详尽的报错信息,无论是依赖缺失还是版本不一致,都可以打印出来。

如果一切顺利的话,你应该能看到0.60版本默认页面如图1.11所示,并且在Learn More中包含了一些基本的文档入口,直接单击即可从浏览器打开对应文档,方便查阅。而该页面局部的代码即包含在项目的App.js中,你可以对其进行修改,开发自己的“Hello World”项目。

图1.11 React Native启动页

1.3.4 在现有原生项目中增加React Native环境

利用命令行工具生成工程无疑是最方便快捷的方式,特别是当你需要从零开始搭建项目的时候。但这并不能满足所有的开发场景,有时候可能需要在现有的原生工程中添加React Native环境,将一部分功能交由JavaScript开发。此时,我们要手动在iOS和Android项目中搭建React Native环境。

无论是iOS还是Android项目,在手动搭建React Native环境时都需要通过package.json安装JavaScript依赖。

    {
      "name": "MyReactNativeApp",
      "version": "0.0.1",
      "private": true,
      "scripts": {
        // react服务启动的脚本
        "start": "yarn react-native start"
      },
      "dependencies": {
        // 可以指定版本号
        "react": "16.8.6",
        "react-native": "0.60.5",
      }
    }

然后执行npm install下载所需的依赖包。

1. iOS

安装完JavaScript依赖后,需要在podfile中添加React Native依赖,其中的path需要指定到上述package.json安装的node_modules路径下。

    pod 'React', :path => '../node_modules/react-native', :subspecs => [
      'Core',
      'DevSupport',
      'RCTText',
      'RCTNetwork',
      'RCTWebSocket',
      'RCTImage',
      'ART',
      'RCTActionSheet',
      'RCTGeolocation',
      'RCTPushNotification',
      'RCTSettings',
      'RCTVibration',
      'RCTLinkingIOS',
      'CxxBridge',
      'RCTAnimation',
      'RCTCameraRoll',
      'RCTBlob'
      ]

添加完成后执行以下命令进行安装。

    pod install

此刻iOS工程中已经构建起了React Native环境,下一步就可以使用了。

    // 初始化Bridge,加载JavaScript Bundle
    RCTBridge *bridge=[[RCTBridge alloc] initWithDelegate:self launchOptions: launchOptions];
    // 开始创建视图:这里的moduleName需要与index.js的registerComponent中使用的名称一致
    RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge
                                                 moduleName:@"example"
                                                 initialProperties:nil];
    [vc.view addSubview:rootView]

2. Android

首先在项目根目录的build.gradle文件中修改maven仓库地址配置,主要增加以下代码。

    allprojects {
      repositories {
        ......
        maven {
          // 注意,如果React Native项目根目录不是Android目录的父级目录,则需要更改此处的相对路径
          url("rootDir/....../node_modules/react-native/android")
        }
        maven {
          // 相对路径同上面保持一致
          url("rootDir/....../node_modules/jsc-android/dist")
        }
      }
    }

然后打开module app目录下的build.gradle文件,增加以下配置。

    apply plugin: "com.android.application"
    import com.android.build.OutputFile
    project.ext.react = [
            entryFile   : "index.js", // 指定JavaScript文件入口
            enableHermes: false,  // clean and rebuild if changing
    ]
    // 确保相对路径是准确的React Native node_modules路径
    apply from: "../../node_modules/react-native/react.gradle"
    def jscFlavor = 'org.webkit:android-jsc:+'
    def enableHermes = project.ext.react.get("enableHermes", false);
    android {
        
        splits {
            abi {
                reset()
                enable enableSeparateBuildPerCPUArchitecture
                universalApk false  // If true, also generate a universal APK
                include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
            }
        }
        
        applicationVariants.all { variant ->
            variant.outputs.each { output ->
                def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, 
    "x86_64": 4]
                def abi = output.getFilter(OutputFile.ABI)
                if (abi != null) {  // null for the universal-debug, universal-
    release variants
                    output.versionCodeOverride =
                            versionCodes.get(abi)*1048576+defaultConfig.versionCode
                }
            }
        }
        packagingOptions {
            pickFirst '**/armeabi-v7a/libc++_shared.so'
            pickFirst '**/x86/libc++_shared.so'
            pickFirst '**/arm64-v8a/libc++_shared.so'
            pickFirst '**/x86_64/libc++_shared.so'
            pickFirst '**/x86/libjsc.so'
            pickFirst '**/armeabi-v7a/libjsc.so'
        }
    }
    dependencies {
        ......
        if (enableHermes) {
            // 确保相对路径是准确的React Native node_modules路径
            def hermesPath = "../../node_modules/hermesvm/android/";
            debugImplementation files(hermesPath + "hermes-debug.aar")
            releaseImplementation files(hermesPath + "hermes-release.aar")
        } else {
            implementation jscFlavor
        }
    }
    task copyDownloadableDepsToLibs(type: Copy) {
        from configurations.compile
        into 'libs'
    }
    // 确保相对路径是准确的React Native node_modules路径
    apply from: file("../../node_modules/@react-native-community/cli-platform-
    android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

接下来打开Android项目根目录下的settings.gradle文件,增加以下配置:

    // 确保相对路径是准确的React Native node_modules路径
    apply from: file("../node_modules/@react-native-community/cli-platform-android/
    native_modules.gradle"); applyNativeModulesSettingsGradle(settings)

最后单击Android Studio右侧的Gradle标签,选择项目主模块App(demo>app),单击Refresh Gradle Project按钮即可,如图1.12所示。

图1.12 Gradle管理面板

安装完依赖之后,需要在Android项目的Application.java中手动添加ReactNativeHost。

    public class MainApplication extends Application implements ReactApplication {
        private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
            @Override
            public boolean getUseDeveloperSupport() {
                return BuildConfig.DEBUG;
            }
            @Nullable
            @Override
            protected String getJSBundleFile() {
                // JavaScriptBundle存放路径,若返回null,则使用默认路径,getJSMainModuleName作
    为JavaScript文件名
                String jsBundleFile=getFilesDir().getAbsolutePath()+"/index.
    android.bundle";
                File file = new File(jsBundleFile);
                return file.exists() ? jsBundleFile : null;
            }
            @Override
            protected List<ReactPackage> getPackages() {
                // PackageList 类可以自动帮我们引入 package.json 安装的依赖
                List<ReactPackage> packageList=new PackageList(this).getPackages();
                return packageList;
            }
            @Override
            protected String getJSMainModuleName() {
                return "index"; // 指定 JavaScript 文件入口名称
            }
        };
        @Override
        public ReactNativeHost getReactNativeHost() {
            return mReactNativeHost;
        }
        @Override
        public void onCreate() {
            super.onCreate();
            // 初始化 React Native 依赖的 so 文件
            SoLoader.init(this, /* native exopackage */ false);
        }
    }

这样就搭建完成了Android的React Native环境。

1.4 本章小结

虽然React Native使得JavaScript开发者不用再关心iOS和Android的区别,能够用一套代码开发出接近于原生效果的App,但在实际业务场景中,如果熟悉原生开发的基本流程和相关概念,将大大降低协作成本,并有助于迅速排查问题。

第2章 React Native启动流程及视图解析

上一章介绍了如何搭建一套React Native开发环境,并在原生App(也称应用或应用程序)中看到了我们用JavaScript实现的Hello World视图。在这个过程中React Native究竟经历了哪些步骤,涉及哪些环节,而生成的原生视图又具备什么样的特性,这一章会给大家做一个详细的介绍。了解这些过程,可以帮助我们规避一些无法解决的异常写法,或者制定一些优化策略。

2.1 React Native启动流程

在使用命令行构建React Native(RN)项目后,可以获得两个完整的原生工程,分别位于ios和android目录。React Native在这两个平台上的启动流程有所不同,但可以总结出大致相同的流程,如图2.1所示。

图2.1 React Native启动流程

如图2.1所示,我们之所以将载入的JavaScript文件拆分成开发模式和生产模式,是因为前者通过Server(服务),也就是IP+端口的方式载入JavaScript文件。此时文件尚未合并、压缩,开发者在工具中可以直接看到原始代码,方便后续调试,类似于React或Vue的开发模式;后者则载入了一个已经完成体积优化的JavaScript文件。

由于平台本身存在差异,因此下面我们会按iOS和Android分别描述具体的启动流程及涉及的概念。

2.1.1 iOS启动流程

在默认项目ios目录下,启动流程代码基本都包含在AppDelete.m中:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
      RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions: launchOptions];
      RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                moduleName:@"example"
                                                initialProperties:nil];
      rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
      self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
      UIViewController *rootViewController = [UIViewController new];
      rootViewController.view = rootView;
      self.window.rootViewController = rootViewController;
      [self.window makeKeyAndVisible];
      return YES;
    }

即便你并不熟悉iOS原生代码,也可以根据类型和方法的名字识别出其中的基本元素和方法的效果。React Native在iOS相关的类都是以RCT开头的,以下我们就介绍其中的几个关键类。

RCTBridge:可以理解为React Native在原生代码层的实例,iOS可以使用该实例进行React Native的相关原生操作,例如页面渲染、事件传递、设置载入的JavaScript名称、实例重启等。

RCTRootView:利用JavaScript创建的原生视图,参数moduleName为我们在React Native端使用AppRegistry.registerComponent注册的组件名称,initialProperties为向JavaScript传递的初始参数,在React组件的props中可以接收到。

UIApplication:一个iOS App创建的第一个对象就是UIApplication对象,并且这个对象是单例的。

UIViewController:iOS最基础的视图控制器,我们可以简单地认为iOS App中的每一页都是一个UIViewController。

iOS React Native启动的整体流程大致可以解释成:UIApplication创建后,RCTBridge载入React Native JavaScript文件,根据AppRegistry.registerComponent注入的moduleName和React组件生成RCTRootView,并且挂载到原生视图控制器(UIViewController)。

2.1.2 Android启动流程

相对而言,Android启动流程涉及的概念和类会多一些,默认项目android目录下,有两个文件Main-Application.java和MainActivity.java。

    public class MainApplication extends Application implements ReactApplication {

      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
          return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
          List<ReactPackage> packageList = new PackageList(this).getPackages();
          return packageList;
        }

        @Override
        protected String getJSMainModuleName() {
          return "index";
        }
      };

      @Override
      public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
      }

      @Override
      public void onCreate() {
        super.onCreate();
        SoLoader.init(this, /* native exopackage */ false);
      }
    }

在分析这段流程之前,有两个Android开发的原生概念——Application和Activity,需要和没有Android背景的开发者解释一下。

Application:每个Android App都有一个Application实例,在App开启的时候首先就会将它实例化,并且只实例化一次。

Activity:是Android中一个App组件,提供一个屏幕,用户通过与其交互来完成某项任务。我们在App上看到的每一“页”,就是一个Activity。

MainApplication.java中的ReactNativeHost是React Native实例在Android环境下的容器,等价于iOS中的RCTBridge,也用于设置载入JavaScript文件路径、渲染页面等功能。当前示例中的Application实现了ReactApplication接口,确保在其他上下文中可以通过getReactNativeHost获取当前React Native的实例。

而在MainActivity.java中:

    public class MainActivity extends ReactActivity {

        /**
         * Returns the name of the main component registered from JavaScript.
         * This is used to schedule rendering of the component.
         */
        @Override
        protected String getMainComponentName() {
            return "example";
        }
    }

这里主要重写了getMainComponentName,确定了需要渲染的视图名称。我们如果进一步探究ReactActivity.java源码,会发现getMainComponentName最终被用于ReactDelegate.java:

    mReactRootView.startReactApplication(
          getReactNativeHost().getReactInstanceManager(),
          appKey,    // 这个值即为getMainComponentName
          mLaunchOptions);

该语句最终生成了一个由React Native绘制而成的Android原生视图,然后被挂载到了Activity中。

Android启动流程涉及的内部类较多,如图2.2所示。

图2.2 React Native Android 视图相关类关系

ReactActivity:继承自Android原生视图(AppCompatActivity),主要包含了ReactActivityDelegate这个类的实例。

ReactActivityDelegate:包含了ReactDelegate的实例,大部分操作是通过调用ReactDelegate来完成的。

ReactDelegate:包含当前Activity实例,并创建了ReactRootView实例,通过ReactRootView来实现视图的创建和销毁。

ReactRootView:继承自Android原生视图类FrameLayout,也就是最后通过React Native JavaScript创建的原生视图实例。

为什么Android中使用了这么多代理来处理视图变化?不同于iOS,Android原生基础视图元素相对而言会多一些,例如Activity、Fragment和FragmentActivity等,这些基础视图包含的事件触发机制大同小异,如果直接操作ReactRootView,代码会相对冗余,类似的逻辑会散落在各个基础视图类中,所以利用代理的方式来统一处理ReactRootView,基础视图类只需调用代理的方法即可。

2.1.3 小结

iOS和Android中的基本概念和启动流程大体是一致的,可以分为以下3个步骤。

(1)创建当前平台下的React Native实例。

(2)使用React Native实例和JavaScript端注册的React组件来创建原生视图。

(3)将创建好的React Native原生视图挂载到当前平台屏幕上。

此时对于原生App来说,就是创建了一个原生视图,只不过创建方式稍有不同而已。

2.2 局部渲染React Native

上一小节描述了App整屏利用React Native渲染的情况,但在实际运用中,我们有时只需要在一屏的某个区域使用React Native,例如将一些需要快速响应变化的营销模块通过热更新机制快速上线。那么,如何将刚刚的整屏React Native视图变成局部视图呢?

2.2.1 iOS局部渲染

iOS的局部渲染相对简单,我们只需要修改RCTRootView实例的位置和大小即可。RCTRootView继承于iOS原生视图UIView,是最基础的视图控件,我们可以直接设置其属性frame来控制其位置和大小,具体示例如下。

    // 创建Bridge和实例化RCTRootView
      RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
      RCTRootView *rnRootView = [[RCTRootView alloc] initWithBridge:bridge moduleName: @"example" initialProperties:nil];
      
    // 设置RCTRootView的Frame,左边距0,上边距100,宽度同屏幕宽度,高度为140
      CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
      rnRootView.frame = CGRectMake(0, 100, screenWidth, 140);
      
      // 添加RCTRootView
      [self.view addSubview:rnRootView];

最终效果如图2.3所示。

图2.3 iOS局部渲染

2.2.2 Android局部渲染

Android的局部渲染会涉及另一个原生概念——Fragment,可以把它理解为碎片,它主要有以下5个特点。

(1)Fragment是依赖于Activity的,不能独立存在。

(2)一个Activity里可以有多个Fragment。

(3)一个Fragment可以被多个Activity重用。

(4)Fragment有自己的生命周期,并能接收输入事件。

(5)在Activity运行时可动态地添加或删除Fragment。

React Native目前并不直接提供可实现局部渲染的类,但在master中却存在一个继承于Fragment的ReactFragment类,其中也包含了上节提到的ReactDelegate实例,来实际管理React Native实现的Fragment视图。所以如果需要实现局部渲染,开发者需要自己实现具体的视图逻辑。

在Android环境中的局部渲染就是在原生的Activity中嵌入一个ReactFragment实例,并设置其位置和大小。那么要如何设置Fragment在Activity中的位置呢?这里就需要简单了解下Android中提供布局的xml方案。

我们先可以看这样一个Activity的布局xml示例:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".C_2_2_2.Activity">
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="这是个原生的Activity"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <FrameLayout
            android:id="@+id/fragment_container"
            android:layout_width="0dp"
            android:layout_height="300dp"
            app:layout_constraintBottom_toTopOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

        </FrameLayout>
    </android.support.constraint.ConstraintLayout>

和HTML类似,Android也是利用标签来确定各元素的关系。除了声明XML布局外,我们还需要在Activity中引入对应的XML,使屏幕和布局关联起来。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chapter2_2_2); // 使用xml中的布局绘制页面
        FragmentManager fragmentManager = getSupportFragmentManager();
        if (fragmentManager.findFragmentById(R.id.fragment_container) == null) {
            ReactFragment.Builder fragmentBuilder = new ReactFragment.Builder();
            fragment = fragmentBuilder
                .setComponentName("2_2_1") // 设置JavaScript端注册的ComponentName
                .setLaunchOptions(new Bundle()) // 设置传入的初始参数
                .build();

        } else {
            fragment = (ReactFragment) fragmentManager.findFragmentById(R.id.fragment_container);
        }
        fragmentManager
            .beginTransaction()
            .add(R.id.fragment_container, fragment)
            .commit();
    }

最终效果如图2.4所示。

图2.4 Android局部渲染

2.3 React Native原生视图详解

在上面几节的代码中我们已经看到了React Native为各平台提供的原生UI视图类,那么其中具体会包含哪些属性和方法,我们又可以怎样合理利用它们呢?本节会和大家详细地解读这两个问题。

2.3.1 iOS——RCTRootView

RCTRootView继承自iOS的基础视图UIView,它本质是窗口上的一块区域,也是iOS中所有控件的基类,负责内部区域的渲染、触摸事件、动画,也可以管理本身所有的子视图。

RCTRootView提供了两种初始化的方法,initWithBridge和initWithBundleURL,前者利用已经实例化的RCTBridge生成视图,后者则直接使用JavaScript的本地地址生成RCTBridge后再生成视图。通常情况下,我们会采用initWithBridge以避免重复生成RCTBridge,减少性能消耗。而在初始化的过程中,为了避免白屏,RCTRootView允许我们重写其中的loadingView属性,在React Native视图尚未显示出来时,显示一个等待中的视图,并可设置这个等待视图的移除时间和动画。

比如,我们可以在2.1.1小节的例子中增加一个loading状态。

    // 创建Bridge和实例化RCTRootView
      RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
      RCTRootView *rnRootView = [[RCTRootView alloc] initWithBridge:bridge moduleName: @"example" initialProperties:nil];
      
      // 设置loadingView的控件, 这里使用iOS默认的loading来作为loadingView
      UIActivityIndicatorView *loadingView = [[UIActivityIndicatorView alloc] init WithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
      [loadingView startAnimating];
      rnRootView.loadingView = loadingView;
      
      // 设置loadingView消失的动画持续时间(默认为0.25s)
      rnRootView.loadingViewFadeDuration = 0.3;
      
      // 设置内容加载完成后的loadingView延时展示的时长, 当loadingViewFadeDuration为0时, 此属性不生效
      rnRootView.loadingViewFadeDelay = 10;
      self.view = rnRootView;

另外,在事件处理方面,RCTRootView则提供了cancelTouches,能够在视图内部取消JavaScript的手势事件,避免在某些场景下原生手势和JavaScript手势冲突。

2.3.2 Android——ReactRootView

ReactRootView继承自Android的FrameLayout(帧布局),是最简单的界面布局,默认把元素放在屏幕的左上角,后续添加的元素会覆盖前一个,如果元素一样大,那么同一时刻只能看到最上面的那个元素。

我们可以通过图2.5简单了解Android的布局种类和具体实现的基类。

具体实现布局的基类主要是ViewGroup和View。

ViewGroup:放置View的容器,给childView计算出建议宽、高和测量模式,以及决定childView的位置。

图2.5 Android局部方式及各布局之间的关系

View:根据测量模式和ViewGroup给出的建议宽和高,计算出自己的宽和高,并且在ViewGroup为其指定的区域内绘制自己的形态。

了解了ReactRootView的布局机制后再来分析具体的源码,其内部还包含以下主要属性及方法。

mReactInstanceManager:ReactInstanceManager实例,在startReactApplication调用的时候传入。在大部分应用场景下,我们会使用ReactNativeHost来获取ReactInstanceManager实例,而ReactInstanceManager则是React Native用于管理ReactContext的模块。需要额外说明的是,在Android中,Context(上下文)描述了当前使用者的场景及资源。例如,React Native的ReactContext就包含了CatalystInstance(处理JavaScript和Java接口的相互调用)、UI、原生模块、JavaScript通信的消息队列线程,以及当前活动的Activity等描述当前场景的属性。

mCustomGlobalLayoutListener:实现了ViewTreeObserver.OnGlobalLayoutListener接口,用于监听视图树,当视图树发生变化时,会通知到当前视图。

mJSTouchDispatcher:JSTouchDispatcher实例,负责将视图接收到的Touch事件传递给JavaScript端。

startReactApplication:创建ReactContext,建立视图监听,并绘制React Native组件。

ReactRootView整体的渲染流程,以及相关的类和方法如图2.6所示。

图2.6 React Native Android 视图渲染时序图

2.3.3 视图长度单位

在实际开发中,我们会使用React Native提供的StyleSheet给视图绘制样式,通过StyleSheet.create()创建视图的宽/高、边距等尺寸。

    const styles = StyleSheet.create({
      container: {
        width: 100,
        height: 100,
        paddingHorizontal: 10,
        ......
      },
    });

那么React Native究竟怎么处理这些无单位的尺寸的大小?在此之前,我们先总结一下移动端的各种尺寸单位及其含义。

px:像素,是图像的基本采样单位。

DPI/PPI:都表示像素密度,也就是每英寸(in,1in=25.4mm)的像素点数。

pt:iOS常用单位,独立像素,代表一个绝对长度,不随屏幕像素密度的变化而变化。

dp:Android常用单位,设备无关像素(device independent pixels),这种尺寸单位在不同设备上的物理大小相同。

可以看出,iOS的pt单位和Android的dp单位的定义基本类似,都是为了避免像素密度的影响,让开发者无须关注屏幕密度、物理像素之间的换算关系。所以,React Native也将自己的尺寸单位根据平台转化成了这两种单位。

另外,React Native也提供了PixelRatio对象,开发者可以直接访问设备的像素密度。该对象提供以下几种方法。

get:返回设备的像素密度。常见的设备像素密度如下。

1:Android的mdpi设备(160dpi)。

1.5:Android的hdpi设备(240dpi)。

2: 最常见的设备,包括iPhone 4~8(除p系列),Android的xhdpi设备(320dpi)。

3:iPhone6~8的p系列,iPhone X/XS/XS Max,Android的xxhdpi设备(480dpi)。

3.5:Nexus 6等少数设备。

getFontScale:返回字体缩放比例,如果没有设置字体缩放,它会直接返回设备的像素密度。目前这个函数仅仅在Android设备上实现了,在iOS设备上它会直接返回默认的像素密度。

getPixelSizeForLayoutSize:将一个布局尺寸(dp)转换为像素尺寸(px),且返回的一定是整数。

roundToNearestPixel:返回最接近对齐物理像素的设备独立像素值。通常用于px转化成dp,例如UI设计图是以px为单位,但需要在不同屏幕上等比缩放,那我们就可以设计一个通用转换算法。

    import {Dimensions} from 'react-native';
    const deviceWidthDp = Dimensions.get('window').width;
    // UI设计图的宽度为640
    const widthPx = 640;
    function pxToDp(elePx) {
      return elePx * deviceWidthDp / widthPx;
    }

除了无单位的数值外,React Native也逐渐支持使用百分比(0.42版本之后),可以用于描述width、height、top、left等属性。

    export default class App extends Component {
      render() {
        return (
          <View style={{ height: '100%' }}>
            <View style={{ width: '25%', height: '25%', top: '50%', left: '50%', borderWidth: 1 }}>
              <Text>25%</Text>
            </View> 
          </View>
        );
      }
    }

实际效果如图2.7所示。

图2.7 React Native绝对定位

2.4 React Native布局方式

React Native中常见的布局方式是Flex布局和绝对定位布局。Flex布局是2009年由W3C提出的布局方案,目前在浏览器端已经得到了广泛支持。对Web开发者来说,Flex布局和绝对定位布局很简单;而对原生开发者而言,这两种布局方式可能会有点陌生,我们会在本节对这两种布局方式进行一个具体的说明。

2.4.1 Flex布局

Flex布局是Flexible Box的缩写,意为“弹性布局”,主要分为Flex Container(容器)和Flex item(子元素)。容器默认存在两根轴,即默认水平的主轴和默认垂直的次轴。

下面就是一个Flex布局的常见场景,布局效果如图2.8所示。

    <View style={{ display: 'flex', flexDirection: 'row', paddingHorizontal: 10 }}>
      <View style={{ flex: 1, borderWidth: 1, height: 100 }}>
        <Text>item 1</Text>
      </View>
      <View style={{ flex: 1, borderWidth: 1, height: 100 }}>
        <Text>item 2</Text>
      </View>
      <View style={{ flex: 1, borderWidth: 1, height: 100 }}>
        <Text>item 3</Text>
      </View>
    </View>

图2.8 Flex布局效果

Flex容器主要有以下几个属性。

flexDirection:决定主轴方向,值可以为以下4种。

flexWrap:默认情况下,flex item都排在一条轴线上,flex-Wrap可决定如果一条轴线排不下,如何换行。

图2.9 justifyContent不同值的效果

图2.10 alignItems不同值的效果

作为子元素的item,包含以下属性。

在React Native中,Yoga是用于实现跨平台布局系统的引擎,遵守W3C规范,兼容Flex布局,支持Java、C#、Objective-C和C 4种语言。其底层代码使用C语言编写,性能良好,并且很容易跟其他平台集成。除了React Native之外,Facebook在自己的原生UI渲染框架中也使用了这个引擎,例如Android的Litho,iOS的ComponentKit。

如图2.11所示Yoga提供了一个在线的playground,可用于直接调试Flex布局,修改元素个数、布局容器属性和item属性,并且可以直接生成对应平台(包括React Native、Android和iOS)的代码。

图2.11 Yoga playground

2.4.2 绝对定位

除了Flex布局外,React Native中也包含了Web开发者常用的绝对定位布局,也就是通过设定元素的top、left、right和bottom来确定元素的位置。与Web不同的是,React Native中每个元素的position属性都默认为relative,也就是说每个position设置为absolute的元素,它的top、left、right和bottom都是相对于自己的父元素的位置,例如:

    <View style={{ borderWidth: 1, height: 500 }}>
      <View style={{ position: 'absolute', top: 100, left: 100, width: 100, height: 100, borderWidth: 1 }}>
        <Text>item 1</Text>
      </View>
      <View style={{ position: 'absolute', top: '50%', left: '50%', width: 100, height: 100, borderWidth: 1 }}>
        <Text>item 2</Text>
      </View>
    </View>

在使用绝对定位布局时(见图2.12),通常有以下两个方面需要特别考虑。

图2.12 绝对定位布局

(1)层级关系。在CSS中,通常使用z-index来控制图层的层级关系,z-index数值越大,图层越高。在React Native中,除了zIndex外,还有一个Android属性elevation,也会影响到视图的层级关系。elevation使用了视图高度来决定应用的视图效果。因此同时使用zIndex和elevation的时候,就需要注意其中的层级影响。

没有zIndex,没有elevation:由自身结构决定,结构下面的视图在上层。

有zIndex,没有elevation:zIndex数值大的在上层。

没有zIndex,有elevation:elevation数值大的在上层。

有zIndex,有elevation:以elevation为准。

我们可以参考下面这个例子:

    <View style={{ borderWidth: 1, height: 500 }}>
      <View style={{ position: 'absolute', top: 0, left: 0, width: 100, height: 100, borderWidth: 1, zIndex: 10, backgroundColor: 'white' }}>
        <Text>only zIndex 10</Text>
      </View>
      <View style={{ position: 'absolute', top: 25, left: 25, width: 100, height: 100, borderWidth: 1, zIndex: 15, backgroundColor: 'white' }}>
        <Text>only zIndex 15</Text>
      </View>
      <View style={{ position: 'absolute', top: 50, left: 50, width: 100, height: 100, borderWidth: 1, backgroundColor: 'white', elevation: 15 }}>
        <Text>only elevation 15 </Text>
      </View>
    </View>

具体效果如图2.13所示,左侧为在iOS系统中的显示效果,右侧为在Android系统中的显示效果。

图2.13 iOS和Android中zIndex和elevation的显示效果

(2)overflow(超出显示、溢出)。在开发中,有时需要制作子视图超出父视图的场景,例如在消息提醒之类的图标右上角展示小红点。在CSS中,通过设置overflow:hidden或overflow:visible来控制父视图是否允许显示子视图超出的部分。在Android中,React Native的overflow:visible并不能生效,也就是说子视图不能超出父视图显示。当然,也不是完全没有办法,Android提供了相关属性来支持这样的操作:给对应视图A设置android:clipChildren="false",这样该布局下的子视图B允许自己的子视图C超出自己显示(视图C是视图A的孙子级视图),然后通过React Native提供的创建自定义原生UI组件的方式(第8章会详细讲解)将这个能力提供给JavaScript端,使子视图具备超出父视图显示的能力,实际效果如图2.14所示。

图2.14 Android中实现超出/显示效果

2.5 本章小结

本章主要解释了React Native在各平台上的启动流程,以及最终转化成的原生组件。了解这些过程及原生视图的特点,有助于我们更好地优化流程,采取预加载、缓存等策略缩短首屏渲染时间,以提供更好的用户体验。相对统一的布局方式在一定程度上减少了跨端绘制页面的成本,三端的开发者都采取相同的布局思维绘制UI,也减少了实现方式的差异。

相关图书

树莓派开发实战(第3版)
树莓派开发实战(第3版)
React Native移动开发实战 第3版
React Native移动开发实战 第3版
Flutter App开发:从入门到实战
Flutter App开发:从入门到实战
React Native移动开发实战 第2版
React Native移动开发实战 第2版
App自动化测试与框架实战
App自动化测试与框架实战
30天App开发从0到1:APICloud移动开发实战
30天App开发从0到1:APICloud移动开发实战

相关文章

相关课程