近几个迭代版本我们大iOS都是采用react-native开发的,因为技术比较新,开发过程中遇到各种各样的问题再是在所难免的。忙里偷闲,赶紧把遇到的问题及解决方案做个总结,免得日后又忘了。
问题一:
如图:
不知道大家有没有遇到过类似的问题。上图中的报错我是在3.14.0版本的开发中第一次遇到,老实说这个问题困扰了我一天,我尝试了各种方法,并且利用 Chrome 联机调试也没发现问题的所在。
这个问题发生了三次:
第一次,是在render里的ListView 组件中,只要我添加了ListView就回报错,但是将ListView替换成普通的View就没有问题了,这下好了锁定目标,一定是ListView有问题,哪里没有配置好。
第二次,发生在渲染自定义Cell中,和第一次在一个类中。
第三次,发生在条件判断中。
不要着急,下面听我给你娓娓道来
情况一:
来粘段代码看看:
render() {
if (this.state.offline) {
return (
<GuideOfflineView />
);
}
if (!this.state.data) {
return (
<LoadingView />
);
}
return (
<View style={{ flex: 1 }}>
<View style={{
width: App.Constant.screenWidth,
marginTop: 30,
marginBottom:20,
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
}}>
<TouchableOpacity style={{
marginLeft: 20,
}}
onPress={this.onCloseBtnPress}>
<Image source={App.Image.btn.guideListClose} />
</TouchableOpacity>
<Text style={{
fontSize: 17,
color: App.Color.darkGray,
textAlign: 'center',
}}>
Title
</Text>
<View style={{
marginRight: 20,
height: 13,
width: 13,
}} />
</View>
<TableView
data={this.state.data}
dataSource={this.state.dataSource}
requestData={this.loadMoreData}
/>
</View>
);
}
这里是render()页面,里面的TableView是我自定义的ListView组件,为了使代码更清晰,就把他提出来
const TableView = ({
style,
dataSource,
data,
requestData,
}) => {
const loadMoreMessage = () => {
if (data.items.length === data.total) {
return;
}
requestData();
};
if (dataSource === undefined || dataSource.getRowCount() === 0) {
return (
<View style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text style={{
alignItems: 'center',
justifyContent: 'center',
fontSize: 14,
color: App.Color.darkGray,
}}>
暂无云导游
</Text>
</View>
);
} else {
return <ListView
opacity={1}
dataSource={dataSource}
renderRow={GuideListCell}
showVerticalScrollIndicator={false}
enableEmptySections={true}
onEndReached={() => { loadMoreMessage(); } }
onEndReachedThreshold={20}
renderSeparator={(sectionID, rowID) => {
return (
<View
key={`${sectionID}-${rowID}`}
style={{
height: 0,
width: App.Constant.screenWidth,
marginTop: 20,
}} />
);
} }
/>;
}
};
在ListView中配置了 datasource 和自定义 cell,经验告诉我这里ListView出问题不是datasource有问题就是自定义cell又问题。
第一步:检查dataSource
updateDataSource(data) {
if (data !== undefined) {
let newData;
if (this.state.data !== undefined) {
newData = this.state.data;
newData.items = this.state.data.items.concat(data.items);
} else {
newData = data;
}
var ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.setState({
data: newData,
dataSource: ds.cloneWithRows(newData.items),
});
}
}
在我请求完数据后对 ListView 的 datasource 也按要求进行了配置。
这里我想稍微提一下:ListView 对它的 datasource 是有严格格式要求的,首先 datasource 必须是数组,这样它才能根据不同的 rowID 来取出对应数据进行渲染。其次,想要 ListView 使用 datasource 必须要经过 ds.cloneWithRow(dataSource) 操作,这步操作主要是为了提取新数据并进行逐行进行比较,这样ListView就知道哪些行需要重新渲染。
来打印一下数据看ListView的dataSource对不对
'--->> dataSource', { _rowHasChanged: [Function: rowHasChanged],
_getRowData: [Function: defaultGetRowData],
_sectionHeaderHasChanged: [Function],
_getSectionHeaderData: [Function: defaultGetSectionHeaderData],
_dataBlob:
{ s1:
[
{ _id: '36398d0d3c3a43cb86473f411eb4094a',
name: '台东区',
weight: 12,
items:
[ { _id: '706b5c36479742308603788a74b6781c',
location: { lat: 0, lng: 0 },
cover: { source: 'https://image-cdn.fishsaying.com/89be2d8757cf47dbb1152abb08d765b6.jpg' },
scenic_id: '36398d0d3c3a43cb86473f411eb4094a',
title: '东京台东区 大和风骨',
weight: 12 } ] },
{ _id: '9de32b5fffd7451883ff16e0905bb8e3',
name: '涩谷',
weight: 14,
items:
[ { _id: 'd643b686ad54426dbd22120861636dd2',
location: { lat: 0, lng: 0 },
cover: { source: 'https://image-cdn.fishsaying.com/a9f971ef9d7647a8aa7586431e7db972.jpg' },
scenic_id: '9de32b5fffd7451883ff16e0905bb8e3',
title: '东京涩谷区 时尚前沿',
weight: 14 } ] },
{ _id: '1c6eeeaa96f647e9b3afc9456ccb4e6e',
name: '新宿',
weight: 15,
items:
[ { _id: '0ae445cf20424c08ac12e03c500a7a46',
location: { lat: 0, lng: 0 },
cover: { source: 'https://image-cdn.fishsaying.com/d872e0876ae44481a391c17bbac76515.jpg' },
scenic_id: '1c6eeeaa96f647e9b3afc9456ccb4e6e',
title: '东京新宿区 都市传说',
weight: 15 } ] },
{ _id: '54cdcbb39a0b8ad439d0605c',
name: '塔尔寺景区',
weight: 21,
items:
[ { _id: '109e76e39d6b428a9e5c93cdc11f3367',
location: { lat: 0, lng: 0 },
cover: { source: 'https://image-cdn.fishsaying.com/485283515ff346119903fc0d000050eb.jpg' },
scenic_id: '54cdcbb39a0b8ad439d0605c',
title: '塔尔寺云导游',
weight: 21 } ] } ] },
_dirtyRows:
[ [ true,
true,
true,
true ] ],
_dirtySections: [ true ],
_cachedRowCount: 4,
rowIdentities: [ [ '0', '1', '2', '3' ] ],
sectionIdentities: [ 's1' ] }
一切正常,格式标准,说明打dataSource是没有问题的。
排除dataSource出错。
第二步:检查renderRow中配置的Cell
这里我所传进去的是自定义的Cell --> GuideListCell
import React from 'react'; // eslint-disable-line no-unused-vars
import {
View,
StyleSheet,
} from 'react-native';
import App from '.././helper/app.js';
import CloudGuideContainer from './CloudGuideContainer.js';
const Constants = {
height: App.Constant.screenWidth * 0.58,
width: App.Constant.screenWidth - 40,
};
const GuideListCell = (rowData, sectionID, rowID) => {
const name = rowData.name;
return (
<View style = {styles.container}>
<Text style = {styles.title}>
{name}
</Text>
<View style = {styles.listContainer}>
<CloudGuideContainer dataSource = {rowData} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
width:Constants.width,
height:Constants.height,
marginLeft:20,
},
listContainer: {
marginTop:20,
width:Constants.width,
height:Constants.height,
},
title: {
width:Constants.width,
fontSize: 14,
color: App.Color.darkGray,
}
});
export default GuideListCell;
看起来也没有啥问题呀,reload 一下还是报同样的错误,Chrome调试断点又不能在return的组件里面打,最后会crash在react里面的js文件中这还怎么玩?
无奈之下我用一个最笨的办法来找报错原因,把return方法中的所有代码都注释掉,只留一个<View />给这个View一个背景色,果然这样ListView出来了,不报错了找这样的方法,一个控件一个控件的打开。几分钟的时间就找到问题了 —— Text,你就是罪魁祸首。可为什么呢,于是我从第一行开始重新浏览代码,结果让我恨毒了自己
import {
View,
StyleSheet,
} from 'react-native';
这里面忘了import Text 了
So,不要对Rect的报错机制有太大幻想,的确有时他会报错说没有找个这个组件或者变量,但有的时候人家就会给你报些摸不着头脑的错误信息。
自定义Cell出了问题,这个猜测是对的。
情况二:
还是自定义GuideListCell中。正在我为了找到问题原因并顺利解决洋洋得意的时候,再次请求时,同样的错误又发生了,不同的只是这次报错的ID和上次不一样,但错误格式还是一毛一样。但刚才分明是好了的,界面完完整整的展现在我面前呀。于是我坚信这次不该是我的问题,一定是数据问题,再次打印请求结果发现,后台修改了返回数据,删除了name字段,但是我依然还在取rowData.name 字段,这时,name字段已经不存在了,当然会报错。这个锅后台Java大叔义不容辞的给背了。
情况三:
粘段代码先,
return (
<View>
{this.state.showEmptyView ?
<EmptyView/>
:
<View style={styles.listViewContainer}>
this.state.dataSource &&
<ListView
opacity={1}
dataSource={this.state.dataSource}
renderRow={ExplicitGuideCell.bind(null,this.updateDownloadStatus)}
showVerticalScrollIndicator={false}
enableEmptySections={true}
onEndReached={this.loadMoreData}
onEndReachedThreshold={20}
renderSeparator={(sectionID, rowID) => {
return (
<View
key={`${sectionID}-${rowID}`}
style={{
height: 0,
width: App.Constant.screenWidth,
marginTop: 10,
}} />
);
} }
/>
</View>
}
</View>
</Image>
</View>
);
这段代码逻辑很简单,就是如果请求回来的数据为空时,显示空页面,否则,判断this.state.datasource 是否存在,存在就显示ListView。
reload 一下,又是那个熟悉的红色界面,背心一阵寒意,这么简单的界面怎么又错了,积累了前两次的经验,我把import中的组件都检查了一遍,用到的都导入了,数据请求成功后也给this.state 设置了数据,为什么还报错呢?
的确,这里有两层条件判断,一个疏忽就会出问题,所以我还是不建议在return 组件中进行太复杂的逻辑判断。错误原因竟是一对花括号
二层逻辑判断 this.state.dataSource &&
前后忘记了花括号。
综上所述,不难看出以上三种情况都报了同样的错误,不同的只是ID。虽然错误信息让你迷茫,但这三种情况都出现在渲染界面的时候,所以均与return()方法相关。
以后若是在遇到这种错误,不妨多在渲染界面的方法中找找原因吧!
问题二:
有这样一个界面
一个列表中的cell里面又是一个列表,并且里层的列表可以向左滑动浏览。
我的想法就是,外面一个ListView,在这个ListView的Cell里面又是一个横向滑动的ListView。找到思路了那就开工吧!
结果是在 react-native 中不能直接嵌套使用 ListView,也就是说,不可以在首层的ListView的Cell 中直接再嵌套一个 ListView,就像这样
const GuideListCell = (rowData, sectionID, rowID) => {
const name = rowData.name;
return (
<View style = {styles.container}>
<Text style = {styles.title}>
{name}
</Text>
<View style = {styles.listContainer}>
<ListVeew
dataSource = {rowData}
renderRow = {CustomerCell}
/>
</View>
</View>
);
};
亲身试验这样是不行的,具体报错信息搞忘了
解决方案
不能直接嵌套,那我就间接嵌套呗!先用一个View把二层的ListView封装进去,然后在一层的ListView Cell中调用这个空间就行啦!
const GuideListCell = (rowData, sectionID, rowID) => {
const name = rowData.name;
return (
<View style = {styles.container}>
<Text style = {styles.title}>
{name}
</Text>
<View style = {styles.listContainer}>
<CloudGuideContainer dataSource = {rowData} />
</View>
</View>
);
};
其中CloudGuideContainer 就是那个包裹,用来包裹二层ListView
来看看这个包裹的真面目吧!
import React from 'react'; // eslint-disable-line no-unused-vars
import {
View,
ListView,
} from 'react-native';
import App from '.././helper/app.js';
import CloudGuideCell from './CloudGuideCell.js';
const Constants = {
height: App.Constant.screenWidth * 0.4776,
};
const CloudGuideContainer = ({dataSource}) => {
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
const guideDataSource = ds.cloneWithRows(dataSource.items);
return (
<ListView
style = {{overflow: 'visible',}}
enableEmptySections={true}
showsHorizontalScrollIndicator={false}
horizontal={true}
dataSource={guideDataSource}
renderRow={CloudGuideCell}
renderSeparator={(sectionID, rowID) => {
return (
dataSource.items.length > 1 ?
<View
key={`${sectionID}-${rowID}`}
style={{
width: 10,
height: Constants.height,
}} /> : null
);
} }
/>
);
};
export default CloudGuideContainer;
机智如我,哈哈
问题三:
关于this.state,不太了解的建议直接看官方文档
我遇到到问题是这样的,有一个列表数据很多,需要分页请求,因此我在this.state 中声明一个变量 page 初始值为 0 ,每次请求的时候给state的page加一,直到this.state.data.length === this
.state.data.total 的时候停止请求直接return,这个做法看上去是没有的问题的,但是在特殊情况下就回发生错误,比如下图是一个搜索功能,当输入关键字后点击搜索将会请求数据。但是会有这种情况,当我输完关键字后搜索了几页数据后,我点击TextInput,但是没有修改关键字,再次点击搜索这时我的page应该从1开始才对,可实验证明,page会从上一次的计数值往上加1,而不是从1开始。
下面粘上部分相关代码
export default class SearchDetail extends React.Component {
constructor(props){
super(props);
this.state = {
title: props.title,
keyword: props.keyword,
contentStr:props.keyword,
cellType:undefined,
data: undefined,
dataSource: undefined,
showEmptyView:false,
fetchingData:false,
page:0,
};
}
// 请求数据
async fetchData (keyword) {
var urlStr;
var cell;
const nextIndex = this.state.page + 1;
this.setState({
cellType: cell,
fetchingData:true,
});
try {
const result = await App.Request.get({
url: urlStr,
parameters: {
page: nextIndex,
limit: App.Constant.limit,
keyword:keyword,
}
});
this.updateDataSource(result);
} catch (error) {
this.setState({
data: [],
offline: true,
fetchingVoices: false,
});
}
}
// 点击搜索
submitKeyWords(text) {
this.setState({
showEmptyView:false,
dataSource:undefined,
data:undefined,,
page:0
});
this.fetchData(text);
}
我先搜索武侯祠相关数据,请求三页
2017-01-12 14:31:21.210 [info][tid:com.facebook.react.JavaScript] '--->> page', 1
2017-01-12 14:31:21.210172 [5377:3542880] '--->> page', 1
2017-01-12 14:31:25.409 [info][tid:com.facebook.react.JavaScript] '--->> page', 2
2017-01-12 14:31:25.409329 [5377:3542880] '--->> page', 2
2017-01-12 14:31:27.649 [info][tid:com.facebook.react.JavaScript] '--->> page', 3
2017-01-12 14:31:27.652452 [5377:3542880] '--->> page', 3
接着我不修改任何关键字,再次点击搜索
[tid:com.facebook.react.JavaScript] '--->> page', 4
2017-01-12 14:31:57.153670 [5377:3542880] '--->> page', 4
page并没有如我所愿的从1开始。也就是说我在submitKeyWords(text)方法中,在fetchData之前给将state中的page设为0,并没有起作用。说不起作用可能有些不妥,只能说在我用state的page的时候它至少还不为0。因此我推断this.setState方法可能是异步的。于是我google了一下果不其然
State Updates May Be Asynchronous
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
文档地址https://facebook.github.io/react/docs/state-and-lifecycle.html
好了,找到问题的原因了,那怎么解决呢?如果在OC中我一定会声明一个属性来做这件事,那不妨也在react中试一试,看是否可行。改一下代码:
export default class SearchDetail extends React.Component {
constructor(props){
super(props);
this.page = 0;
this.state = {
title: props.title,
keyword: props.keyword,
contentStr:props.keyword,
cellType:undefined,
data: undefined,
dataSource: undefined,
showEmptyView:false,
fetchingData:false,
};
}
async fetchData (keyword) {
var urlStr;
var cell;
const nextIndex = this.page + 1;
this.setState({
cellType: cell,
fetchingData:true,
});
this.page = nextIndex;
try {
const result = await App.Request.get({
url: urlStr,
parameters: {
page: nextIndex,
limit: App.Constant.limit,
keyword:keyword,
}
});
this.updateDataSource(result);
} catch (error) {
this.setState({
data: [],
offline: true,
fetchingVoices: false,
});
}
}
submitKeyWords(text) {
this.setState({
showEmptyView:false,
dataSource:undefined,
data:undefined,
});
this.page = 0;
this.fetchData(text);
}
reload 一下,成功!每当我点击搜索按钮page都会先被置为0。
问题四:
你是怎么设施Text组件的背景色透明呢?
一开始我用了一种很笨的方法 backgroundColor: App.Color.white.alpha(0)
简直要被自己蠢哭了
有次无意中看到别人的方法
backgroundColor: 'transparent'
学习了!