React Native 混合开发 - iOS篇(一)

混合开发有一些使用场景:

  1. 在 Native 项目中加入 React Native 界面. 比如详情页采用 RN 实现.
  2. 在 React Native 项目中加入 Native 界面. 比如详情页采用 Native 实现.
  3. 在 Native 项目中加入 React Native 模块. 比如列表中某个 cell 采用 RN 模块实现.
  4. 在 React Native 项目中加入 Native 模块. 比如地图模块

在 Native 应用中添加 React Native 界面(模块)

主要步骤如下:

  1. 创建一个 React Native 的空项目(不包含 iOS 模块和 Android 模块).
  2. 为已存在的 iOS 项目配置 React Native 所需的依赖.
  3. 创建 index.js 文件, 并添加 React Native 代码. 用于 Native 应用加载 React Native 界面(模块).
  4. 通过 RCTRootView 作为容器, 加载 React Native 组件.
  5. 运行混编项目.
  6. 添加更多 React Native 的组件.
  7. 打包 iOS 项目.

1. 创建一个 React Native 的空项目

有两种方式

    1. 创建并配置 package.json 文件, 通过 yarn 安装 react-native, react 等依赖的方式创建项目
    1. 直接通过 react-native init ProjectName 创建项目, 然后删除 iOS 和 Android 文件内容.

对于方式一, 我们需要创建一个空目录存放所有的项目文件, 然后创建并配置 package.json 文件, 内容如下:

{
  "name": "MyReactNativeApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

然后, 在项目根目录执行 yarn add react-native 添加模块.

此时会有警告信息, 需要我们安装对应版本的 React 模块

yarn add react@16.6.3

yarn 在添加依赖的时候都会将其安装到项目根目录下的 node_modules 文件夹中, 这个目录一般比较大.

我们应该将其添加到 .gitignore 文件中(如果有的话), 保证这个文件夹只保留在本地, 不上传到版本控制系统.

最后结果如下:


另外一种方式创建 React Native 项目, 就比较简单了. 通过如下创建

react-native init ProjectName

不过此方法会产生多余的文件, 需要删除. 下面是创建的 package.json 文件.

2. 为已存在的 iOS 项目配置 React Native 所需的依赖

这一步骤主要用来介绍如何将 React Native 项目与 Native 项目融合.

比如我们有一个 RNHybridiOS 项目, 我们直接将其复制到 RNHybrid 文件夹中, 现在项目的根目录中, 文件结构如下:

在 iOS 项目中我们一般使用 CocoaPods 来管理项目依赖.

RNHybridiOS 文件夹中创建 Podfile 文件

pod init

Podfile 文件中配置依赖

# 对于Swift应用来说下面两句是必须的
platform :ios, '9.0'
use_frameworks!

# target的名字一般与你的项目名字相同
target 'RNHybridiOS' do

  # 'node_modules'目录一般位于根目录中
  # 但是如果你的结构不同,那你就要根据实际路径修改下面的`:path`
  pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # 如果RN版本 >= 0.47则加入此行
    'DevSupport', # 如果RN版本 >= 0.43,则需要加入此行才能开启开发者菜单
    'RCTText',
    'RCTNetwork',
    'RCTWebSocket', # 调试功能需要此模块
    'RCTAnimation', # FlatList和原生动画功能需要此模块
    # 在这里继续添加你所需要的其他RN模块
  ]
  # 如果你的RN版本 >= 0.42.0,则加入下面这行
  pod "yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

  # 如果RN版本 >= 0.45则加入下面三个第三方编译依赖
  pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
  pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
  pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

end

依赖的内容参考自官方文档

这里简单讲一下 Podfile 文件中这些代码的意思. 后续可能会做源码分析.

  • React Native 框架整体是作为 node 模块安装到项目中的, 我们能在 /node_modules/react-native 目录中找到.
  • Podfile 里面关于 React 库这一部分的操作主要就是将相关的库文件的 引用 添加到 React 目录下(原始文件还是在 /node_modules/react-native 目录下). 以供使用

接下来在 iOS 项目根目录执行以下命令, 安装 CocoaPods 依赖.

pod install

安装完依赖, 就需要创建 React Native 代码以供 iOS 项目使用.

3. 创建 index.js 文件, 并添加 React Native 代码

RNHybrid 目录下创建一个 index.js 文件并添加如下代码:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('Welcome', () => App);

向 React Native 注册一个名为 Welcome 的组件.

上述代码引入了一个 App.js 文件. 内容可以如下:

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Button} from 'react-native';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

export default class App extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

这是默认初始化项目的一个初始页面. 显示简单的文本数据.

4. 通过 RCTRootView 作为容器, 加载 React Native 组件.

在上面我们创建了一个 Welcome 组件, 接下来是如何使用这个组件.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        
        if let jsCodeLocation = RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil) {

            let welcomeView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "Welcome", initialProperties: nil, launchOptions: nil)
            // 一定要设置 frame, 默认是 0
            welcomeView?.frame = view.bounds
            
            view.addSubview(welcomeView!)
        }
    }
}

有几点需要注意:

  • initWithBundleURL: 用于指示 js 代码位置, 在开发阶段可以使用RCTBundleURLProvider 的形式生成 jsCodeLocation, 也可以直接指定
let jsCodeLocation = URL(string: "http://localhost:8081/index.bundle?platform=ios")

在发布版本只会使用静态js bundle. 而不是像这样通过本地服务器加载.

  • moduleName: 用于指定 React Native 要加载的 JS 模块名, 也就是上文中所讲的在index.js 中注册的模块名.
  • launchOptions: 主要在 AppDelegate 加载 JS Bundle 时使用,这里传nil就行;
  • initialProperties: 接受一个字典类型的参数来作为 RN 初始化时传递给 JS 的初始化数据.

5. 运行混编项目

在上一步中我们已经加载了在 JS 中注册的 React Native 组件. 下面我们需要启动开发服务器(即 Packager, 它负责实时监测 js 文件的变动并实时打包, 输出给客户端运行), 通过这加载 js 代码.

在混编项目的根目录执行以下命令

npm start

随即可以直接用 Xcode 运行项目, 或者在项目的根目录执行以下

react-native run-ios

第一次运行可能会遇到几个问题:

  1. 由于 React Native 部分的代码是通过本地服务器进行加载的, 并且它是 http 协议传输的, 为了能在 iOS 原生项目中能使用, 我们需要设置 App Transport Security Settings, 让其支持 http 传输.
    在 iOS 项目根目录下, 找到 info.plist 文件.
<key>NSAppTransportSecurity</key>
<dict>
   // 这个是允许所有 http 格式加载
   <key>NSAllowsArbitraryLoads</key>
   <true/>
      
    // 下面是为 localhost 添加白名单,  两种方式任选其一
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
  1. 对于 iOS 项目中的 RCTRootView, 默认加载出来的视图控件的 frame 是 0, 所以我们需要为其设置大小, 否则将不会显示.

6. 添加更多 React Native 的组件

在 index.js 文件中, 我们可以添加多个组件以供 iOS 项目调用.

import {AppRegistry} from 'react-native';
import App from './App';
import App2 from './App2';
import App3 from './App3';

AppRegistry.registerComponent("Welcome", () => App);
AppRegistry.registerComponent("Welcome2", () => App2);
AppRegistry.registerComponent("Welcome3", () => App3);

在 iOS 项目中指定需要加载的组件名称即可.

7. 打包 iOS 项目.

对于发布版本我们不能使用本地服务器加载 js 代码, 所以我们需要将 js 代码打成 bundle, 在 iOS 项目中使用.

  1. 生成 js bundle
react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

说明一下:

react-native, 执行命令
参数以下
bundle, 命令类型
--entry-file 文件入口, 这里指定为 index.js
--platform, 平台, 这里指定 ios
--dev, 是否为开发版本, 这里指定 false
 --bundle-output, bundle 输出路径, 这里指定release_ios/main.jsbundle, 如果没有 release_ios 文件夹需要手动创建
 --assets-dest, 如果有图片资源, 也需要打包, 这里指定在 release_ios/ 文件夹中.
  1. 将 js bundle 和 assets 直接拖到项目根目录.


  2. 在 iOS 项目代码中, 指定 js code 路径.
    在下面代码中, 我们获取到了 react native 界面, 将其用在 App 的根控制器中.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        #if DEBUG         // 调试版本
        
        let jsCodeLocation = RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
        
        #else             // 发布版本, 本地加载 js 代码
        
        let jsCodeLocation = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
        
        #endif
        
        
        if let jsCodeLocation = jsCodeLocation {
            
            let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "NativeDemo_Swift", initialProperties: nil, launchOptions: launchOptions)
            
            window = UIWindow(frame: UIScreen.main.bounds)
            
            let rootVC = UIViewController()
            rootVC.view = rootView
            window?.rootViewController = rootVC
            window?.makeKeyAndVisible()
        }
        
        return true
}

代码里面的 DEBUG 它只是我们自定义的一个标记


我在测试的时候发现, 在项目中导入 main.jsbundle 后, 加载 js 代码的规律如下.


  • 开发模式下, 如果未开启本地服务器, 那么它会默认先去找有没有 main.jsbundle 这个文件, 如果没有, 屏幕会直接黑屏. 即无法加载页面. 如果有, 会优先加载本地的 main.jsbundle 文件.
  • 开发模式下, 本地服务器肯定没有开启, 规律和上面也一样.

在保证 main.jsbundle 文件存在的情况下, 我们可以偷懒, 不需要分两种版本. 直接按照 RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil) 这种来加载. 不过, 这种方式不推荐, 因为需要多做一次是否开服务器的判断, 性能有一点点损耗. 而且, 对于其他更复杂的情况, 我们可能需要 flag 来做判断.

对于 React Native 如何加载 iOS 模块, 在下一篇文章中讲解.

其实只要明白数据的流通原理, 都是一样的.

参考

React Native 中文网 - 在原生项目中集成 RN

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容