使用react native(以下簡(jiǎn)稱rn)開(kāi)發(fā)移動(dòng)端app已經(jīng)有四個(gè)月的時(shí)間了(包括第一個(gè)月的上手),感謝rn,讓前端開(kāi)發(fā)人員也能夠開(kāi)發(fā)原生的app蒿辙。前幾天遇到一個(gè)需求:打開(kāi)第三方的支付應(yīng)用并監(jiān)聽(tīng)返回的結(jié)果自阱。聽(tīng)上去這個(gè)需求并不難,然而使用rn來(lái)實(shí)現(xiàn)就會(huì)遇到大大小小的坑关拒。為了能讓其他開(kāi)發(fā)人員少走彎路,在這里總結(jié)一下庸娱。
使用Linking
寫這篇博客的原因還有一個(gè):網(wǎng)上有很多關(guān)于Linking的博客着绊,然而有深度的文章少之又少,大部分都是簡(jiǎn)單介紹了Linking的使用方法(我搜過(guò)好幾篇文章內(nèi)容和代碼都是一樣的)熟尉。
Linking基本使用方法
這里我建議去rn的中文官網(wǎng)學(xué)習(xí)归露,那里講解的十分詳細(xì)。通過(guò)查看文檔我們了解到斤儿,Linking使用url來(lái)喚起系統(tǒng)應(yīng)用或鏈接剧包。其實(shí)Linking還可以喚起其他的app,前提條件是你的手機(jī)上已經(jīng)安裝了它往果。
喚起其他app
使用Linking喚起其他app比較簡(jiǎn)單疆液,只需要簡(jiǎn)單的兩個(gè)步驟:1.檢查該app能否被喚起,也就是檢查該app是否已安裝成功陕贮;2.喚起并傳遞參數(shù)堕油。
Linking提供了canOpenURL這個(gè)方法,用來(lái)檢測(cè)某個(gè)url是否可以打開(kāi):
Linking.canOpenURL('appName://').then(canOpen=>{
...
})
使用Linking打開(kāi)app也比較簡(jiǎn)單飘蚯,調(diào)用openURL方法即可:
Linking.openURL('appName://?params');
為了方便演示馍迄,我準(zhǔn)備了兩個(gè)app:lka和lkb。這兩個(gè)應(yīng)用功能比較簡(jiǎn)單局骤,只含有一個(gè)button攀圈,點(diǎn)擊的時(shí)候喚起另外一個(gè)app,同時(shí)傳遞參數(shù)峦甩。被喚起的app獲取參數(shù)并alert出來(lái)赘来。
現(xiàn)在现喳,我需要在lka里喚起lkb,代碼是這樣的:
Linking.canOpenURL('lkb://').then(canOpen=>{
if(canOpen){
Linking.openURL('lkb://?orderId=1');
}
});
你如果直接點(diǎn)擊button的話是肯定不會(huì)跳轉(zhuǎn)的犬辰,因?yàn)閏anOpen是false嗦篱。可能有些人會(huì)問(wèn):我明明已經(jīng)安裝了lkb幌缝,為什么會(huì)打不開(kāi)灸促?這里就要說(shuō)到scheme了,我們可以把它理解為一個(gè)app的標(biāo)識(shí)涵卵,當(dāng)url的協(xié)議部分與scheme匹配時(shí)浴栽,app就會(huì)被打開(kāi)。
我們需要在AndroidManifest.xml里進(jìn)行相關(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í)典鸡。我們新添加了一個(gè)intent-filter,關(guān)于intent-filter的相關(guān)知識(shí)可以自行上網(wǎng)搜索坏晦。Intent-filter顧名思義就是意圖過(guò)濾器萝玷,它就像過(guò)濾器一樣篩選每次傳過(guò)來(lái)的url,只要有符合條件的url就會(huì)執(zhí)行intent-filter里面的相關(guān)操作昆婿。
在本代碼中球碉,我們?cè)趇ntent-filter里配置了scheme,只要url的協(xié)議為lka就會(huì)打開(kāi)lka app仓蛆。請(qǐng)注意汁尺,不要把兩個(gè)intent-filter合并到一起,雖然你的app能夠正常運(yùn)行多律,但是你將會(huì)在手機(jī)上找不到app的圖標(biāo)。
再次點(diǎn)擊openLkb按鈕搂蜓,喚起成功狼荞。
//lkb
componentDidMount(){
Linking.getInitialURL().then(url=>{
alert(url);
})
}
開(kāi)始踩坑
現(xiàn)在,lka已經(jīng)能夠成功喚起lkb了帮碰,并且傳遞的參數(shù)在lkb里也能接收到相味,那么反過(guò)來(lái)也是一樣的?現(xiàn)在我們?cè)黾右幌滦枨笱惩欤灰猯ka從后臺(tái)運(yùn)行到了前臺(tái)或者首次打開(kāi)均彈出url丰涉。
實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,我們需要監(jiān)聽(tīng)app的運(yùn)行狀態(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é)果如下:
我們通過(guò)任務(wù)管理切回到lkb,然后點(diǎn)擊按鈕再次喚起lka伪煤,你得到的結(jié)果還是正確的:
先別急著高興加袋,我們把lka和lkb都關(guān)掉,重新打開(kāi)lka抱既,你將得到“didmount:null”的結(jié)果职烧。這是當(dāng)然的,因?yàn)槟闶亲约捍蜷_(kāi)的嘛防泵。
然后蚀之,我們通過(guò)lka喚起lkb,再通過(guò)lkb喚起lka择克,你得到的結(jié)果如下:
發(fā)現(xiàn)問(wèn)題沒(méi)有恬总?你可以多嘗試幾次,最終會(huì)發(fā)現(xiàn)一個(gè)規(guī)律:AppState.addEventListener
里面獲取的url的值永遠(yuǎn)與componentDidMount
里直接獲取的url的值相同肚邢。只要首次獲取的是null壹堰,那么以后永遠(yuǎn)都是null;只要首次獲取的是有值的骡湖,那么以后永遠(yuǎn)都是有值的贱纠。
我們看一下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端都會(huì)獲取當(dāng)前的activity响蕴,并且返回activity對(duì)象里面的data值(uri)谆焊。
我們可以把AppState.addEventListener
里面獲取的url稱為臟數(shù)據(jù)。通過(guò)上網(wǎng)翻閱相關(guān)資料后我發(fā)現(xiàn)浦夷,原生的android跳轉(zhuǎn)其實(shí)是activity之間的跳轉(zhuǎn)∠绞裕現(xiàn)在回過(guò)頭來(lái)看一下我們的xml,只有一個(gè)activity劈狐。你可以嘗試一下把a(bǔ)ctivity拆成兩個(gè)罐孝,其中一個(gè)專門用來(lái)配置scheme,運(yùn)行結(jié)果并不符合我們的預(yù)期肥缔。
原因是什么呢莲兢?這是因?yàn)閞eact native只配置了一個(gè)activity,整個(gè)應(yīng)用都是在這個(gè)activity里運(yùn)行的续膳。當(dāng)lka尚未啟動(dòng)改艇,由lkb喚起時(shí),lka的activity會(huì)執(zhí)行onCreate
生命周期鉤子坟岔,初始化intent谒兄,此時(shí)你將會(huì)得到全新的url:null。當(dāng)lka已經(jīng)運(yùn)行在后臺(tái)炮车,由lkb喚起時(shí)舵变,lka的activity不會(huì)執(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文件夾里面的東西后都需要重新打包才能生效)赊豌,我們?cè)賴L試一下:1.關(guān)掉lka和lkb;2.打開(kāi)lka绵咱,你會(huì)收到null值碘饼;3.喚起lkb;4.由lkb喚起lka悲伶。你得到的結(jié)果如下:
結(jié)果與我們的預(yù)期相符艾恼。
另一個(gè)問(wèn)題
其實(shí)這里還有一個(gè)潛在的問(wèn)題。同樣的麸锉,通過(guò)lkb喚起lka钠绍,你將接收到正確的參數(shù)“l(fā)ka://?name=sunnychuan&age=23”。然后花沉,我們手動(dòng)將lka運(yùn)行在后臺(tái)柳爽,然后重新讓它運(yùn)行在前臺(tái)(不通過(guò)lkb喚起),你得到的值依舊是“l(fā)ka://?name=sunnychuan&age=23”碱屁。
從代碼上來(lái)看磷脯,這個(gè)結(jié)果是正確的,因?yàn)闆](méi)有人更改activity的url娩脾,所以值一直沒(méi)有改變赵誓;從需求上來(lái)看,這個(gè)結(jié)果是不正確的柿赊。我們假設(shè)lka在監(jiān)聽(tīng)函數(shù)里獲取url的參數(shù)俩功,如果url有參數(shù)就跳轉(zhuǎn)到支付成功頁(yè)面。現(xiàn)在碰声,只要lka由后臺(tái)運(yùn)行到前臺(tái)都會(huì)跳轉(zhuǎn)到支付成功頁(yè)面(沒(méi)準(zhǔn)真的有用戶喜歡來(lái)回切換應(yīng)用)绑雄。這樣顯然是不合理的,我們期望的是:只有l(wèi)ka是由lkb喚起的(無(wú)論lka已經(jīng)運(yùn)行在后臺(tái)還是尚未啟動(dòng))奥邮,才會(huì)跳轉(zhuǎn)到支付成功頁(yè)面。
我的思路是罗珍,在getInitialURL.then
里洽腺,首先將activity的intent重置成默認(rèn)值,這需要我們自己封裝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)
});
})
}
下面我們來(lái)為lka封裝一下這個(gè)方法蘸朋,如果你是安卓工程師,這點(diǎn)操作就是小兒科扣唱;如果你是前端工程師藕坯,并且對(duì)安卓不了解团南,跟著我一步一步寫,很簡(jiǎn)單炼彪。
CustomLinking
首先吐根,我們需要在與MainActivity.java同級(jí)的目錄下新建一個(gè)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類,并實(shí)現(xiàn)getName函數(shù)喜爷。這里的getName函數(shù)是必須的冗疮,返回值就是你在js端通過(guò)NativeModules拿到的模塊名"LinkingCustom"一致:
public class LinkingCustom extends ReactContextBaseJavaModule {
public LinkingCustom(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "LinkingCustom";
}
}
然后,我們實(shí)現(xiàn)重置intent的函數(shù)檩帐,將其命名為resetURL:
...
@Override
public String getName() {
return "LinkingCustom";
}
//必須添加@ReactMethod關(guān)鍵字才能在js側(cè)被調(diào)用
@ReactMethod
//不可以直接將結(jié)果return术幔,因?yàn)閖s側(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
我們?cè)谕?jí)下新建LinkingCustomReactPackage.java文件诅挑,用來(lái)注冊(cè)模塊:
//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;
//必須實(shí)現(xiàn)ReactPackage接口和createNativeModules方法
public class LinkingCustomReactPackage implements ReactPackage{
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext){
List<NativeModule> modules=new ArrayList<>();
//在這里添加你想注冊(cè)的模塊
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();
}
}
為包管理添加實(shí)例
最后一步就是在MainApplication.java里添加實(shí)例,與添加第三方組件實(shí)例相同:
//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ù)之前的步驟揍障,看一下運(yùn)行結(jié)果:
除非lka是由lkb喚起的,否則在其他情況下運(yùn)行l(wèi)ka得到的均是null值俩由。