这里有一个引导 native-modules-setup,该引导适用于 android/iOS,其实就是自动创建原生模块的模板,之后再去实现自己的逻辑就好了。
可以查看 native-modules-android 【翻译 】、native-modules-ios【翻译】 来了解 android 、IOS 下模块开发的一些 API 和 开发流程。
官网给了一个 Toasts 的简单示例,这里就不用这个了,直接实战化,以集成友盟 SDK 作为例子(备注:以下过程是在 rn 0.61 版本下进行的,版本不同,可能过程也有不同)。
友盟的产品有很多,统计/推送/分享/广告监测等等,分享和广告监测不太是刚需,且会和其他功能交叉(比如分享,集成微信分享的同时一般还会集成支付功能)所有就不集成了,这里仅集成 统计/推送 功能。
目前行业现状:
统计,目前国内用的较多的是 友盟 和 腾讯移动分析,二者在功能上,后者略有胜出,但也仅仅是略有胜出;
推送,最好用的当然是手机厂商的系统级推送通道,目前华为/小米/OPPO/vivo/魅族 都支持,这基本可以覆盖国内 70% (市场份额) 以上的机器了,第三方推送主要是处理剩余那 30% 的,据我个人了解,国内口碑相对可以的是 极光 和 友盟,况且友盟从 CNZZ 起家就开始做统计了,技术积累还是可以的。最后再考虑到注册一个平台就能用两个功能了,不用在不同平台切换,所以推送功能也选友盟。
一、创建项目
// 安装 create-react-native-module
yarn global add create-react-native-module
// 创建 rn 项目
react-native init sample
// 进入 rn 目录创建 module
cd sample
create-react-native-module uapp
打开 sample 文件夹可以看到已创建了一个 react-native-uapp
文件夹,打开这个文件夹看看目录结构
- android
- index.js
- ios
- package.json
- react-native-uapp.podspec
- README.md
很贴心,连 readme 都帮忙创建好了,不过我看了一下,介绍主要还是针对 rn6.0 以下版本的,后面还是要自己写点使用方法的,暂且不管了,下面的主要工作就是在 android 和 ios 两个文件夹进行了,最后搞一下 index.js 对外提供接口。
二、Android
1、修改命名空间
先来做 android 版本的,进入 android 文件夹,还有一个 readme, 打开看一下,是介绍如何打包为 maven 依赖的,这个就没必要了,可以删了。按照惯例和自己准备用名称,先改一下 android 的命名空间。
修改
src/main/java/com/reactlibrary/UappModule.java
src/main/java/com/reactlibrary/UappPackage.java
头部从 package com.reactlibrary;
改为 package com.umreact.uapp;
移动文件夹:
src/main/java/com/reactlib/
-> src/main/java/com/umreact/uapp
最后修改
build.gradle
和 src/main/AndroidManifest.xml
,查找 com.reactlibrary
改为 com.umreact.uapp
2、安装本地包
使用 rn 一般都是用 yarn
来管理包的,所以我这里用 yarn
来安装本地包,如果你使用 npm
自行搜索一下,应该都是差不多的,在 sample
目录执行
yarn add file:./react-native-uapp
这样就把这个本地包装上了,对于 android 而言,由于 rn 0.60 之后,是 autolink 的,所以无需其他操作了,可以先 react-native run-android
试一下,看能不能编译成功。
【注意】: 此时真正使用的 react-native-uapp
目录是在 node_modules 下,yarn add
命令自动复制了,所以后续的原生开发应该是在 node_modules/react-native-uapp
目录下。但实际使用的 js 模块却还在 file:./react-native-uapp
目录下
3、设置友盟 sdk
第一步是修改 android/build.gradle
, 先来看看这个文件的结构
// 内置函数, 用于安全获取当前使用的 android sdk 版本
// 尝试使用主工程版本, 若无, 则指定一个默认版本
// 在 `android` 块的配置中有使用
def safeExtGet(prop, fallback) {
..
}
// 把当前包作为独立工程的编译配置
// 就是直接在当前插件目录执行 `yarn install`,然后编译当前目录
buildscript {
...
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
// 当前插件的编译配置
android {
....
}
// 也是作为独立工程的配置
// 关于 buildscript 和 repositories 和参考
// https://www.jianshu.com/p/ee57e4de78a3
repositories {
....
}
// 当前插件的依赖
dependencies {
...
}
// 在 afterEvaluate 中用到了
// 解析 package.json 中的包信息(名称,作者,协议等)
def configureReactNativePom(def pom) {
....
}
// Gradle 构建生命周期的钩子
// 对 android 不是特别熟悉,看这部分代码像是打包,可以发布到线上 lib 库的
afterEvaluate { project ->
....
}
了解这个文件结构后,来改一下,就拿来做插件的,不需要做独立工程,可以去除相关代码;也不需要发布为lib,所以也可以去除相关代码;当然,这个修改不是必须的,保留这些代码也没什么副作用。
根据友盟的文档 基础组件集成、U-Push 集成 、U-App 集成 可以看出,集成 sdk 有自动和手动两种模式,自动使用 https://dl.bintray.com/umsdk/release
作为 maven 中心库,手动则是在 SDK下载 页面下载 sdk,我这里选择自动化集成,后期维护升级也必将方便;找到需要集成的 sdk,有:
com.umeng.umsdk:utdid:version
com.umeng.umsdk:common:version
com.umeng.umsdk:analytics:version
com.umeng.umsdk:push:version
可以在 https://dl.bintray.com/umsdk/release/ 查看当下的最新版本,最终修改 build.gradle
的代码为
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
def safeCfgGet(prop, fallback) {
return project.hasProperty(prop) ? project.getProperties().get(prop) : fallback
}
apply plugin: 'com.android.library'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 28)
buildToolsVersion safeExtGet('buildToolsVersion', "28.0.3")
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
# 创建一个文件设置 与模块相关的混淆规则
consumerProguardFiles "proguard.pro"
// +新增,下面会进行说明
buildConfigField("int", "UMENG_DEVICE_TYPE", safeCfgGet("UMENG_DEVICE_TYPE", "1"))
buildConfigField("String", "UMENG_PUSH_SECRET", safeCfgGet("UMENG_PUSH_SECRET", "\"\""))
}
lintOptions {
abortOnError false
}
}
# 添加友盟的线上仓库地址
rootProject.allprojects {
repositories {
maven { url 'https://dl.bintray.com/umsdk/release' }
}
}
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation 'com.umeng.umsdk:utdid:1.1.5.3'
implementation 'com.umeng.umsdk:common:2.1.0'
implementation 'com.umeng.umsdk:analytics:8.1.3'
implementation 'com.umeng.umsdk:push:6.0.1'
}
4、开发
src/main/java/com/reactlibrary/UappPackage.java
无需改动
主要是修改 src/main/java/com/reactlibrary/UappModule.java
载入的 sdk 需要在
application.onCreate
阶段初始化统计模块需要在 Activity 的 onResume 和 onPause 触发统计 按照文档其实在 auto 模式下是无需这一步的,但实测了一下,发现 auto 模式有问题)
3.推送模块需要在Activity 的 onCreate 调用 PushAgent.getInstance(context).onAppStart();
- RN 可以通过
addLifecycleEventListener
在 Module 中添加 active 生命周期的监听函数
// context - application 上下文
// appkey - 友盟申请得到的 app key
// channel - 渠道,方便后期统计查看(比如小米商店、华为商店),可以为不同市场打包不同apk
// deviceType - 设备类型,支持手机或平板
// pushSecret - 推送秘钥
UMConfigure.init(Context context, String appkey, String channel, int deviceType, String pushSecret);
// 少了 appkey/channel,使用这个调用的前提是
// 在AndroidManifest.xml中配置过appkey和channel值
UMConfigure.init(Context context, int deviceType, String pushSecret);
从初始化函数调用可以看到, appkey、channel、deviceType、pushSecret 作为配置信息,肯定不能在模块中定义的,需要在主工程中设置,主工程要如何传递参数呢,找到了几篇有帮助的文章:gradle-tips、buildConfigField 和 gradle.properties
从这三篇文章可以看出,传递参数可以通过 android 项目根目录的 build.gradle
或 gradle.properties
,这两个都是顶级配置文件,任何子工程(插件)都可以读取配置值,很明显的, gradle.properties
作为 kv 配置文件,特别适合做这个。
看完这三篇文章,应该很容易理解上面 build.gradle
中 buildConfigField()
的作用了,就是为了可以在 gradle.properties
配置友盟参数,但没有设置 appkey
和 channel
,因为这两个可以直接在 AndroidManifest.xml
中设置,并且这样更容易进行分渠道打包,如果有兴趣,可以看看 build-variants
结合上面的信息,最终修改 UappModule.java
package com.umreact.uapp;
import android.content.Context;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import android.util.Log;
import com.umeng.message.PushAgent;
import com.umeng.commonsdk.UMConfigure;
import com.umeng.analytics.MobclickAgent;
import com.umeng.message.IUmengRegisterCallback;
public class UappModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
public static void init(Context appContext) {
// 开启 debug
UMConfigure.setLogEnabled(true);
// 初始化友盟
UMConfigure.init(appContext, BuildConfig.UMENG_DEVICE_TYPE, BuildConfig.UMENG_PUSH_SECRET);
// 设置 session 时长 (这里为了调试设置的比较小, 可以不设置,保持默认)
MobclickAgent.setSessionContinueMillis(1000);
// 设置统计模式 (不使用 auto 模式, 手工触发)
MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.MANUAL);
// 打印 测试统计设备 id
String[] devices = UMConfigure.getTestDeviceInfo(appContext);
Log.i("UMLog", "did:" + devices[0]);
Log.i("UMLog", "mac:" + devices[1]);
Log.i(
"UMLog",
"info:" + "{\"device_id\":\""+devices[0]+"\",\"mac\":\""+devices[1]+"\"}"
);
// 注册推送
final PushAgent mPushAgent = PushAgent.getInstance(appContext);
mPushAgent.register(new IUmengRegisterCallback() {
@Override
public void onSuccess(String deviceToken) {
Log.i("UMLog","注册成功:deviceToken:--------> " + deviceToken);
}
@Override
public void onFailure(String s, String s1) {
Log.e("UMLog","注册失败:--------> " + "s:" + s + ",s1:" + s1);
}
});
}
/**
* 不做特殊开发, 一般 rn 就一个 MainActivity, 但这里也留一个接口
* 如果有多个 active 的话, 在 active 的 onCreate 调用
* https://developer.umeng.com/docs/66632/detail/98581
* @param activity
*/
public static void onAppStart(Context activity) {
PushAgent.getInstance(activity).onAppStart();
}
@Override
public String getName() {
return "Uapp";
}
public UappModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addLifecycleEventListener(this);
// 这里调用, 设备总是处于离线状态, 无法接收推送消息
// https://developer.umeng.com/docs/66632/detail/98581
// 官方文档提到: 务必在 Application类的 onCreate() 方法中做SDK代码初始化工作
// init(reactContext.getApplicationContext());
}
@Override
public void onHostResume() {
MobclickAgent.onResume(getCurrentActivity());
}
@Override
public void onHostPause() {
MobclickAgent.onPause(getCurrentActivity());
}
@Override
public void onHostDestroy() {
// do nothing
}
}
代码中调用的 BuildConfig.UMENG_DEVICE_TYPE
和 BuildConfig.UMENG_PUSH_SECRET
是在 gradle.properties
文件中配置的
UMENG_DEVICE_TYPE=1
UMENG_PUSH_SECRET="666"
appkey、channel 则在主工程的 AndroidManifest.xml
中配置
<manifest ..>
....
// 添加友盟所需基本权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application ...>
....
<meta-data android:value="666" android:name="UMENG_APPKEY"/>
<meta-data android:value="Test" android:name="UMENG_CHANNEL"/>
...
</application>
...
</manifest>
还需要在 android/src/main/.../MainApplication.java
添加一行
...
import com.umreact.uapp.UappModule;
public class MainApplication extends Application implements ReactApplication {
...
@Override
public void onCreate() {
..
// 添加这一行
UappModule.init(this);
..
}
...
}
配置完成后,运行 app;
1、 从 android 的 log 中获取到 {"device_id":"xxx","mac":"xxx"}
到 友盟测试设备 去添加一下,然后在 实时日志 查看是否成功统计,没统计的话,可以尝试重新运行一次 app。
2、从 Log 中找到 deviceToken
,到 友盟推送工具 查询设备状态,如果是在线状态,可以到 测试模式 这里添加测试设备,并发送一条推送试一下。
获取 android log 有很多办法,我们需要 filter 的关键词为 UMLog
1、命令行: adb logcat *:S UMLog:V
2、打开 Android Studio 的 Logcat 窗口
3、使用 FB 的 flipper (这个可能需要 rn 版本大于0.60)
5、后记
最终的扩展使用方法为
1、在 gradle.properties
设置 UMENG_DEVICE_TYPE
和 UMENG_PUSH_SECRET
2、在 android/src/main/AndroidManifest.xml
设置 UMENG_APPKEY
和 UMENG_CHANNEL
3、本想除友盟信息外零配置,但奈何 android 开发没有经验,还需在 android/src/main/.../MainApplication.java
调用初始化方法
另外关于 MainApplication.java
中的配置,可以看在 UappModule.java
的 UappModule()
函数的注释,一开始是想在这里直接载入的,这样就少了一个配置,但实测发现,这样做的话,可以注册成功,但设备总处于离线状态,无法接收推送消息;并且在 推送接入文档 中也有说明:务必在工程的自定义Application类的 onCreate() 方法中做SDK代码初始化工作。所以最终也不再深究了,就先这么着吧(其实排查设备离线,定位到是这里的问题,就花了我好几个钟头,实在懒得继续搞了)
上面的流程只是跑通了统计/推送功能,做一个能用的扩展其实还有不少活要干,比如页面统计、埋点统计、厂商推送通道集成、接收消息后的处理、通过 js 暴露接口给 rn jsx 使用,本篇主要是为了记录一下原生模块开发的流程,所有就不过多着墨了
三、iOS
待填坑....