开发React Native原生组件-For Android
1.什么是React Native原生开发
先看一张React Native的技术架构图(图片来源)
对于一个简单的APP来说,我们只需要进行JS的开发即可(图中绿色的部分)。但是某些情况下,我们使用一些平台相关的原生能力,这时候就需要做RN原生开发(途中黄色的部分)。比如以下场景:
- 需要使用原生的系统能力,但是React Native社区中找不到提供相关接口的组件,我们需要自己包一下;
- 使用第三方的lib,比如IM、直播、广告等功能,官方提供了原生的库,我们将其包成RN原生模块后才能使用;
- 遇到性能问题或需要特殊的UI动画效果,这种场景我们需要直接使用原生组件来提升性能。
当你掌握了RN原生开发,大部分的APP需求都可以满足了。
2.如何入手原生开发
RN的原生开发分为两种:
- 原生模块开发(Native Modules)
- 原生UI组件开发(Native UI Components)
从使用方式上很容易弄清两者的区别:
1.原生模块的使用
import {NativeModules} from 'react-native'
const {ModuleA} = NativeModules
ModuleA.show()
2.原生UI组件的使用
import {requireNativeComponent} from 'react-native'
const UIComponentB = requireNativeComponent("UIComponentB")
render () => <UIComponentB props={...}></UIComponentB>
这次主要讨论原生模块的开发,原生UI组件先放在一边
2.1.安卓原生模块开发
原生模块开发主要涉及到3个部分:
- 业务相关原生代码
- bridge原生代码
- js代码
2.1.1 一个最简单的例子
拿FB官方的Toast例子来说明,我们需要一个提醒窗,使用安卓的原生Toast实现。
Step1 编写安卓原生业务代码
我们在项目目录android/app/src/main/java/your_package_dir/
下创建一个ToastModule.java
文件(与MainApplication.java
文件平级)
// ToastModule.java
package com.your-app-name;
import android.widget.Toast;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
public class ToastModule extends ReactContextBaseJavaModule {
private static ReactApplicationContext reactContext;
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
// 构造函数,没有特殊需求时照猫画虎即可
ToastModule(ReactApplicationContext context) {
super(context);
reactContext = context;
}
// 模块名称,决定了在js中引用的模块名字
@Override
public String getName() {
return "ToastExample";
}
// 可选方法,定义一些常量供js使用。
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
// 通过ReactMethod注释器将show方法暴露出去,供js使用
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}
Step2 编写bridge原生代码
在ToastModule.java
同级目录创建一个CustomToastPackage.java
文件
注意,createJSModules方法在React Native 0.47版本中移除了,所以在比较老的组件中可能会见到此方法,在0.47之后的版本汇总不再使用。现在只有 createViewManagers 和 createNativeModules两个方法
// CustomToastPackage.java
package com.your-app-name;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CustomToastPackage implements ReactPackage {
// UI Components 在此注册
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
// Native Modules 在此注册
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ToastModule(reactContext));
return modules;
}
}
Step3 编写javascript代码
为了方便使用,我们一般会在js中把原生组件简单包装一下再使用。在JS代码中创建一个Toast.js
文件
import {NativeModules} from 'react-native';
module.exports = NativeModules.ToastExample;
这样,我们就可以在RN项目中使用Toast组件了
import Toast from './Toast';
// 这里的Toast.SHORT使我们在原生代码中通过getConstants暴露出来的
Toast.show('Awesome', Toast.SHORT);
2.1.2 高级特性
在实际应用中上述例子只能称为一个玩具,其实是无法满足真实需求的。
通常情况下我们需要在js和原生代码之间有一个双向的交互,等待原生代码返回结果或异常;通过js注入一些钩子到原生代码中;监听原生代码抛出的事件,诸如此类。
好在RN在此方面提供了比较完整的解决方案,比如Callback
, Promise
, RCTDeviceEventEmitter
等,利用这些特性,几乎可以满足所有需求,尽管有时候实现的会有些丑陋。
关于这些特性的介绍,本文中不再赘述,直接看FB的文档即可,传送门:Navtive Modules开发文档。
2.2 更进一步,将组件发布到npm
通过上面的学习,我们几乎可以把任何原生功能集成到项目中。但是在实际项目中,还是不够的。
当我们开发多个RN工程时,会希望自己的RN原生组件能够像社区中的那些开源组件一样,通过yarn install
安装后即可使用;在发现组件BUG后,只需要执行yarn upgrade react-native-xxx
即可修复,从而不用在每个项目的原生代码中折腾。
因此,我们需要将原生模块发布到npm仓库中,方便维护和复用。
最近项目中正好有集成广告sdk的需求,以此为例谈一谈如何开发一个RN原生组件并发布到npm仓库中。
3.开发安卓广告RN原生组件并发布
此次我们集成了优量汇(广点通)以及穿山甲(头条)两个广告平台的sdk,本文中以集成优量汇举例。
3.1 初始化一个RN组件工程
使用react-native-create-library初始化一个RN组件工程,该工具会为我们创建一个react native组件工程骨架。
$ npm install -g react-native-create-library
$ react-native-create-library --package-identifier com.qhkj.rn.advert --platforms android,ios advert
$ mv advert react-native-advert
其中 com.qhkj.rn.advert是包名, advert是文件夹名称。
3.2 编写原生代码接入优量汇广告
3.2.1 独立广告sdk接入逻辑
为了能够在其他的纯原生项目中使用,把原生功能码放在单独的module中开发。
因此在项目中新建一个moduleqhkj-android-advert
(可以使用android studio来创建 File->New->New Module->Android Library),并修改两个文件
#/android/settings.gradle
include ':qhkj-android-advert'
#/android/build.gradle
dependencies {
...
implementation "com.facebook.react:react-native:+"
api project(':qhkj-android-advert')
}
目录结构这里简单说明一下dependencies中,使用
implementation
和api
关键字是有区别的。implementation
是用来引用在工程内部使用的依赖,当把当前工程给提供给其它项目使用时,通过implementation
引入的库是不能被外部项目使用的。而通过api
引入的库的接口是可以供外部项目使用的。由于我们需要暴露qhkj-android-advert中的接口,所以此处使用api
,而不是implementation
.
如上图,
-
qhkj-android-advert
文件夹中为纯原生代码,用于集成各个平台的广告sdk,直接将优量汇的demo移植到工程中改一改即可,此处不做更多描述,具体可参考文末项目开源代码; -
com.qhkj.rn.advert
中为RN桥接代码,用于把原生广告能力暴露出去,包含一个Module文件和一个Package文件。
需要注意的是,在我们的android libaray
qhkj-android-advert
中除了Java代码外,我们还把资源文件如layout, drawable, xml, AndroidManifest.xml等全部集成进来,简化外部使用。
由于RNAdvertModule
用到了几个高级特性,这里详细说明一下。
3.2.2 RNAdvertModule的实现
需求:
- 对于激励视频这类广告来说,我们需要知道用户是否观看完了广告,以决定是否给予用户相应的激励和提示。显然,这是一个异步操作,我们需要
Promise
特性。 - 另外,由于我们引入的广告sdk实际是以Activity的方式调用的,我们还需要在MainActivity和AdvertActivity之间传递数据。这里我们用到了安卓的
startActivityForResult
接口。
// RNAdvertModule.java
public class RNAdvertModule extends ReactContextBaseJavaModule {
// 定义激励视频Activity的返回request值
private static final int SHOW_REWARD_VIDEO_REQUEST = 2;
// 定义一个全局promise对象,用于保存js传入的promise对象
private Promise mAdvertPromise;
// 定义一个activity事件监听器
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
// 在此函数中处理广告activity的返回结果,并通过promise完成这个异步流程
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
if (requestCode == SHOW_SPLASH_REQUEST
|| requestCode == SHOW_REWARD_VIDEO_REQUEST) {
if (mAdvertPromise != null) {
if (resultCode == Activity.RESULT_CANCELED) {
// 调用js传入的promise.resolve方法
mAdvertPromise.resolve(false);
} else if (resultCode == Activity.RESULT_OK) {
// 调用js传入的promise.resolve方法
mAdvertPromise.resolve(true);
}
mAdvertPromise = null;
}
}
}
};
public RNAdvertModule(ReactApplicationContext reactContext) {
super(reactContext);
// 将Activity事件处理器注册到MainActivity中
reactContext.addActivityEventListener(mActivityEventListener);
}
@Override
public String getName() {
return "RNAdvert";
}
@ReactMethod
public void init(ReadableMap config) {
mConfig = config;
}
// 拉起激励视频的方法,注意这里的入参promise
@ReactMethod
public void showRewardVideo(final Promise promise) {
Context context = getReactApplicationContext();
Intent intent;
mAdvertPromise = promise;
// 随机拉起广点通或者穿山甲的激励视频广告
double random = Math.random();
if (random <= 0.5) {
intent = new Intent(context, GDTRewardVideoActivity.class);
intent.putExtra("app_id", mConfig.getString("gdtAppId"));
intent.putExtra("pos_id", mConfig.getString("gdtRewardVideoPosId"));
} else {
intent = new Intent(context, TTRewardVideoActivity.class); // mContext got from your overriden constructor
intent.putExtra("horizontal_rit", mConfig.getString("ttRewardVideoHPosId"));
intent.putExtra("vertical_rit", mConfig.getString("ttRewardVideoVPosId"));
}
try {
// 拉起广告Activity并接受返回结果
getCurrentActivity().startActivityForResult(intent, SHOW_REWARD_VIDEO_REQUEST);
// 禁止原生动画
getCurrentActivity().overridePendingTransition(0, 0);
} catch (Exception e) {
// 处理异常,调用promise.reject
mAdvertPromise.reject("拉起激励视频广告失败!", e);
mAdvertPromise = null;
}
}
}
通过上述处理,我们js代码中即可同步调用showRewardVideo
方法,并根据返回结果进行相应的处理。
import {NativeModules} from 'react-native'
const {RNAdvert} = NativeModules
try {
const finish = await RNAdvert.showRewardVideo()
if (finish) {
Navigation.showToast({ message: '恭喜获得3个积分!' })
dispatch(Actions.incPointProfile, { value: 3 })
console.log('获得激励')
} else {
console.log('未获得激励')
}
} catch (err) {
console.log(err)
}
3.2.3 JS封装
作为一个react native组件,我们希望在使用时不要每次都引入NativeModules
,或则希望把接口进行二次封装方便使用。
为此,我们可以在组件工程的index.js
中在做一次封装
// react-native-advert/index.js
import { NativeModules } from 'react-native';
const { RNAdvert } = NativeModules;
export default RNAdvert;
我们在使用时就可以这样:
import Advert from 'react-native-advert'
...
3.2.4 支持ReactNative的Autolinking特性
ReactNative在0.60
版本中引入了Autolinking,极大简化了引入原生组件的流程,
关于Autolinking特性的说明可参考《一文读懂ReactNative0.60的 Autolinking 新特性》。
由于我们的安卓工程中使用了multi project结构,我们需要指定packageImportPath
,否则autolink会使用错误的包名。
如果是IOS平台,需要加入.podspec文件,以支持Autolinking特性
创建一个react-native-advert/react-native.config.js
文件,填入如下代码
// react-native-advert/react-native.config.js
module.exports = {
dependency: {
platforms: {
android: {
packageImportPath: 'import com.qhkj.rn.advert.RNAdvertPackage;',
},
},
},
};
3.4 发布到npm仓库
npm仓库是javascript的包管理中心,全世界的开发者都把自己开发的js组件发布到这里。
我们需要把组件发布到npm仓库中,此后便可通过npm install / yarn install
来使用。
3.4.1 注册并登录npm
1.在https://www.npmjs.com网站中创建你的npm账号
2.在终端中登录
这里需要注意,因为npm官方仓库下载慢的问题,我们通常会设置为淘宝的镜像,所以我们在登录npm仓库和发布时需要带上
--registry=http://registry.npmjs.org
来指定官方仓库地址
npm login --registry=http://registry.npmjs.org
你可以使用npm whoami
命令来确认本地是否成功登陆认证成功
$ qhkj npm whoami
qianhaikeji
3.4.2 修改package.json文件
package.json文件中定义了组件名、版本、作者、描述、依赖等发布信息,你需要修改为自己的信息,比如:
{
"name": "react-native-advert",
"version": "1.0.1",
"description": "A ReactNative Advert Component for android",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"react-native",
"android",
"advert",
"gdt",
"tt"
],
"author": {
"name": "qhkj",
"email": "service@qianhaikeji.cn"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:qianhaikeji/react-native-advert.git"
},
"devDependencies": {
"react": "16.9.0",
"react-native": "^0.61.1"
},
"peerDependencies": {
"react-native": ">=0.47"
}
}
3.4.3 发布npm包
进入项目目录下
$ cd react-native-advert
$ npm publish --registry=http://registry.npmjs.org
发布成功后,进入项目页面查看是否发布成功:https://www.npmjs.com/package/react-native-advert
3.4.4 更新包版本后不生效的问题
在升级npm包的时候,很多人应该会碰到这个问题,自己明明在npm仓库中已经发布了新版本,但是在项目中使用yarn install
或者yarn upgrade
还是老版本,这种一般都是因为我们在本地配置了淘宝镜像源导致的。
淘宝的镜像源是定时拉取同步npm主站的资源,所以会有一定的滞后,我们需要手动同步一下。
1.打开https://npm.taobao.org/淘宝源网站
2.在右上角的搜索框中搜索你的包名,比如react-native-advert
,进入项目页面
3.然后点击SYNC
按钮,即可完成手动同步
3.5 项目开源地址
https://github.com/qianhaikeji/react-native-advert.git
欢迎留言交流~
关于我们
我们是一个高效、热情、有责任的技术团队,承接各种软件系统定制需求。
长期招聘远程开发者,如果您喜欢尝试新技术,有一点代码洁癖,能够使用文档进行高效的沟通,React/nodejs/ES6任意一种玩的飞起,那么,欢迎来撩~(想赚快钱的请绕道,谢谢)
简历请发送到:service@qianhaikeji.cn
当然,也欢迎甲方爸爸把项目甩我们脸上。添加微信:bdalbbtx