对于一个新的工程,可以直接使用React Native进行开发,但是对于现有的项目如果全面改造将会是一项巨大的工程。好在RN提供了方式为我们接入现有工程。同时,对于Android中的一些组件,RN不一定有已经继承的实现方式,我们可以通过实现ReactMethod的方式实现RN使用Android原生组件。
1. 接入React Native
1. RN开发环境准备及Android原生项目
这里不再赘述,按照搭建开发环境便可以轻松实现对于RN环境的搭建。
原生项目相信大家也一定会建立的~
2. 安装 JavaScript 依赖包
在项目根目录下创建一个名为package.json
的空文本文件,并在其中填入以下内容
{
"name": "RNHybridDemo",//可自定义
"version": "0.0.1",//可自定义
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start" //必填
}
}
其中scripts
中是用于启动 packager 服务的命令。
接下来我们使用 yarn 或 npm(两者都是 node 的包管理器)来安装 React 和 React Native 模块(推荐使用yarn)。打开一个终端/命令提示行,进入到项目根目录(即包含有 package.json 文件的目录),然后运行下列命令来安装:
$ yarn add react-native
这样默认会安装最新版本的 React Native,同时会打印出类似下面的警告信息:
warning “react-native@0.52.2” has unmet peer dependency “react@16.6.1”.
这是正常的现象,我们还需要安装指定的React
$ yarn add react@16.6.1
注意必须严格匹配警告信息中所列出的版本,高了或者低了都不可以。
最后,所有 JavaScript 依赖模块都会被安装到项目根目录下的node_modules/
目录中(这个目录我们原则上不复制、不移动、不修改、不上传,随用随装)。把node_modules/
目录记录到.gitignore
文件中(即不上传到版本控制系统,只保留在本地)。
3. 把 React Native 添加到你的应用中
首先我们配置gradle。在你的app中build.gradle
文件中添加 React Native 依赖:
dependencies {
...
implementation "com.facebook.react:react-native:+"
}
如果想要指定特定的 React Native 版本,可以用具体的版本号替换 +,当然前提是你从 npm 里下载的是这个版本。
接下来是在项目的build.gradle
文件中为 React Native 添加一个 maven 依赖的入口,必须写在 allprojects
代码块中:
allprojects {
repositories {
...
maven {
url "$rootDir/node_modules/react-native/android"
}
}
}
这里需要主要一定是上述的,在RN教程中是
url "$rootDir/../node_modules/react-native/android"
,但是这样会导致在gradle编译时找不到我们所需要的RN包。这里的坑我呆了半天才爬出来?
4. 配置权限
接着,在 AndroidManifest.xml 清单文件中声明网络权限:
<uses-permission android:name="android.permission.INTERNET" />
如果需要访问 DevSettingsActivity 界面(即开发者菜单),则还需要在 AndroidManifest.xml 中声明:
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
开发者菜单一般仅用于在开发时从 Packager 服务器刷新 JavaScript 代码,所以在正式发布时你可以去掉这一权限。
5. 代码继承
首先是在项目根目录中创建一个index.js文件,index.js是 React Native 应用在 Android 上的入口文件。而且它是不可或缺的!它可以是个很简单的文件,简单到可以只包含一行require/import导入语句。
其实是实现RN在原生页面的展现。如果你的应用会运行在 Android 6.0(API level 23)或更高版本,请确保你在开发版本中有打开悬浮窗(overlay)权限。
这里直接放出页面的代码吧(Kotlin),记得在Manifest中注册页面:
class MyReactActivity : Activity(), DefaultHardwareBackBtnHandler {
private lateinit var mReactRootView: ReactRootView
private lateinit var mReactInstanceManager: ReactInstanceManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestOverlayPermission()
initView()
}
private fun initView() {
mReactRootView = ReactRootView(this)
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(application)
.setBundleAssetName("index.android.bundle")//设置加载bundle文件在asserts中的路径
.setJSMainModulePath("index")//在开发模式时入口js文件名称,仅在开发模式时起作用
.addPackage(MainReactPackage())//自定义Package
.setUseDeveloperSupport(BuildConfig.DEBUG)//是否是Debug模式
.setInitialLifecycleState(LifecycleState.RESUMED)//初始化React时的生命周期
.build()
// 注意这里的MyReactNativeApp必须对应“index.js”中的
// “AppRegistry.registerComponent()”的第一个参数
mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null)
setContentView(mReactRootView)
}
private fun requestOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
)
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
// SYSTEM_ALERT_WINDOW permission not granted
}
}
}
mReactInstanceManager.onActivityResult(this, requestCode, resultCode, data)
}
override fun invokeDefaultOnBackPressed() {
super.onBackPressed()
}
override fun onPause() {
super.onPause()
mReactInstanceManager.onHostPause(this)
}
override fun onResume() {
super.onResume()
mReactInstanceManager.onHostResume(this, this)
}
override fun onDestroy() {
super.onDestroy()
mReactInstanceManager.onHostDestroy(this)
mReactRootView.unmountReactApplication()
}
override fun onBackPressed() {
mReactInstanceManager.onBackPressed()
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_MENU) {
mReactInstanceManager.showDevOptionsDialog()
return true
}
return super.onKeyUp(keyCode, event)
}
companion object {
var OVERLAY_PERMISSION_REQ_CODE = 1234
}
}
这样对于RN的集成就初步完成了,你可以编写index.js
来实现页面。
在初步运行时可能会存在问题:
- 在真机调试时由于index.js获取不到导致的错误
解决:在App>src>main中创建
assets
目录,在调试前运行下面代码,这样在每次编译打包之前需要先执行 js 文件的打包(即生成离线的 jsbundle 文件)。:
react-native bundle --platform android --dev false --entry-file index.js --bundle-output app/src/main/assets/index.android.bundle --assets-dest app/src/main/res/
- 部分真机由于so库对于64位的不兼容导致错误
解决:限制使用32位的so库
gradle.properties
中添加android.useDeprecatedNdk=true
- app目录下的
build.gradle
添加下属代码:
android {
...
defaultConfig {
...
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
}
2. RN与原生组件的通信
有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块包装;或者你需要复用一些 Java 代码,而不是用 Javascript 重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。
这时就需要实现通信了。
这里以toast
为例子来实现一个。
-
创建一个
ToastModule
:ReactContextBaseJavaModule要求派生类实现getName方法。这个函数用于返回一个字符串名字,这个名字在 JavaScript 端标记这个模块。这里我们把这个模块叫做ToastExample,这样就可以在 JavaScript 中通过NativeModules.ToastExample访问到这个模块。
要导出一个方法给 JavaScript 使用,Java 方法需要使用注解@ReactMethod。方法的返回类型必须为void。React Native 的跨语言访问是异步进行的,所以想要给 JavaScript 返回一个值的唯一办法是使用回调函数或者发送事件。
class ToastModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String { return "ToastExample" } override fun getConstants(): Map<String, Any>? { val constants = mutableMapOf<String, Any>() constants[DURATION_SHORT_KEY] = Toast.LENGTH_SHORT constants[DURATION_LONG_KEY] = Toast.LENGTH_LONG return constants } @ReactMethod fun showToast(message: String, duration: Int) { Toast.makeText(reactApplicationContext, message, duration).show() } companion object { private const val DURATION_SHORT_KEY = "SHORT" private const val DURATION_LONG_KEY = "LONG" } }
-
注册模块
我们需要在应用的 Package 类的createNativeModules方法中添加这个模块。如果模块没有被注册,它也无法在 JavaScript 中被访问到。
class ReactPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> { val modules = mutableListOf<NativeModule>() modules.add(ToastModule(reactContext)) return modules } override fun createViewManagers(reactContext: ReactApplicationContext?): MutableList<ViewManager<View, ReactShadowNode<*>>> { return Collections.emptyList() } }
-
在Application中引入包
class MyApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() SoLoader.init(this, false) } private val mReactNativeHost = object : ReactNativeHost(this) { override fun getUseDeveloperSupport(): Boolean { return BuildConfig.DEBUG } override fun getPackages(): List<ReactPackage> { return Arrays.asList( MainReactPackage(), ReactPackage() ) } } override fun getReactNativeHost(): ReactNativeHost { return mReactNativeHost } }
-
实现index.js
在js文件中添加
NativeModules
的引用。import React, { Component } from 'react'; import { AppRegistry, StyleSheet, NativeModules, Button, View } from 'react-native'; var RNToast = NativeModules.ToastExample; export default class HelloWorld extends Component { render() { return ( <View style={styles.container}> <View style={styles.buttonContainer}> <Button onPress={() => { RNToast.showToast("test",RNToast.LONG); }} title="Press Me" /> </View> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', }, buttonContainer: { margin: 20 }, alternativeLayoutButtonContainer: { margin: 20, flexDirection: 'row', justifyContent: 'space-between' } }) AppRegistry.registerComponent('MyReactNativeApp', () => HelloWorld);
-
Last but not least:在实现的原生页面中(MyReactActivity)也需要添加包
这一点并没有说明,我也是摸索了很久才想起来的。(当然要是有不足之处请给位指出)
.addPackage(ReactPackage())
以上便实现了原生通信。