近日使用React Native Linking踩過的坑

使用react native(以下簡稱rn)開發(fā)移動端app已經(jīng)有四個月的時間了(包括第一個月的上手),感謝rn,讓前端開發(fā)人員也能夠開發(fā)原生的app。前幾天遇到一個需求:打開第三方的支付應(yīng)用并監(jiān)聽返回的結(jié)果。聽上去這個需求并不難,然而使用rn來實現(xiàn)就會遇到大大小小的坑。為了能讓其他開發(fā)人員少走彎路,在這里總結(jié)一下。

使用Linking

寫這篇博客的原因還有一個:網(wǎng)上有很多關(guān)于Linking的博客,然而有深度的文章少之又少,大部分都是簡單介紹了Linking的使用方法(我搜過好幾篇文章內(nèi)容和代碼都是一樣的)。

Linking基本使用方法

這里我建議去rn的中文官網(wǎng)學(xué)習(xí),那里講解的十分詳細。通過查看文檔我們了解到,Linking使用url來喚起系統(tǒng)應(yīng)用或鏈接。其實Linking還可以喚起其他的app,前提條件是你的手機上已經(jīng)安裝了它。

喚起其他app

使用Linking喚起其他app比較簡單,只需要簡單的兩個步驟:1.檢查該app能否被喚起,也就是檢查該app是否已安裝成功;2.喚起并傳遞參數(shù)。

Linking提供了canOpenURL這個方法,用來檢測某個url是否可以打開:

Linking.canOpenURL('appName://').then(canOpen=>{
    ...
})

使用Linking打開app也比較簡單,調(diào)用openURL方法即可:

Linking.openURL('appName://?params');

為了方便演示,我準備了兩個app:lka和lkb。這兩個應(yīng)用功能比較簡單,只含有一個button,點擊的時候喚起另外一個app,同時傳遞參數(shù)。被喚起的app獲取參數(shù)并alert出來。


image.png

image.png

現(xiàn)在,我需要在lka里喚起lkb,代碼是這樣的:

Linking.canOpenURL('lkb://').then(canOpen=>{
    if(canOpen){
        Linking.openURL('lkb://?orderId=1');
    }
});

你如果直接點擊button的話是肯定不會跳轉(zhuǎn)的,因為canOpen是false??赡苡行┤藭枺何颐髅饕呀?jīng)安裝了lkb,為什么會打不開?這里就要說到scheme了,我們可以把它理解為一個app的標識,當url的協(xié)議部分與scheme匹配時,app就會被打開。

我們需要在AndroidManifest.xml里進行相關(guān)的配置:

<activity
    android:name=".MainActivity"
/*add  -->*/ android:launchMode="singleTask"
    android:label="@string/app_name"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="stateAlwaysHidden|adjustPan"
>
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
        <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>    
    </intent-filter>
/*add start*/
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="lka" />
    </intent-filter>
/*add end*/
</activity>

我們添加了兩塊代碼:launchMode和intent-filter。關(guān)于launchMode可以參考這篇文章學(xué)習(xí)。我們新添加了一個intent-filter,關(guān)于intent-filter的相關(guān)知識可以自行上網(wǎng)搜索。Intent-filter顧名思義就是意圖過濾器,它就像過濾器一樣篩選每次傳過來的url,只要有符合條件的url就會執(zhí)行intent-filter里面的相關(guān)操作。

在本代碼中,我們在intent-filter里配置了scheme,只要url的協(xié)議為lka就會打開lka app。請注意,不要把兩個intent-filter合并到一起,雖然你的app能夠正常運行,但是你將會在手機上找不到app的圖標。

再次點擊openLkb按鈕,喚起成功。


image.png
//lkb
componentDidMount(){
    Linking.getInitialURL().then(url=>{
        alert(url);
    })
}

開始踩坑

現(xiàn)在,lka已經(jīng)能夠成功喚起lkb了,并且傳遞的參數(shù)在lkb里也能接收到,那么反過來也是一樣的?現(xiàn)在我們增加一下需求,只要lka從后臺運行到了前臺或者首次打開均彈出url。

實現(xiàn)起來比較簡單,我們需要監(jiān)聽app的運行狀態(tài),需要用到AppState:

//lka
import {
    Linking,
    AppState
} from 'react-native'
...
componentDidMount(){
    AppState.addEventListener('change',(appState)=>{
        if(appState=='active'){
            Linking.getInitialURL().then(url=>{
                alert('stateChange'+url)        
            })
        }
    })
    Linking.getInitialURL().then(url=>{
        alert('didmount:'+url);
    })
}
//lkb
openLka(){
    Linking.canOpenURL('lka://').then(res=>{
        if(res){
            Linking.openURL('lka://?name=sunnychuan&age=23');
        }
    });
}

同樣的,為lka配置好AndroidManifest.xml,把scheme配置成lka。我們首先把lka關(guān)掉,然后在lkb里喚起它,結(jié)果如下:


image.png

我們通過任務(wù)管理切回到lkb,然后點擊按鈕再次喚起lka,你得到的結(jié)果還是正確的:


image.png

先別急著高興,我們把lka和lkb都關(guān)掉,重新打開lka,你將得到“didmount:null”的結(jié)果。這是當然的,因為你是自己打開的嘛。

然后,我們通過lka喚起lkb,再通過lkb喚起lka,你得到的結(jié)果如下:


image.png

發(fā)現(xiàn)問題沒有?你可以多嘗試幾次,最終會發(fā)現(xiàn)一個規(guī)律:AppState.addEventListener里面獲取的url的值永遠與componentDidMount里直接獲取的url的值相同。只要首次獲取的是null,那么以后永遠都是null;只要首次獲取的是有值的,那么以后永遠都是有值的。

我們看一下Linking的源碼吧:

//node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java
...
@ReactMethod
public void getInitialURL(Promise promise) {
    try {
        Activity currentActivity = getCurrentActivity();
        String initialURL = null;
        if (currentActivity != null) {
            Intent intent = currentActivity.getIntent();
            String action = intent.getAction();
            Uri uri = intent.getData();
            if (Intent.ACTION_VIEW.equals(action) && uri != null) {
                initialURL = uri.toString();
            }
        }
        promise.resolve(initialURL);
    } catch (Exception e) {
        promise.reject(new JSApplicationIllegalArgumentException(
        "Could not get the initial URL : " + e.getMessage()));
      }
}

每一次調(diào)用getInitialURL,android端都會獲取當前的activity,并且返回activity對象里面的data值(uri)。

我們可以把AppState.addEventListener里面獲取的url稱為臟數(shù)據(jù)。通過上網(wǎng)翻閱相關(guān)資料后我發(fā)現(xiàn),原生的android跳轉(zhuǎn)其實是activity之間的跳轉(zhuǎn)?,F(xiàn)在回過頭來看一下我們的xml,只有一個activity。你可以嘗試一下把activity拆成兩個,其中一個專門用來配置scheme,運行結(jié)果并不符合我們的預(yù)期。

原因是什么呢?這是因為react native只配置了一個activity,整個應(yīng)用都是在這個activity里運行的。當lka尚未啟動,由lkb喚起時,lka的activity會執(zhí)行onCreate生命周期鉤子,初始化intent,此時你將會得到全新的url:null。當lka已經(jīng)運行在后臺,由lkb喚起時,lka的activity不會執(zhí)行onCreate方法,你得到的url還是舊值:null。

解決方案參考了這篇文章,在android/app/src/main/java/com/lka/MainActivity.java的最下面添加:

@Override
public void onNewIntent(Intent intent){
    super.onNewIntent(intent);
    setIntent(intent);
}

重新打包之后(每次修改android文件夾里面的東西后都需要重新打包才能生效),我們再嘗試一下:1.關(guān)掉lka和lkb;2.打開lka,你會收到null值;3.喚起lkb;4.由lkb喚起lka。你得到的結(jié)果如下:


image.png

結(jié)果與我們的預(yù)期相符。

另一個問題

其實這里還有一個潛在的問題。同樣的,通過lkb喚起lka,你將接收到正確的參數(shù)“l(fā)ka://?name=sunnychuan&age=23”。然后,我們手動將lka運行在后臺,然后重新讓它運行在前臺(不通過lkb喚起),你得到的值依舊是“l(fā)ka://?name=sunnychuan&age=23”。


image.png

image.png

image.png

從代碼上來看,這個結(jié)果是正確的,因為沒有人更改activity的url,所以值一直沒有改變;從需求上來看,這個結(jié)果是不正確的。我們假設(shè)lka在監(jiān)聽函數(shù)里獲取url的參數(shù),如果url有參數(shù)就跳轉(zhuǎn)到支付成功頁面。現(xiàn)在,只要lka由后臺運行到前臺都會跳轉(zhuǎn)到支付成功頁面(沒準真的有用戶喜歡來回切換應(yīng)用)。這樣顯然是不合理的,我們期望的是:只有l(wèi)ka是由lkb喚起的(無論lka已經(jīng)運行在后臺還是尚未啟動),才會跳轉(zhuǎn)到支付成功頁面。

我的思路是,在getInitialURL.then里,首先將activity的intent重置成默認值,這需要我們自己封裝android方法,我們先看一下封裝后的代碼:

//lka
import {
    Linking,
    AppState,
    NativeModules
} from 'react-native'
...
componentDidMount(){
    AppState.addEventListener('change',(appState)=>{
        if(appState=='active'){
            Linking.getInitialURL().then(url=>{
                NativeModules.LinkingCustom.resetURL().then(()=>{
                    alert('stateChange'+url)
                });     
            })
        }
    })
    Linking.getInitialURL().then(url=>{
        NativeModules.LinkingCustom.resetURL().then(()=>{
            alert('didmount'+url)
        }); 
    })
}

下面我們來為lka封裝一下這個方法,如果你是安卓工程師,這點操作就是小兒科;如果你是前端工程師,并且對安卓不了解,跟著我一步一步寫,很簡單。

CustomLinking

首先,我們需要在與MainActivity.java同級的目錄下新建一個java文件,導(dǎo)入必要的java包:

//android/app/src/main/java/com/lka/LinkingCustom.java
package com.lka;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;

其次,創(chuàng)建CustomLinking類,你需要繼承ReactContextBaseJavaModule類,并實現(xiàn)getName函數(shù)。這里的getName函數(shù)是必須的,返回值就是你在js端通過NativeModules拿到的模塊名"LinkingCustom"一致:

public class LinkingCustom extends ReactContextBaseJavaModule {
    public LinkingCustom(ReactApplicationContext reactContext) {
        super(reactContext);
    }
    @Override
    public String getName() {
        return "LinkingCustom";
    }
}

然后,我們實現(xiàn)重置intent的函數(shù),將其命名為resetURL:

...
@Override
public String getName() {
    return "LinkingCustom";
}
//必須添加@ReactMethod關(guān)鍵字才能在js側(cè)被調(diào)用
@ReactMethod
//不可以直接將結(jié)果return,因為js側(cè)是異步獲取結(jié)果的,這里將結(jié)果返回成promise,
public void resetURL(Promise promise) {
    try {
        Activity currentActivity = getCurrentActivity();
        if (currentActivity != null) {
            Intent intent = new Intent(Intent.ACTION_MAIN);
            currentActivity.setIntent(intent);
        }
        promise.resolve(true);
    } catch (Exception e) {
        promise.reject(new JSApplicationIllegalArgumentException("Could not reset URL"));
      }
}

LinkingCustomReactPackage

我們在同級下新建LinkingCustomReactPackage.java文件,用來注冊模塊:

//android/app/src/main/java/com/lka/LinkingCustomReactPackage.java
package com.coomarts;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
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;
//必須實現(xiàn)ReactPackage接口和createNativeModules方法
public class LinkingCustomReactPackage implements ReactPackage{
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext){
        List<NativeModule> modules=new ArrayList<>();
        //在這里添加你想注冊的模塊
        modules.add(new LinkingCustom(reactContext));
        return modules;
    }

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules(){
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContent){
        return Collections.emptyList();
    }
}

為包管理添加實例

最后一步就是在MainApplication.java里添加實例,與添加第三方組件實例相同:

//android/app/src/main/java/com/lka/MainApplication.java
...
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
        new SQLitePluginPackage(),
        new MainReactPackage(),
        new RNDeviceInfo(),
        new VectorIconsPackage(),
        new LinkingCustomReactPackage()
    );
}

大功告成,現(xiàn)在我們重復(fù)之前的步驟,看一下運行結(jié)果:

image.png

image.png

image.png

除非lka是由lkb喚起的,否則在其他情況下運行l(wèi)ka得到的均是null值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容