開發React Native原生組件-For Android

1.什么是React Native原生開發

先看一張React Native的技術架構圖(圖片來源)

image

對于一個簡單的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中,使用implementationapi關鍵字是有區別的。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按鈕,即可完成手動同步

image

3.5 項目開源地址

https://github.com/qianhaikeji/react-native-advert.git

歡迎留言交流~


關于我們

深圳市淺海科技有限公司

我們是一個高效、熱情、有責任的技術團隊,承接各種軟件系統定制需求。

長期招聘遠程開發者,如果您喜歡嘗試新技術,有一點代碼潔癖,能夠使用文檔進行高效的溝通,React/nodejs/ES6任意一種玩的飛起,那么,歡迎來撩~(想賺快錢的請繞道,謝謝)

簡歷請發送到:service@qianhaikeji.cn

當然,也歡迎甲方爸爸把項目甩我們臉上。添加微信:bdalbbtx

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容