React Native
目前我们常用的移动端应用开发方式主要为原生方式、混合开发这两种,其中原生开发运行效率高, 流畅, 用户体验好, 可以做各种复杂的动画效果。不过我们需要去掌握不同开发平台上特定的开发语言与内建的组件框架,譬如在Android 开发中开发者需要掌握Java ,而iOS 开发中开发者需要掌握Objective-C 或者Swift ;并且由于平台之间的独立性,代码无法在其他平台上运行, 无法做到跨平台。而传统混合开发方式则以Cordova 与Ionic 为代表,定义好原生功能与Web 界面之间的协议, 拦截特定的URL Schema 进行原生功能的调用,应用则调用Web 提供的JavaScript 方法,将数据回传给Web 界面。这种方式可以满足一套代码到处运行的目标,不过受限于UIWebView 等容器本身的限制,其性能体验与原生应用不可同日而语。实际上无论哪一种开发方式都致力于解决如下几个问题:找到一种能达到或者接近原生体验的开发方式、找到一种一套代码能在各个平台上运行, 达到代码复用的目的、能够以热更新或者类似的方式进行快速问题修复。
随着React 在Web 领域取得的巨大成功,Facebook 继续推出React Native 以创建接近原生性能的跨平台移动应用,其倡导的Learn Once ,Write Anywhere 的概念同时兼顾了性能与快速迭代的需求。React 的核心设计理念其提供了抽象的、平台无关的组件定义范式,然后通过react-dom 等库将其渲染到不同的承载体上;这些承载可以是服务端渲染中的字符串,或者客户端渲染中的DOM 节点。在React Native 中,我们只需要了解React 组件定义规范与语法,然后利用React Native 这个新的渲染库将界面渲染到原生界面组件中。在未来的客户端开发中,负责与用户交互以及存储这一部分建议采用原生的代码,而对于逻辑控制这边,建议是采用JavaScript 方式实现。
React Native 本质上是用JSX 的语法风格编写原生的应用,它本质上还是跨平台编译性质的,并没有提供完整的类似于WebView 那样的上下文,并且大量的HTML 元素也是不可以直接应用的。React Native 只是借用了HTML 的语法风格,并且提供了JavaScript 与原生的桥接。React Native 使用了所谓的Native Widget APIs 来调用底层的操作系统相关代码,并且处于性能的考虑它会异步批量地调用原生平台接口,其整体架构如下所示:
快速开始
当使用react-native 命令创建新的项目时,调用的即https://github.com/facebook/react-native/blob/master/react-native-cli/index.js 这个脚本。当使用react-native init HelloWorld
创建一个新的应用目录时,它会创建一个新的HelloWorld 的文件夹,包含如下列表:
HelloWorld.xcodeproj/
Podfile
iOS/
Android/
index.ios.js
index.android.js
node_modules/
package.json
React Native 最大的卖点在于(1) 可以使用JavaScript 编写iOS 或者Android 原生程序。(2) 应用可以运行在原生环境下并且提供流畅的UI 与用户体验。众所周知,iOS 或者Android 并不能直接运行JavaScript 代码,而是依靠类似于UIWebView 这样的原生组件去运行JavaScript 代码,也就是传统的混合式应用。整个应用运行开始还是自原生开始,不过类似于Objective-C/Java 这样的原生代码只是负责启动一个WebView 容器,即没有浏览器界面的浏览器引擎。
而对于React Native 而言,并不需要一个WebView 容器去执行Web 方面的代码,而是将所有的JavaScript 代码运行在一个内嵌的JavaScriptCore 容器实例中,并最终渲染为高级别的平台相关的组件。这里以iOS 为例,打开HelloWorld/AppDelegate.m 文件,可以看到如下的代码:
.....................
RCTRootView * rootView = [[ RCTRootView alloc ]
initWithBundleURL : jsCodeLocation
moduleName : @"HelloWorld"
launchOptions : launchOptions ];
.....................
AppDelegate.m 文件本身是iOS 程序的入口,相信每一个有iOS 开发经验的同学都不会陌生,这也是本地的Objective-C 代码与React Native 的JavaScript 代码胶合的地方。而这种胶合的关键就是RCTRootView 这个组件,可以从React 声明的组件中加载到Native 的组件。RCTRootView 组件是一个由React Native 提供的原生的Objective-C 类,可以读取React 的JavaScript 代码并且执行,除此之外,也允许我们从JavaScript 代码中调用iOS UI 的组件。
到这里我们可以看出,React Native 并没有将JavaScript 代码编译转化为原生的Objective-C 或者Swift 代码,但是这些在React 中创建的组件渲染的方式也非常类似于传统的Objective-C 或者Swift 创建的基于UIKit 的组件,并不是类似于WebView 中网页渲染的结果。
这种架构也就很好地解释了为什么可以动态加载我们的应用,当我们仅仅改变了JS 代码而没有原生的代码改变的时候,不需要去重新编译。RCTRootView 组件会监听Command+R
组合键然后重新执行JavaScript 代码。
Virtual Dom 是React 的核心机制之一,对于Virtual Dom 的详细说明可以参考笔者React 系列文章。在React 组件被用于原生渲染之前,Clipboard 已经将React 用于渲染到HTML 的Canvas 中,可以查看render React to the HTML element 这篇文章。对于React Web 而言,就是将React 组件渲染为DOM 节点,而对于React Natively 而言,就是利用原生的接口把React 组件渲染为原生的接口,其大概示意图可以如下:
虽然React 最初是以Web 的形式呈现,但是React 声明的组件可以通过bridge ,即不同的桥接器转化器会将同样声明的组件转化为不同的具体的实现。React 在组件的render 函数中返回具体的平台中应该如何去渲染这些组件。对于React Native 而言,<View/>
这个组件会被转化为iOS 中特定的UIView
组件。
React Native 提供了非常方便的动态调试机制,具体的表现而言即是允许以一种类似于中间件服务器的方式动态的加载JS 代码,即
jsCodeLocation = [ NSURL URLWithString : @"http://localhost:8081/index.ios.bundle" ];
另一种发布环境下,可以将JavaScript 代码打包编译,即npm build
:
jsCodeLocation = [[ NSBundle mainBundle ] URLForResource : @"main" withExtension : @"jsbundle" ];
如果在Xcode 中直接运行程序会自动调用npm start
命令来启动一个动态编译的服务器,如果没有自动启动可以手动的使用npm start
命令,就如定义在package.json 文件中的,它会启动node_modules/react-native/packager/packager.sh 这个脚本。
React Native 中的现代JavaScript 代码
从上文中可以看出,React Native 中使用的是所谓的JSX 以及大量的ES6 的语法,在打包器打包之前需要将JavaScript 代码进行一些转换。这是因为iOS 与Android 中的JavaScript 解释器目前主要还是支持到了ES5 版本,并不能完全识别React Native 中提供的语法或者关键字。当然,并不是说我们不能使用ES5 的语法去编写React Native 程序,只是最新的一些语法细则规范可以辅助我们快速构建高可维护的应用程序。
譬如我们以JSX 的语法编写了如下渲染函数:
render : function ( ) {
return (
< View style = { styles . container } >
< TextInput
style = { styles . nameInput }
onChange = { this . onNameChanged }
placeholder = 'Who should be greeted?' />
< Text style = { styles . welcome } >
Hello , { this . state . name } !< /Text >
< Text style = { styles . instructions } >
To get started , edit index . ios . js
< /Text >
< Text style = { styles . instructions } >
Press Cmd + R to reload ,{ '\n' }
Cmd + Control + Z for dev menu
< /Text >
< /View >
);
}
在JS 代码载入之前,React 打包器需要首先将JSX 语法转化为ES5 的表达式:
render : function ( ) {
return (
React . createElement ( View , { style : styles . container },
React . createElement ( TextInput , {
style : styles . nameInput ,
onChange : this . onNameChanged ,
placeholder : "Who should be greeted?" }),
React . createElement ( Text , { style : styles . welcome },
"Hello, " , this . state . name , "!" ),
React . createElement ( Text , { style : styles . instructions },
"To get started, edit index.ios.js"
),
React . createElement ( Text , { style : styles . instructions },
"Press Cmd+R to reload," , '\n' ,
"Cmd+Control+Z for dev menu"
)
)
);
}
另一些比较常用的语法转换,一个是模块导入时候的结构器,即我们常常见到模块导入:
var React = require ( "react-native" );
var { AppRegistry , StyleSheet , Text , TextInput , View } = React ;
上文中的用法即是所谓的解构赋值,一个简单的例子如下:
var fruits = { banana : "A banana" , orange : "An orange" , apple : "An apple" };
var { banana , orange , apple } = fruits ;
那么我们在某个组件中进行导出的时候,就可以用如下语法:
module . exports . displayName = "Name" ;
module . exports . Component = Component ;
而导入时,即是:
var { Component } = require ( "component.js" );
另一个常用的ES6 的语法即是所谓的Arrow Function ,这有点类似于Lambda 表达式:
AppRegistry . registerComponent ( "HelloWorld" , () => HelloWorld );
会被转化为:
AppRegistry . registerComponent ( "HelloWorld" , function ( ) {
return HelloWorld ;
});
RN 需要一个JS 的运行环境,在IOS 上直接使用内置的javascriptcore ,在Android 则使用webkit.org 官方开源的jsc.so 。此外还集成了其他开源组件,如fresco 图片组件,okhttp 网络组件等。
RN 会把应用的JS 代码( 包括依赖的framework) 编译成一个js 文件( 一般命名为index.android.bundle),, RN 的整体框架目标就是为了解释运行这个js 脚本文件,如果是js 扩展的API ,则直接通过bridge 调用native 方法; 如果是UI 界面,则映射到virtual DOM 这个虚拟的JS 数据结构中,通过bridge 传递到native ,然后根据数据属性设置各个对应的真实native 的View 。bridge 是一种JS 和Java 代码通信的机制,用bridge 函数传入对方module 和method 即可得到异步回调的结果。
对于JS 开发者来说,画UI 只需要画到virtual DOM 中,不需要特别关心具体的平台, 还是原来的单线程开发,还是原来HTML 组装UI(JSX) ,还是原来的样式模型( 部分兼容) 。RN 的界面处理除了实现View 增删改查的接口之外,还自定义一套样式表达CSSLayout ,这套CSSLayout 也是跨平台实现。RN 拥有画UI 的跨平台能力,主要是加入Virtual DOM 编程模型,该方法一方面可以照顾到JS 开发者在html DOM 的部分传承,让JS 开发者可以用类似DOM 编程模型就可以开发原生APP ,另一方面则可以让Virtual DOM 适配实现到各个平台,实现跨平台的能力,并且为未来增加更多的想象空间,比如react-cavas, react-openGL 。而实际上react-native 也是从react-js 演变而来。
对于Android 开发者来说,RN 是一个普通的安卓程序加上一堆事件响应,事件来源主要是JS 的命令。主要有二个线程,UI main thread, JS thread。UI thread 创建一个APP 的事件循环后,就挂在looper 等待事件, 事件驱动各自的对象执行命令。JS thread 运行的脚本相当于底层数据采集器,不断上传数据,转化成UI 事件,通过bridge 转发到UI thread, 从而改变真实的View 。后面再深一层发现,UI main thread 跟JS thread 更像是CS 模型,JS thread 更像服务端,UI main thread 是客户端,UI main thread 不断询问JS thread 并且请求数据,如果数据有变,则更新UI 界面。
利用Create React Native App 快速创建React Native 应用
Create React Native App 是由Facebook 与 Expo 联合开发的用于快速创建React Native 应用的工具,其深受我们在前文介绍的 Create React App 的影响。很多没有移动端开发经验的Web 开发者在初次尝试React Native 应用开发时可能会困扰于大量的原生依赖与开发环境,特别对于Android 开发者而言。而Create React Native App 则能够让用户在未安装Xcode 或者Android Studio 时,即使是在Linux 或者Windows 环境下也能开始React Native 的开发与调试。这一点主要基于我们可以选择将应用运行在Expo 的客户端应用内,该应用能够加载远端的纯粹的JavaScript 代码而不用进行任何的原生代码编译操作。我们可以使用NPM 快速安装命令行工具:
$ npm i -g create -react-native -app
$ create -react-native -app my-project
$ cd my-project
$ npm start
命令行中会输出如下界面,我们可以在Expo 移动端应用中扫描二维码,即可以开始远程调试。我们也可以选择使用Expo 的桌面端辅助开发工具 XDE ,其内置了命令行工具与发布工具,同时支持使用内部模拟器:
除此之外,Expo 还提供了 Sketch 这个在线编辑器,提供了组件拖拽、内建的ESLint 等功能,允许开发者直接在网页中进行快速开发与共享,然后通过二维码在应用内预览。
Expo 支持标准的React Native 组件,目前已经内置了相机、视频、通讯录等等常用的系统API ,并且预置了Airbnb react-native-maps 、Facebook authentication 等优秀的工具库,未来也在逐步将常用的微信、百度地图等依赖作为预置纳入到SDK 中。我们也可以使用 npm run eject
来将其恢复为类似于 react-native init
创建的包含原生代码的初始化项目,这样我们就能够自由地添加原生模块。我们也可以使用Expo 提供的 exp
命令行将项目编译为独立可发布的应用。我们需要使用 npm install -g exp
安装命令行工具,然后配置exp.json 文件:
{
name : "Playground" ,
icon : "https://s3.amazonaws.com/exp-us-standard/rnplay/app-icon.png" ,
version : "2.0.0" ,
slug : "rnplay" ,
sdkVersion : "8.0.0" ,
ios : {
bundleIdentifier : "org.rnplay.exp" ,
},
android : {
package : "org.rnplay.exp" ,
}
}
配置完毕之后在应用目录内使用 exp start
命令来启动Expo 打包工具,然后选择使用 exp build:android
或者 exp build:ios
分别构建Android 或者iOS 独立应用。
除此之外,我们还可以使用 PepperoniAppKit ,或者Deco
开发第一个应用程序
在安装React Native 开发环境时官方就推荐了Flow 作为开发辅助工具,Flow 是一个用于静态类型检查的JavaScript 的开发库。Flow 依赖于类型推导来检测代码中可能的类型错误,并且允许逐步向现存的项目中添加类型声明。如果需要使用Flow ,只需要用如下的命令:
flow check
一般情况下默认的应用中都会包含一个*.flowconfig 文件,用于配置Flow 的行为。如果不希望flow 检查全部的文件,可以在 .flowconfig* 文件中添加配置进行忽略:
[ignore]
.*/node_modules/ .*
最终检查的时候就可以直接运行:
$ flow check
$ Found 0 errors.
React Native 支持使用Jest 进行React 组件的测试,Jest 是一个基于Jasmine 的单元测试框架,它提供了自动的依赖Mock ,并且与React 的测试工具协作顺利。
npm install jest-cli
可以将test 脚本加入到package.son 文件中:
{
...
"scripts" : {
"test" : "jest"
}
...
}
直接使用npm test 命令直接运行jest 命令,下面可以创建tests 文件夹,Jest 会递归搜索tests 目录中的文件,这些测试文件中的代码如下:
"use strict" ;
describe ( "a silly test" , function ( ) {
it ( "expects true to be true" , function ( ) {
expect ( true ). toBe ( true );
});
});
而对于一些复杂的应用可以查看React Native 的官方文档,以其中一个getImageSource 为例:
**
* Taken from https :
* /
'use strict' ;
jest . dontMock ( '../getImageSource' );
var getImageSource = require ( '../getImageSource' );
describe ( 'getImageSource' , () => {
it ( 'returns null for invalid input' , () => {
expect ( getImageSource (). uri ). toBe ( null );
});
...
});
因为Jest 是默认自动Mock 的,所以需要对待测试的方法设置dontMock.