1.什么是React Native原生開發(fā)
先看一張React Native的技術(shù)架構(gòu)圖(圖片來源)
對于一個簡單的APP來說弛矛,我們只需要進(jìn)行JS的開發(fā)即可(圖中綠色的部分)蚤假。但是某些情況下乏矾,我們使用一些平臺相關(guān)的原生能力嵌牺,這時候就需要做RN原生開發(fā)(途中黃色的部分)。比如以下場景:
- 需要使用原生的系統(tǒng)能力凝颇,但是React Native社區(qū)中找不到提供相關(guān)接口的組件整吆,我們需要自己包一下;
- 使用第三方的lib硫兰,比如IM诅愚、直播、廣告等功能,官方提供了原生的庫违孝,我們將其包成RN原生模塊后才能使用刹前;
- 遇到性能問題或需要特殊的UI動畫效果,這種場景我們需要直接使用原生組件來提升性能雌桑。
當(dāng)你掌握了RN原生開發(fā)喇喉,大部分的APP需求都可以滿足了。
2.如何入手原生開發(fā)
RN的原生開發(fā)分為兩種:
- 原生模塊開發(fā)(Native Modules)
- 原生UI組件開發(fā)(Native UI Components)
從使用方式上很容易弄清兩者的區(qū)別:
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>
這次主要討論原生模塊的開發(fā)校坑,原生UI組件先放在一邊
2.1.安卓原生模塊開發(fā)
原生模塊開發(fā)主要涉及到3個部分:
- 業(yè)務(wù)相關(guān)原生代碼
- bridge原生代碼
- js代碼
2.1.1 一個最簡單的例子
拿FB官方的Toast例子來說明拣技,我們需要一個提醒窗,使用安卓的原生Toast實現(xiàn)耍目。
Step1 編寫安卓原生業(yè)務(wù)代碼
我們在項目目錄android/app/src/main/java/your_package_dir/
下創(chuàng)建一個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";
// 構(gòu)造函數(shù)膏斤,沒有特殊需求時照貓畫虎即可
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
同級目錄創(chuàng)建一個CustomToastPackage.java
文件
注意毅访,createJSModules方法在React Native 0.47版本中移除了沮榜,所以在比較老的組件中可能會見到此方法,在0.47之后的版本匯總不再使用∮鞔猓現(xiàn)在只有 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代碼中創(chuàng)建一個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 高級特性
在實際應(yīng)用中上述例子只能稱為一個玩具振愿,其實是無法滿足真實需求的。
通常情況下我們需要在js和原生代碼之間有一個雙向的交互弛饭,等待原生代碼返回結(jié)果或異常;通過js注入一些鉤子到原生代碼中萍歉;監(jiān)聽原生代碼拋出的事件侣颂,諸如此類。
好在RN在此方面提供了比較完整的解決方案枪孩,比如Callback
, Promise
, RCTDeviceEventEmitter
等憔晒,利用這些特性,幾乎可以滿足所有需求蔑舞,盡管有時候?qū)崿F(xiàn)的會有些丑陋拒担。
關(guān)于這些特性的介紹,本文中不再贅述攻询,直接看FB的文檔即可从撼,傳送門:Navtive Modules開發(fā)文檔。
2.2 更進(jìn)一步钧栖,將組件發(fā)布到npm
通過上面的學(xué)習(xí)低零,我們幾乎可以把任何原生功能集成到項目中婆翔。但是在實際項目中,還是不夠的掏婶。
當(dāng)我們開發(fā)多個RN工程時啃奴,會希望自己的RN原生組件能夠像社區(qū)中的那些開源組件一樣,通過yarn install
安裝后即可使用雄妥;在發(fā)現(xiàn)組件BUG后最蕾,只需要執(zhí)行yarn upgrade react-native-xxx
即可修復(fù),從而不用在每個項目的原生代碼中折騰老厌。
因此瘟则,我們需要將原生模塊發(fā)布到npm倉庫中,方便維護(hù)和復(fù)用梅桩。
最近項目中正好有集成廣告sdk的需求壹粟,以此為例談一談如何開發(fā)一個RN原生組件并發(fā)布到npm倉庫中。
3.開發(fā)安卓廣告RN原生組件并發(fā)布
此次我們集成了優(yōu)量匯(廣點通)以及穿山甲(頭條)兩個廣告平臺的sdk宿百,本文中以集成優(yōu)量匯舉例趁仙。
3.1 初始化一個RN組件工程
使用react-native-create-library初始化一個RN組件工程,該工具會為我們創(chuàng)建一個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 編寫原生代碼接入優(yōu)量匯廣告
3.2.1 獨立廣告sdk接入邏輯
為了能夠在其他的純原生項目中使用,把原生功能碼放在單獨的module中開發(fā)痊焊。
因此在項目中新建一個moduleqhkj-android-advert
(可以使用android studio來創(chuàng)建 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
關(guān)鍵字是有區(qū)別的薄啥。implementation
是用來引用在工程內(nèi)部使用的依賴辕羽,當(dāng)把當(dāng)前工程給提供給其它項目使用時,通過implementation
引入的庫是不能被外部項目使用的垄惧。而通過api
引入的庫的接口是可以供外部項目使用的刁愿。由于我們需要暴露qhkj-android-advert中的接口,所以此處使用api
到逊,而不是implementation
.
如上圖铣口,
-
qhkj-android-advert
文件夾中為純原生代碼,用于集成各個平臺的廣告sdk觉壶,直接將優(yōu)量匯的demo移植到工程中改一改即可脑题,此處不做更多描述,具體可參考文末項目開源代碼铜靶; -
com.qhkj.rn.advert
中為RN橋接代碼叔遂,用于把原生廣告能力暴露出去,包含一個Module文件和一個Package文件。
需要注意的是掏熬,在我們的android libaray
qhkj-android-advert
中除了Java代碼外佑稠,我們還把資源文件如layout, drawable, xml, AndroidManifest.xml等全部集成進(jìn)來,簡化外部使用旗芬。
由于RNAdvertModule
用到了幾個高級特性舌胶,這里詳細(xì)說明一下。
3.2.2 RNAdvertModule的實現(xiàn)
需求:
- 對于激勵視頻這類廣告來說疮丛,我們需要知道用戶是否觀看完了廣告幔嫂,以決定是否給予用戶相應(yīng)的激勵和提示。顯然誊薄,這是一個異步操作履恩,我們需要
Promise
特性。 - 另外呢蔫,由于我們引入的廣告sdk實際是以Activity的方式調(diào)用的切心,我們還需要在MainActivity和AdvertActivity之間傳遞數(shù)據(jù)。這里我們用到了安卓的
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事件監(jiān)聽器
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
// 在此函數(shù)中處理廣告activity的返回結(jié)果,并通過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) {
// 調(diào)用js傳入的promise.resolve方法
mAdvertPromise.resolve(false);
} else if (resultCode == Activity.RESULT_OK) {
// 調(diào)用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;
}
// 拉起激勵視頻的方法俏脊,注意這里的入?yún)romise
@ReactMethod
public void showRewardVideo(final Promise promise) {
Context context = getReactApplicationContext();
Intent intent;
mAdvertPromise = promise;
// 隨機(jī)拉起廣點通或者穿山甲的激勵視頻廣告
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并接受返回結(jié)果
getCurrentActivity().startActivityForResult(intent, SHOW_REWARD_VIDEO_REQUEST);
// 禁止原生動畫
getCurrentActivity().overridePendingTransition(0, 0);
} catch (Exception e) {
// 處理異常全谤,調(diào)用promise.reject
mAdvertPromise.reject("拉起激勵視頻廣告失敗爷贫!", e);
mAdvertPromise = null;
}
}
}
通過上述處理认然,我們js代碼中即可同步調(diào)用showRewardVideo
方法,并根據(jù)返回結(jié)果進(jìn)行相應(yīng)的處理漫萄。
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
,或則希望把接口進(jìn)行二次封裝方便使用腾务。
為此子刮,我們可以在組件工程的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,極大簡化了引入原生組件的流程窑睁,
關(guān)于Autolinking特性的說明可參考《一文讀懂ReactNative0.60的 Autolinking 新特性》。
由于我們的安卓工程中使用了multi project結(jié)構(gòu)葵孤,我們需要指定packageImportPath
担钮,否則autolink會使用錯誤的包名。
如果是IOS平臺尤仍,需要加入.podspec文件箫津,以支持Autolinking特性
創(chuàng)建一個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 發(fā)布到npm倉庫
npm倉庫是javascript的包管理中心,全世界的開發(fā)者都把自己開發(fā)的js組件發(fā)布到這里苏遥。
我們需要把組件發(fā)布到npm倉庫中饼拍,此后便可通過npm install / yarn install
來使用。
3.4.1 注冊并登錄npm
1.在https://www.npmjs.com網(wǎng)站中創(chuàng)建你的npm賬號
2.在終端中登錄
這里需要注意田炭,因為npm官方倉庫下載慢的問題师抄,我們通常會設(shè)置為淘寶的鏡像,所以我們在登錄npm倉庫和發(fā)布時需要帶上
--registry=http://registry.npmjs.org
來指定官方倉庫地址
npm login --registry=http://registry.npmjs.org
你可以使用npm whoami
命令來確認(rèn)本地是否成功登陸認(rèn)證成功
$ qhkj npm whoami
qianhaikeji
3.4.2 修改package.json文件
package.json文件中定義了組件名教硫、版本叨吮、作者、描述瞬矩、依賴等發(fā)布信息茶鉴,你需要修改為自己的信息,比如:
{
"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 發(fā)布npm包
進(jìn)入項目目錄下
$ cd react-native-advert
$ npm publish --registry=http://registry.npmjs.org
發(fā)布成功后景用,進(jìn)入項目頁面查看是否發(fā)布成功:https://www.npmjs.com/package/react-native-advert
3.4.4 更新包版本后不生效的問題
在升級npm包的時候涵叮,很多人應(yīng)該會碰到這個問題,自己明明在npm倉庫中已經(jīng)發(fā)布了新版本伞插,但是在項目中使用yarn install
或者yarn upgrade
還是老版本割粮,這種一般都是因為我們在本地配置了淘寶鏡像源導(dǎo)致的。
淘寶的鏡像源是定時拉取同步npm主站的資源蜂怎,所以會有一定的滯后穆刻,我們需要手動同步一下。
1.打開https://npm.taobao.org/淘寶源網(wǎng)站
2.在右上角的搜索框中搜索你的包名杠步,比如react-native-advert
氢伟,進(jìn)入項目頁面
3.然后點擊SYNC
按鈕,即可完成手動同步
3.5 項目開源地址
https://github.com/qianhaikeji/react-native-advert.git
歡迎留言交流~
關(guān)于我們
我們是一個高效朵锣、熱情、有責(zé)任的技術(shù)團(tuán)隊甸私,承接各種軟件系統(tǒng)定制需求诚些。
長期招聘遠(yuǎn)程開發(fā)者,如果您喜歡嘗試新技術(shù)皇型,有一點代碼潔癖诬烹,能夠使用文檔進(jìn)行高效的溝通,React/nodejs/ES6任意一種玩的飛起弃鸦,那么绞吁,歡迎來撩~(想賺快錢的請繞道,謝謝)
簡歷請發(fā)送到:service@qianhaikeji.cn
當(dāng)然唬格,也歡迎甲方爸爸把項目甩我們臉上家破。添加微信:bdalbbtx