原理与机制

Application Exploration(应用探究)

Architecture(应用架构)

当使用 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 的扩展

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 Native behaves much like React, but can render to many different targets.

虽然 React 最初是以 Web 的形式呈现,但是 React 声明的组件可以通过bridge,即不同的桥接器转化器会将同样声明的组件转化为不同的具体的实现。React 在组件的 render 函数中返回具体的平台中应该如何去渲染这些组件。对于 React Native 而言,<View/>这个组件会被转化为 iOS 中特定的UIView组件。

载入 JavaScript 代码

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;
});
上一页