title: RN實戰(zhàn)經(jīng)驗總結(jié)
前言
在草稿箱中發(fā)現(xiàn)了許久之前寫的這篇文章,雖然不搞RN已經(jīng)大半年了,但是之前寫過的東西還是還出來作個紀(jì)念。如果能幫到別人那再好不過了。
Android中集成RN
這個需要說的都在這里說了,見在原有Android項目中快速集成React Native
關(guān)于RN在項目中Android端的預(yù)加載
此前曾根據(jù)網(wǎng)上的做法并結(jié)合最新的RN源碼做了一個RN的預(yù)加載庫,不過在后來發(fā)現(xiàn)會出現(xiàn)內(nèi)存泄漏問題。在集成到項目Android端此著手解決了這一問題。
然而在實際的開發(fā)中,幾乎有大半的頁面是用RN開發(fā),如果全部頁面都使用預(yù)加載,那么對內(nèi)存會有很大的壓力,而且也沒有這個必要。
首先說一下目前項目的頁面組織結(jié)構(gòu),其實就是目前主流的主Activity(帶四個Fragment)+其他Activity,主Activity在應(yīng)用運行期間是一直存在的,這就為預(yù)加載提供了一個絕佳的基礎(chǔ)。
最終使用預(yù)加載的是主Activity【我的】Fragment頁面。在RN中加載Fragment并不難,在Android中加載RN,無論是在Activity還是Fragment,加載的都只是一個View而已。而給Fragment設(shè)置View,只需要Fragment的onCreateView返回RN的View即可。
具體見:在Android中預(yù)加載React Native jsBundle
優(yōu)化非預(yù)加載初始化屬性傳遞
在原本的ReactActivity中傳遞啟動屬性可以用以下方式
public class C3RNActivity extends ReactActivity {
public static final String MAIN_COMPONENT_NAME = C3RNActivity.class.getSimpleName();
protected @Nullable
String getMainComponentName() {
return MAIN_COMPONENT_NAME;
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Nullable
@Override
protected Bundle getLaunchOptions() {
Bundle bundle=new Bundle();
//往bundle中添加啟動屬性鍵值對
bundle.putString("key","value");
return bundle;
}
};
}
}
這種方式傳遞是完全沒問題的,但是有點局限性。查看ReactActivity的源碼,createReactActivityDelegate是在ReactActivity的構(gòu)造方法調(diào)用(在OnCreate之前)。但這樣一來就無法在OnCreate通過getItent獲取別的Activity傳遞過來的參數(shù),因此我們需要對原本的ReactActivity進行改造。將createReactActivityDelegate方法調(diào)用從ReactActivity移到onCreate方法中,但是在ReactActivityDelegate的onCreate方法之前。這樣我們需要重寫ReactActivity而不是直接通過繼承創(chuàng)建滿足我們要求的ReactActivity。
public class MyReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private MyReactActivityDelegate mDelegate;
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
* e.g. "MoviesApp"
*/
protected @Nullable
String getMainComponentName() {
return null;
}
/**
* Called at construction time, override if you have a custom delegate implementation.
*/
protected MyReactActivityDelegate createReactActivityDelegate(final Intent intent) {
return new MyReactActivityDelegate(this, getMainComponentName()){
@Nullable
@Override
protected Bundle getLaunchOptions() {
Bundle bundle=new Bundle();
//在這里將intent參數(shù)放入bundle,作為RN的頁面啟動參數(shù)
//例如:
bundle.putString("key",intent.getStringExtra("xxx"));
return bundle;
}
};
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent=getIntent();
mDelegate = createReactActivityDelegate(intent);
mDelegate.onCreate(savedInstanceState);
}
@Override
protected void onPause() {
super.onPause();
mDelegate.onPause();
}
@Override
protected void onResume() {
super.onResume();
mDelegate.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
mDelegate.onDestroy();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mDelegate.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed() {
if (!mDelegate.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
if (!mDelegate.onNewIntent(intent)) {
super.onNewIntent(intent);
}
}
@Override
public void requestPermissions(
String[] permissions,
int requestCode,
PermissionListener listener) {
mDelegate.requestPermissions(permissions, requestCode, listener);
}
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults) {
mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
protected final ReactNativeHost getReactNativeHost() {
return mDelegate.getReactNativeHost();
}
protected final ReactInstanceManager getReactInstanceManager() {
return mDelegate.getReactInstanceManager();
}
}
其中,MyReactActivityDelegate 是直接繼承ReactActivityDelegate,因為在ReactActivityDelegate中,onCreate,onPause,onDestroy等方法是protect修飾,無法在其他包中引用,所以需要對其復(fù)寫,實現(xiàn)中只需要調(diào)用父類方法即可。
多入口
在本次項目中使用的是多注冊方式實現(xiàn)RN的多入口,實際上通過啟動屬性傳遞需要打開的RN頁面參數(shù)也是可以的。不過因為使用多注冊實現(xiàn)多入口還是踩了一些坑。在多注冊方式下,RN的全局變量在iOS客戶端是無效的。也就是說,在一個根組件中給一個全局變量賦值,在另外一個根組件中讀取到的全局變量值是空的。而在Android端是沒有這個問題。
網(wǎng)絡(luò)圖片加載過渡
這里不得不提這是RN的一個坑,最新的RN都發(fā)布到0.5x了,在Android中依然沒有支持默認占位圖,默認加載錯誤圖,以及加載進度方法。這三個都只有在iOS端有效,在Android端則需要自己手動實現(xiàn)。
網(wǎng)絡(luò)狀況判斷
這里不得不提這又是RN的一個坑。在RN中官網(wǎng)推薦判斷網(wǎng)絡(luò)是否可用的方法如下:
NetInfo.isConnected.fetch().done(
(isConnected) => { this.setState({isConnected}); }
);
然而實際上在iOS端,isConnected返回的永遠是false。
官方文檔說上面這個方法是Android和iOS平臺通用的,然而你實際使用的時候在iOS端就會發(fā)現(xiàn)問題,即使到了0.51,這個問題仍然存在.....
其實這個解決辦法很多,
具體可見:Github issue:iOS: NetInfo.isConnected returns always false
其中一種解決辦法如下:
function handleFirstConnectivityChange(isConnected) {
if (!sConnected) {
// do action
}
NetInfo.isConnected.removeEventListener('change', handleFirstConnectivityChange);
}
if (Platform.OS === 'ios') {
NetInfo.isConnected.addEventListener('change', handleFirstConnectivityChange);
} else {
NetInfo.isConnected.fetch().then(isConnected => {
if (!sConnected) {
// do action
}
}
Linking模塊在Android release模式下getInitialURL返回為null
這也是一個大坑。在RN中,如果你的應(yīng)用被其注冊過的外部url調(diào)起,則可以在任何組件內(nèi)這樣獲取和處理它:
componentDidMount() {
Linking.getInitialURL().then((url) => {
if (url) {
console.log('Initial url is: ' + url);
}
}).catch(err => console.error('An error occurred', err));
}
然而在Android端打成Release包時,返回的url偶爾會為空,對,是偶爾,并且概率還比較大,原因暫時未知。所以要通過外部鏈接和Linking模塊來打開RN的話,這種做法是不靠譜的。解決辦法是用Android原生的老辦法,在Activity的onCreate方法中獲取外部鏈接以及相關(guān)參數(shù),并作為啟動參數(shù)傳遞給RN。為此,需要重寫ReactActivity和ReactActivityDelegate。具體參考:優(yōu)化非預(yù)加載初始化屬性傳遞一節(jié)。
總結(jié):
從17年4月份開始接觸RN,至今如有大半年時間,在這大半年時間里,從入門學(xué)習(xí)到實際動手寫出一個完整的仿實際產(chǎn)品的App出來花了一個月時間,與當(dāng)初學(xué)習(xí)Android相比這個時間短得太多了。到17年6月份在我們公司的天翼云iOS客戶端其中一個頁面試點使用RN,然后前后花了一個月時間,但實際動手接入項目中與從零開始一個RN項目有很大的不同,期間踩了好幾個坑,還好都能及時解決。到17年10月份,在我們公司的產(chǎn)品兩個客戶端都接入RN并且是重度使用,大概有50%~60%頁面是使用RN開發(fā)。在這一次接近2個月的開發(fā)過程中,對RN簡直又愛又恨,踩了大大小小好多個坑,看到了RN的許多不足,也看到了原生與RN無法比擬的一些優(yōu)勢。
先來說一下切身體會的優(yōu)勢:
- 上手快,即使不懂JS,入門也不用太長時間,半個月時間其實就足夠了。上手之后,開發(fā)效率其實可以很高。
- 跨平臺,這是一個巨大的優(yōu)勢,雖然RN的代碼不能做到100%兩個端復(fù)用,但是90%還是沒問題的。而自然地,可以節(jié)省一定的人力成本。
- 熱更新,這一功能在Android中實現(xiàn)比較簡單,但是因為蘋果爸爸禁用了JSPatch,因此在iOS端能用的熱更新方法不多了,而熱更新則是其中一個。
- 更新快,這其實是一個優(yōu)點也可以說是缺點,說它是優(yōu)點因為勤快地更新則說明RN加了某些新特性或者修復(fù)了一些歷史遺留的bug,說它是缺點則是因為更新太快,說不定某些API哪天突然就不能用了,代碼的寫法又不一樣了,RN版本升級的時候也是一件比較痛苦的事情。
- 調(diào)試方便:可以在谷歌瀏覽器上面單步調(diào)試JS代碼,雙擊R(或搖一搖或CMD+R)就能快速reload代碼
不足:
- 開發(fā)過程會時不時就踩到坑,RN作為一個還沒正式發(fā)布1.0版本的框架,有一些bug是必然的。
- 列表控件性能仍不能滿足要求,在快速滑動時會看到一些空白項。
- 圖片緩存:官方?jīng)]有很好的支持,第三方庫也沒有找到比較滿意的方案。
- 動畫效果不佳:這個眾所周知,動畫效果需要自己做優(yōu)化。