開發(fā)React Native原生組件-For Android

1.什么是React Native原生開發(fā)

先看一張React Native的技術(shù)架構(gòu)圖(圖片來源)

image

對于一個簡單的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中,使用implementationapi關(guān)鍵字是有區(qū)別的薄啥。implementation是用來引用在工程內(nèi)部使用的依賴辕羽,當(dāng)把當(dāng)前工程給提供給其它項目使用時,通過implementation引入的庫是不能被外部項目使用的垄惧。而通過api引入的庫的接口是可以供外部項目使用的刁愿。由于我們需要暴露qhkj-android-advert中的接口,所以此處使用api到逊,而不是implementation.

目錄結(jié)構(gòu)

如上圖铣口,

  • 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按鈕,即可完成手動同步

image

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颜说,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汰聋,更是在濱河造成了極大的恐慌门粪,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烹困,死亡現(xiàn)場離奇詭異玄妈,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)韭邓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門措近,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人女淑,你說我怎么就攤上這事瞭郑。” “怎么了鸭你?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵屈张,是天一觀的道長。 經(jīng)常有香客問我袱巨,道長阁谆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任愉老,我火速辦了婚禮场绿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嫉入。我一直安慰自己焰盗,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布咒林。 她就那樣靜靜地躺著熬拒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪垫竞。 梳的紋絲不亂的頭發(fā)上澎粟,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天,我揣著相機(jī)與錄音欢瞪,去河邊找鬼活烙。 笑死,一個胖子當(dāng)著我的面吹牛遣鼓,可吹牛的內(nèi)容都是我干的瓣颅。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼譬正,長吁一口氣:“原來是場噩夢啊……” “哼宫补!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起曾我,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤粉怕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抒巢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贫贝,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年蛉谜,在試婚紗的時候發(fā)現(xiàn)自己被綠了稚晚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡型诚,死狀恐怖客燕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狰贯,我是刑警寧澤也搓,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站涵紊,受9級特大地震影響傍妒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摸柄,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一颤练、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驱负,春花似錦嗦玖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至匾乓,卻和暖如春捞稿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拼缝。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工娱局, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咧七。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓衰齐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親继阻。 傳聞我的和親對象是個殘疾皇子耻涛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容