本文均爲RN開發過程當中遇到的問題、坑點的分析及解決方案,各問題點之間無關聯,但願能幫助讀者少走彎路,持續更新中... (2019年3月29日更新)html
原文連接:http://www.kovli.com/2018/06/...java
做者:Kovlireact
在ReactNative開發過程當中,有時須要在原生端顯示RN裏的圖片,這樣的好處是能夠經過熱更新來更新APP裏的圖片,而不須要發佈原生版本,而ReactNative裏圖片路徑是相對路徑,相似'./xxximage.png'
的寫法,原生端是沒法解析這類路徑,那麼若是將RN的圖片傳遞給原生端呢?android
解決方案:ios
一、圖片若是用網絡圖,那隻須要將url字符串地址傳遞給原生便可,這種作法須要時間和網絡環境加載圖片,不屬於本地圖片,不是本方案所追求的最佳方式。git
二、懶人作法是把RN的本地圖片生成base64字符串而後傳遞給原生再解析,這種作法若是圖片太大,字符串會至關長,一樣不認爲是最佳方案。github
其實RN提供了相關的解決方法,以下:算法
RN端npm
const myImage = require('./my-image.png'); const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource'); const resolvedImage = resolveAssetSource(myImage); NativeModules.NativeBridge.showRNImage(resolvedImage);
iOS端json
#import <React/RCTConvert.h> RCT_EXPORT_METHOD(showRNImage:(id)rnImageData){ dispatch_async(dispatch_get_main_queue(), ^{ UIImage *rnImage = [RCTConvert UIImage:rnImageData]; ... }); }
安卓端
第一步,從橋接文件獲取到uri地址
@ReactMethod public static void showRNImage(Activity activity, ReadableMap params){ String rnImageUri; try { //圖片地址 rnImageUri = params.getString("uri"); Log.i("Jumping", "uri : " + uri); ... } catch (Exception e) { return; } }
第二步,建立JsDevImageLoader.java
package com.XXX; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.StrictMode; import android.support.annotation.NonNull; import android.util.Log; import com.XXX.NavigationApplication; import java.io.IOException; import java.net.URL; public class JsDevImageLoader { private static final String TAG = "JsDevImageLoader"; public static Drawable loadIcon(String iconDevUri) { try { StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitNetwork().build()); Drawable drawable = tryLoadIcon(iconDevUri); StrictMode.setThreadPolicy(threadPolicy); return drawable; } catch (Exception e) { Log.e(TAG, "Unable to load icon: " + iconDevUri); return new BitmapDrawable(); } } @NonNull private static Drawable tryLoadIcon(String iconDevUri) throws IOException { URL url = new URL(iconDevUri); Bitmap bitmap = BitmapFactory.decodeStream(url.openStream()); return new BitmapDrawable(NavigationApplication.instance.getResources(), bitmap); } }
第三步,導入ResourceDrawableIdHelper.java
package com.xg.navigation.react;// Copyright 2004-present Facebook. All Rights Reserved. import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import com.facebook.common.util.UriUtil; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** * Direct copy paste from react-native, because they made that class package scope. -_-" * Can be deleted in react-native ^0.29 */ public class ResourceDrawableIdHelper { public static final ResourceDrawableIdHelper instance = new ResourceDrawableIdHelper(); private Map<String, Integer> mResourceDrawableIdMap; public ResourceDrawableIdHelper() { mResourceDrawableIdMap = new HashMap<>(); } public int getResourceDrawableId(Context context, @Nullable String name) { if (name == null || name.isEmpty()) { return 0; } name = name.toLowerCase().replace("-", "_"); if (mResourceDrawableIdMap.containsKey(name)) { return mResourceDrawableIdMap.get(name); } int id = context.getResources().getIdentifier( name, "drawable", context.getPackageName()); mResourceDrawableIdMap.put(name, id); return id; } @Nullable public Drawable getResourceDrawable(Context context, @Nullable String name) { int resId = getResourceDrawableId(context, name); return resId > 0 ? context.getResources().getDrawable(resId) : null; } public Uri getResourceDrawableUri(Context context, @Nullable String name) { int resId = getResourceDrawableId(context, name); return resId > 0 ? new Uri.Builder() .scheme(UriUtil.LOCAL_RESOURCE_SCHEME) .path(String.valueOf(resId)) .build() : Uri.EMPTY; } }
第四步,建立BitmapUtil.java
package com.XXX; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.MediaStore; import android.text.TextUtils; import com.XXX.NavigationApplication; import com.XXX.JsDevImageLoader; import com.XXX.ResourceDrawableIdHelper; import java.io.IOException; public class BitmapUtil { private static final String FILE_SCHEME = "file"; public static Drawable loadImage(String iconSource) { if (TextUtils.isEmpty(iconSource)) { return null; } if (NavigationApplication.instance.isDebug()) { return JsDevImageLoader.loadIcon(iconSource); } else { Uri uri = Uri.parse(iconSource); if (isLocalFile(uri)) { return loadFile(uri); } else { return loadResource(iconSource); } } } private static boolean isLocalFile(Uri uri) { return FILE_SCHEME.equals(uri.getScheme()); } private static Drawable loadFile(Uri uri) { Bitmap bitmap = BitmapFactory.decodeFile(uri.getPath()); return new BitmapDrawable(NavigationApplication.instance.getResources(), bitmap); } private static Drawable loadResource(String iconSource) { return ResourceDrawableIdHelper.instance.getResourceDrawable(NavigationApplication.instance, iconSource); } public static Bitmap getBitmap(Activity activity, String uri) { if (activity == null || uri == null || TextUtils.isEmpty(uri)) { return null; } Uri mImageCaptureUri; try { mImageCaptureUri = Uri.parse(uri); } catch (Exception e) { e.printStackTrace(); return null; } if (mImageCaptureUri == null) { return null; } Bitmap bitmap = null; try { bitmap = MediaStore.Images.Media.getBitmap(activity.getContentResolver(), mImageCaptureUri); } catch (IOException e) { e.printStackTrace(); return null; } return bitmap; } }
第五步,使用第一步裏的rnImageUri地址
... BitmapUtil.loadImage(rnImageUri) ...
第六步,顯示圖片
import android.widget.RelativeLayout; import android.support.v7.widget.AppCompatImageView; import android.graphics.drawable.Drawable; ... final RelativeLayout item = (RelativeLayout) mBottomBar.getChildAt(i); final AppCompatImageView itemIcon = (AppCompatImageView) item.getChildAt(0); itemIcon.setImageDrawable(BitmapUtil.loadImage(rnImageUri)); ...
I upgraded from react-naitve 0.55.4 to react-native 0.57.0 and I get this error
bundling failed: Error: The 'decorators' plugin requires a 'decoratorsBeforeExport' option, whose value must be a boolean. If you are migrating from Babylon/Babel 6 or want to use the old decorators proposal, you should use the 'decorators-legacy' plugin instead of 'decorators'.
解決方案:參考以下例子
First install the new proposal decorators with npm install @babel/plugin-proposal-decorators --save-dev
or yarn add @babel/plugin-proposal-decorators --dev
Then, inside of your .babelrc file, change this:
{ "presets": ["react-native"], "plugins": ["transform-decorators-legacy"] } To this: { "presets": [ "module:metro-react-native-babel-preset", "@babel/preset-flow" ], "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy" : true }] ] }
EDIT:
After you've updated your .babelrc file, make sure to add preset-flow as well with the command yarn add @babel/preset-flow --dev
or npm install @babel/preset-flow --save-dev
使用這個組件KeyboardAvoidingView
本組件用於解決一個常見的尷尬問題:手機上彈出的鍵盤經常會擋住當前的視圖。本組件能夠自動根據鍵盤的位置,調整自身的position或底部的padding,以免被遮擋。
解決方案:參考以下例子
<ScrollView style={styles.container}> <KeyboardAvoidingView behavior="position" keyboardVerticalOffset={64}> ... <TextInput /> ... </KeyboardAvoidingView> </ScrollView>
這個問題關鍵在ScrollView
的keyboardShouldPersistTaps
屬性
,首先TextInput
的特殊性(有鍵盤彈起)決定了其最好包裹在ScrollView裏,其次若是當前界面有軟鍵盤,那麼點擊scrollview
後是否收起鍵盤,取決於keyboardShouldPersistTaps
屬性的設置。(譯註:不少人反應TextInput
沒法自動失去焦點/須要點擊屢次切換到其餘組件等等問題,其關鍵都是須要將TextInput
放到ScrollView
中再設置本屬性)
解決方案:看以下例子
<ScrollView style={styles.container} keyboardShouldPersistTaps="handled"> <TextInput /> ... </ScrollView> //按鈕點擊事件注意收起鍵盤 _checkAndSubmit = () => { Keyboard.dismiss(); };
<Image source={require('./icon.png'.../>
這類相對路徑地址的圖片資源如何獲取到絕對路徑)關鍵是要獲取到本地圖片的uri,用到了Image.resolveAssetSource
方法,ImageEditor.cropImage
方法和ImageStore.getBase64ForTag
方法,具體能夠查詢官方文檔
解決方案:看以下代碼
import item from '../../images/avator_upload_icon.png'; const info = Image.resolveAssetSource(item); ImageEditor.cropImage(info.uri, { size: { width: 126, height: 126 }, resizeMode: 'cover' }, uri => { ImageStore.getBase64ForTag(uri, base64ImageData => { // 獲取圖片字節碼的base64字符串 this.setState({ avatarBase64: base64ImageData }); }, err => { console.warn("ImageStoreError" + JSON.stringify(err)); }); }, err => { console.warn("ImageEditorError" + JSON.stringify(err)); });
解決方案:看以下代碼
let RNFS = require('react-native-fs'); <Image style={{width:100, height:100}} source={{uri: 'file://' + RNFS.DocumentDirectoryPath + '/myAwesomeSubDir/my.png', scale:1}}
RN圖片均須要指定寬高才會顯示,若是圖片數據的寬高不定,但又但願寬度保持不變、不一樣圖片的高度根據比例動態變化,就須要用到下面這個庫,業務場景經常使用於文章、商品詳情的多圖展現。
解決方案:使用react-native-scalable-image
從0.44版本開始,Navigator被從react native的核心組件庫中剝離到了一個名爲react-native-deprecated-custom-components
的單獨模塊中。若是你須要繼續使用Navigator,則須要先npm i facebookarchive/react-native-custom-components
安裝,而後從這個模塊中import,即import { Navigator } from 'react-native-deprecated-custom-components'
若是報錯以下參考下面的解決方案
React-Native – undefined is not an object (「evaluating _react3.default.PropTypes.shape」)
解決方案:
若是已經安裝了,先卸載npm uninstall --save react-native-deprecated-custom-components
用下面的命令安裝npm install --save https://github.com/facebookarchive/react-native-custom-components.git
在咱們使用Navigator的js文件中加入下面這個導入包就能夠了。
import { Navigator } from'react-native-deprecated-custom-components';
(注意最後有一個分號)
就能夠正常使用Navigator組件了。
因爲處理JS須要時間,APP啓動會出現一閃而過白屏,能夠經過啓動頁延遲加載方法來避免這類白屏,能夠用下面的庫
解決方案:react-native-splash-screen
不管是整包熱更新仍是差量熱更新,均須要最終替換JSBundle等文件來完成更新過程,實現原理是js來控制啓動頁的消失時間,等原生把bundle包下載(或合併成新bundle包)解壓到目錄之後,通知js消失啓動頁,因爲熱更新時間通常很短,建議使用差量熱更新,一秒左右,因此用戶等啓動頁消失後看到的就是最新的版本。
解決方案(以整包更新爲例):
[_bridge reload]
//前往更新js包 RCT_EXPORT_METHOD(gotoUpdateJS:(NSString *)jsUrl andResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ if (!jsUrl) { return; } //jsbundle更新採用靜默更新 //更新 NSLog(@"jsbundleUrl is : %@", jsUrl); [[LJFileHelper shared] downloadFileWithURLString:jsUrl finish:^(NSInteger status, id data) { if(status == 1){ NSLog(@"下載完成"); NSError *error; NSString *filePath = (NSString *)data; NSString *desPath = [NSString stringWithFormat:@"%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]]; [SSZipArchive unzipFileAtPath:filePath toDestination:desPath overwrite:YES password:nil error:&error]; if(!error){ [_bridge reload]; resolve([NSNumber numberWithBool:true]); NSLog(@"解壓成功"); }else{ resolve([NSNumber numberWithBool:false]); NSLog(@"解壓失敗"); } } }]; reject = nil; }
// 原生端經過回調結果通知JS熱更新狀況,JS端 UpdateModule.gotoUpdateJS(jsUrl).then(resp => { if ( resp ) { // 成功更新通知隱藏啓動頁 DeviceEventEmitter.emit("hide_loading_page",'hide'); } else { // 出問題也要隱藏啓動頁,用戶繼續使用舊版本 DeviceEventEmitter.emit("hide_loading_page",'hide'); // 其餘處理 } });
async componentWillMount() { this.subscription = DeviceEventEmitter.addListener("hide_loading_page", this.hideLoadingPage); appUpdateModule.updateJs(); } hideLoadingPage = ()=> { SplashScreen.hide(); };
注意作好容錯,例如弱網無網環境下的處理,熱更新失敗下次保證再次熱更新的處理,熱更新時間把控,超過期間下次再reload,是否將熱更新reload權利交給用戶等等均可以擴展。
debug模式下調試常常會有黃色的警告,有些警告多是短期不須要處理,經過下面的解決方法能忽略部分警告提示
解決方案:使用console.ignoredYellowBox
import { AppRegistry } from 'react-native'; import './app/Common/SetTheme' import './app/Common/Global' import App from './App'; console.ignoredYellowBox = ['Warning: BackAndroid is deprecated. Please use BackHandler instead.', 'source.uri should not be an empty string','Remote debugger is in a background tab which', 'Setting a timer', 'Encountered two children with the same key,', 'Attempt to read an array index', ]; AppRegistry.registerComponent('ReactNativeTemplate', () => App);
開發過程當中有時會遇到iOS圖片正常顯示,可是安卓卻只能顯示部分網絡圖片,形成這個的緣由有多種,參考下面的解決方案。
解決方案:
<Image style={styles.imageStyle} source={{uri: itemInfo.imageUrl || ''}} resizeMethod={'resize'}/>
resizeMethod官方解釋
resizeMethod enum('auto', 'resize', 'scale') 當圖片實際尺寸和容器樣式尺寸不一致時,決定以怎樣的策略來調整圖片的尺寸。默認值爲auto。 auto:使用啓發式算法來在resize和scale中自動決定。 resize: 在圖片解碼以前,使用軟件算法對其在內存中的數據進行修改。當圖片尺寸比容器尺寸大得多時,應該優先使用此選項。 scale:對圖片進行縮放。和resize相比, scale速度更快(通常有硬件加速),並且圖片質量更優。在圖片尺寸比容器尺寸小或者只是稍大一點時,應該優先使用此選項。 關於resize和scale的詳細說明請參考http://frescolib.org/docs/resizing-rotating.html.
removeClippedSubviews={true}//ios set false
還能夠謹慎嘗試使用react-native-fast-image
提早獲取用戶的網絡狀況頗有必要,RN主要靠NetInfo來獲取網絡狀態,不過隨着RN版本的更新也有一些變化。
解決方案:
this.queryConfig(); queryConfig = ()=> { this.listener = NetInfo.addEventListener('connectionChange', this._netChange); }; // 網絡發生變化時 _netChange = async(info)=> { const { type, //effectiveType } = info; const netCanUse = !(type === 'none' || type === 'unknown' || type === 'UNKNOWN' || type === 'NONE'); if (!netCanUse) { this.setState({ isNetError : true }); this.alertNetError(); //或者其餘通知形式 } else { try { // 注意這裏的await語句,其所在的函數必須有async關鍵字聲明 let response = await fetch(CONFIG_URL); let responseJson = await response.json(); const configData = responseJson.result; if (response && configData) { this.setState({ is_show_tip: configData.is_show_tip, app_bg: CONFIG_HOST + configData.app_bg, jumpUrl: configData.url, isGetConfigData: true }, () => { SplashScreen.hide(); }) } else { // 錯誤碼也去殼 if ( responseJson.code === 400 ) { this.setState({ isGetConfigData: true }, () => { SplashScreen.hide(); }) } else { this.setState({ isGetConfigData: false }, () => { SplashScreen.hide(); }) } } } catch (error) { console.log('queryConfig error:' + error); this.setState({ isGetConfigData: true }, () => { SplashScreen.hide(); }) } } }; alertNetError = () => { setTimeout(()=> { SplashScreen.hide(); }, 1000); if ( ! this.state.is_show_tip && this.state.isGetConfigData ) { return } else { Alert.alert( 'NetworkDisconnected', '', [ {text: 'NetworkDisconnected_OK', onPress: () => { this.checkNetState(); }}, ], {cancelable: false} ); } }; checkNetState = () => { NetInfo.isConnected.fetch().done((isConnected) => { if ( !isConnected ) { this.alertNetError(); } else { this.queryConfig(); } }); };
async componentWillMount() { this.queryConfig(); } checkNetState = () => { NetInfo.isConnected.fetch().done((isConnected) => { console.log('111Then, is ' + (isConnected ? 'online' : 'offline')); if (!isConnected) { this.alertNetError(); } else { this.queryConfig(); } }); }; alertNetError = () => { setTimeout(()=> { SplashScreen.hide(); }, 1000); console.log('111111'); if (!this.state.is_show_tip && this.state.isGetConfigData) { console.log('222222'); return } else { console.log('33333'); Alert.alert( 'NetworkDisconnected', '', [ { text: 'NetworkDisconnected_OK', onPress: () => { this.checkNetState(); } }, ], {cancelable: false} ); } }; queryConfig = ()=> { NetInfo.isConnected.addEventListener( 'connectionChange', this._netChange ); }; // 網絡發生變化時 _netChange = async(isConnected)=> { console.log('Then, is ' + (isConnected ? 'online' : 'offline')); if (!isConnected) { console.log('666'); this.setState({ isNetError: true }); this.alertNetError(); } else { try { // 注意這裏的await語句,其所在的函數必須有async關鍵字聲明 let response = await fetch(CONFIG_URL); let responseJson = await response.json(); const configData = responseJson.result; if (response && configData) { this.setState({ is_show_tip: configData.is_show_tip, app_bg: CONFIG_HOST + configData.app_bg, jumpUrl: configData.url, isGetConfigData: true }, () => { SplashScreen.hide(); this.componentNext(); }) } else { this.setState({ isGetConfigData: false }, () => { SplashScreen.hide(); this.componentNext(); }) } } catch (error) { console.log('queryConfig error:' + error); this.setState({ isGetConfigData: true }, () => { SplashScreen.hide(); this.componentNext(); }) } } };
使用第三方庫或者老版本升級時會遇到報錯提示某些方法被廢棄,這時候尋找和替換要花很多時間,並且還容易漏掉。
解決方案:
根據報錯信息,搜索廢棄的代碼,例如
報錯提示:Use viewPropTypes instead of View.propTypes.
搜索命令:grep -r 'View.propTypes' .
替換搜索出來的代碼便可。
這是用於查找項目裏的錯誤或者被廢棄的代碼的好方法
此問題主要體如今iOS中文輸入法沒法輸入漢字,是0.55版RN的一個bug
解決方案:使用下面的MyTextInput
替換原TextInput
import React from 'react'; import { TextInput as Input } from 'react-native'; export default class MyTextInput extends React.Component { static defaultProps = { onFocus: () => { }, }; constructor(props) { super(props); this.state = { value: this.props.value, refresh: false, }; } shouldComponentUpdate(nextProps, nextState) { if (this.state.value !== nextState.value) { return false; } return true; } componentDidUpdate(prevProps) { if (prevProps.value !== this.props.value && this.props.value === '') { this.setState({ value: '', refresh: true }, () => this.setState({ refresh: false })); } } focus = (e) => { this.input.focus(); }; onFocus = (e) => { this.input.focus(); this.props.onFocus(); }; render() { if (this.state.refresh) { return null; } return ( <Input {...this.props} ref={(ref) => { this.input = ref; }} value={this.state.value} onFocus={this.onFocus} /> ); } }
報錯信息以下
Ignoring return value of function declared with warn_unused_result attribute
解決方案:
StackOverFlow上的解決方法:
在navigator雙擊RCTWebSocket project,移除build settings > custom compiler 下的flags
版權聲明:
轉載時請註明做者Kovli以及本文地址:
http://www.kovli.com/2018/06/...