近幾個迭代版本我們大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'
學習了!