本文均为 RN 开发过程中遇到的问题、坑点的分析及解决方案,各问题点之间无关联,希望能帮助读者少走弯路,持续更新中... ( 2018 年 12 月 19 日更新)
原文链接: http://www.kovli.com/2018/06/25/rn-anything/
作者:Kovli
使用这个组件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-image-progress
还可以谨慎尝试使用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/25/rn-anything/
1
kovli OP 不可以编辑主题,好吧,以后通过回复来更新我的博文。
- 升级旧 RN 版本到目前最新的 0.57.8 如果采用手动升级需要注意如下。 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 |
2
kovli OP - 如何在原生端( iOS 和 android 两个平台)使用 ReactNative 里的本地图片(路径类似 require('./xxximage.png'))。
在 ReactNative 开发过程中,有时需要在原生端显示 RN 里的图片,这样的好处是可以通过热更新来更新 APP 里的图片,而不需要发布原生版本,而 ReactNative 里图片路径是相对路径,类似'./xxximage.png'的写法,原生端是无法解析这类路径,那么如果将 RN 的图片传递给原生端呢? 解决方案: 1、图片如果用网络图,那只需要将 url 字符串地址传递给原生即可,这种做法需要时间和网络环境加载图片,不属于本地图片,不是本方案所追求的最佳方式。 2、懒人做法是把 RN 的本地图片生成 base64 字符串然后传递给原生再解析,这种做法如果图片太大,字符串会相当长,同样不认为是最佳方案。 其实 RN 提供了相关的解决方法,如下: RN 端 const myImage = require('./my-image.png'); const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource'); const resolvedImage = resolveAssetSource(myImage); NativeModules.NativeBridge.showRNImage(resolvedImage); iOS 端 #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)); ... |