ReactNative開發(fā)過(guò)程中的那些磕磕絆絆

近幾個(gè)迭代版本我們大iOS都是采用react-native開發(fā)的,因?yàn)榧夹g(shù)比較新,開發(fā)過(guò)程中遇到各種各樣的問(wèn)題再是在所難免的。忙里偷閑,趕緊把遇到的問(wèn)題及解決方案做個(gè)總結(jié),免得日后又忘了。

問(wèn)題一:

如圖:

報(bào)錯(cuò)信息.png

不知道大家有沒有遇到過(guò)類似的問(wèn)題。上圖中的報(bào)錯(cuò)我是在3.14.0版本的開發(fā)中第一次遇到,老實(shí)說(shuō)這個(gè)問(wèn)題困擾了我一天,我嘗試了各種方法,并且利用 Chrome 聯(lián)機(jī)調(diào)試也沒發(fā)現(xiàn)問(wèn)題的所在。
這個(gè)問(wèn)題發(fā)生了三次:

第一次,是在render里的ListView 組件中,只要我添加了ListView就回報(bào)錯(cuò),但是將ListView替換成普通的View就沒有問(wèn)題了,這下好了鎖定目標(biāo),一定是ListView有問(wèn)題,哪里沒有配置好。

第二次,發(fā)生在渲染自定義Cell中,和第一次在一個(gè)類中。

第三次,發(fā)生在條件判斷中。

不要著急,下面聽我給你娓娓道來(lái)

情況一:

來(lái)粘段代碼看看:

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()頁(yè)面,里面的TableView是我自定義的ListView組件,為了使代碼更清晰,就把他提出來(lái)

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,
                }}>
                    暫無(wú)云導(dǎo)游
                </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,經(jīng)驗(yàn)告訴我這里L(fēng)istView出問(wèn)題不是datasource有問(wèn)題就是自定義cell又問(wèn)題。

第一步:檢查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),
            });
        }
    }

在我請(qǐng)求完數(shù)據(jù)后對(duì) ListView 的 datasource 也按要求進(jìn)行了配置。

這里我想稍微提一下:ListView 對(duì)它的 datasource 是有嚴(yán)格格式要求的,首先 datasource 必須是數(shù)組,這樣它才能根據(jù)不同的 rowID 來(lái)取出對(duì)應(yīng)數(shù)據(jù)進(jìn)行渲染。其次,想要 ListView 使用 datasource 必須要經(jīng)過(guò) ds.cloneWithRow(dataSource) 操作,這步操作主要是為了提取新數(shù)據(jù)并進(jìn)行逐行進(jìn)行比較,這樣ListView就知道哪些行需要重新渲染。

來(lái)打印一下數(shù)據(jù)看ListView的dataSource對(duì)不對(duì)

'--->> dataSource', { _rowHasChanged: [Function: rowHasChanged],
  _getRowData: [Function: defaultGetRowData],
  _sectionHeaderHasChanged: [Function],
  _getSectionHeaderData: [Function: defaultGetSectionHeaderData],
  _dataBlob: 
   { s1: 
      [ 
        { _id: '36398d0d3c3a43cb86473f411eb4094a',
          name: '臺(tái)東區(qū)',
          weight: 12,
          items: 
           [ { _id: '706b5c36479742308603788a74b6781c',
               location: { lat: 0, lng: 0 },
               cover: { source: 'https://image-cdn.fishsaying.com/89be2d8757cf47dbb1152abb08d765b6.jpg' },
               scenic_id: '36398d0d3c3a43cb86473f411eb4094a',
               title: '東京臺(tái)東區(qū) 大和風(fēng)骨',
               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: '東京澀谷區(qū) 時(shí)尚前沿',
               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: '東京新宿區(qū) 都市傳說(shuō)',
               weight: 15 } ] },
        { _id: '54cdcbb39a0b8ad439d0605c',
          name: '塔爾寺景區(qū)',
          weight: 21,
          items: 
           [ { _id: '109e76e39d6b428a9e5c93cdc11f3367',
               location: { lat: 0, lng: 0 },
               cover: { source: 'https://image-cdn.fishsaying.com/485283515ff346119903fc0d000050eb.jpg' },
               scenic_id: '54cdcbb39a0b8ad439d0605c',
               title: '塔爾寺云導(dǎo)游',
               weight: 21 } ] } ] },
  _dirtyRows: 
   [ [ true,
       true,
       true,
       true ] ],
  _dirtySections: [ true ],
  _cachedRowCount: 4,
  rowIdentities: [ [ '0', '1', '2', '3' ] ],
  sectionIdentities: [ 's1' ] }

一切正常,格式標(biāo)準(zhǔn),說(shuō)明打dataSource是沒有問(wèn)題的。

排除dataSource出錯(cuò)。

第二步:檢查renderRow中配置的Cell

這里我所傳進(jìn)去的是自定義的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;

看起來(lái)也沒有啥問(wèn)題呀,reload 一下還是報(bào)同樣的錯(cuò)誤,Chrome調(diào)試斷點(diǎn)又不能在return的組件里面打,最后會(huì)crash在react里面的js文件中這還怎么玩?

Chrome調(diào)試報(bào)錯(cuò)信息.png

無(wú)奈之下我用一個(gè)最笨的辦法來(lái)找報(bào)錯(cuò)原因,把return方法中的所有代碼都注釋掉,只留一個(gè)<View />給這個(gè)View一個(gè)背景色,果然這樣ListView出來(lái)了,不報(bào)錯(cuò)了找這樣的方法,一個(gè)控件一個(gè)控件的打開。幾分鐘的時(shí)間就找到問(wèn)題了 —— Text,你就是罪魁禍?zhǔn)住?蔀槭裁茨兀谑俏覐牡谝恍虚_始重新瀏覽代碼,結(jié)果讓我恨毒了自己

import {
    View,
    StyleSheet,
} from 'react-native';

這里面忘了import Text 了

So,不要對(duì)Rect的報(bào)錯(cuò)機(jī)制有太大幻想,的確有時(shí)他會(huì)報(bào)錯(cuò)說(shuō)沒有找個(gè)這個(gè)組件或者變量,但有的時(shí)候人家就會(huì)給你報(bào)些摸不著頭腦的錯(cuò)誤信息。

自定義Cell出了問(wèn)題,這個(gè)猜測(cè)是對(duì)的。

情況二:

還是自定義GuideListCell中。正在我為了找到問(wèn)題原因并順利解決洋洋得意的時(shí)候,再次請(qǐng)求時(shí),同樣的錯(cuò)誤又發(fā)生了,不同的只是這次報(bào)錯(cuò)的ID和上次不一樣,但錯(cuò)誤格式還是一毛一樣。但剛才分明是好了的,界面完完整整的展現(xiàn)在我面前呀。于是我堅(jiān)信這次不該是我的問(wèn)題,一定是數(shù)據(jù)問(wèn)題,再次打印請(qǐng)求結(jié)果發(fā)現(xiàn),后臺(tái)修改了返回?cái)?shù)據(jù),刪除了name字段,但是我依然還在取rowData.name 字段,這時(shí),name字段已經(jīng)不存在了,當(dāng)然會(huì)報(bào)錯(cuò)。這個(gè)鍋后臺(tái)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>
        );

這段代碼邏輯很簡(jiǎn)單,就是如果請(qǐng)求回來(lái)的數(shù)據(jù)為空時(shí),顯示空頁(yè)面,否則,判斷this.state.datasource 是否存在,存在就顯示ListView。
reload 一下,又是那個(gè)熟悉的紅色界面,背心一陣寒意,這么簡(jiǎn)單的界面怎么又錯(cuò)了,積累了前兩次的經(jīng)驗(yàn),我把import中的組件都檢查了一遍,用到的都導(dǎo)入了,數(shù)據(jù)請(qǐng)求成功后也給this.state 設(shè)置了數(shù)據(jù),為什么還報(bào)錯(cuò)呢?
的確,這里有兩層條件判斷,一個(gè)疏忽就會(huì)出問(wèn)題,所以我還是不建議在return 組件中進(jìn)行太復(fù)雜的邏輯判斷。錯(cuò)誤原因竟是一對(duì)花括號(hào)

二層邏輯判斷 this.state.dataSource && 前后忘記了花括號(hào)。

綜上所述,不難看出以上三種情況都報(bào)了同樣的錯(cuò)誤,不同的只是ID。雖然錯(cuò)誤信息讓你迷茫,但這三種情況都出現(xiàn)在渲染界面的時(shí)候,所以均與return()方法相關(guān)。

以后若是在遇到這種錯(cuò)誤,不妨多在渲染界面的方法中找找原因吧!

問(wèn)題二:

有這樣一個(gè)界面

列表.png

一個(gè)列表中的cell里面又是一個(gè)列表,并且里層的列表可以向左滑動(dòng)瀏覽。
我的想法就是,外面一個(gè)ListView,在這個(gè)ListView的Cell里面又是一個(gè)橫向滑動(dòng)的ListView。找到思路了那就開工吧!

結(jié)果是在 react-native 中不能直接嵌套使用 ListView,也就是說(shuō),不可以在首層的ListView的Cell 中直接再嵌套一個(gè) 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>
);
};

親身試驗(yàn)這樣是不行的,具體報(bào)錯(cuò)信息搞忘了

解決方案

不能直接嵌套,那我就間接嵌套唄!先用一個(gè)View把二層的ListView封裝進(jìn)去,然后在一層的ListView Cell中調(diào)用這個(gè)空間就行啦!

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 就是那個(gè)包裹,用來(lái)包裹二層ListView

來(lái)看看這個(gè)包裹的真面目吧!

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;

機(jī)智如我,哈哈

問(wèn)題三:

關(guān)于this.state,不太了解的建議直接看官方文檔

我遇到到問(wèn)題是這樣的,有一個(gè)列表數(shù)據(jù)很多,需要分頁(yè)請(qǐng)求,因此我在this.state 中聲明一個(gè)變量 page 初始值為 0 ,每次請(qǐng)求的時(shí)候給state的page加一,直到this.state.data.length === this
.state.data.total 的時(shí)候停止請(qǐng)求直接return,這個(gè)做法看上去是沒有的問(wèn)題的,但是在特殊情況下就回發(fā)生錯(cuò)誤,比如下圖是一個(gè)搜索功能,當(dāng)輸入關(guān)鍵字后點(diǎn)擊搜索將會(huì)請(qǐng)求數(shù)據(jù)。但是會(huì)有這種情況,當(dāng)我輸完關(guān)鍵字后搜索了幾頁(yè)數(shù)據(jù)后,我點(diǎn)擊TextInput,但是沒有修改關(guān)鍵字,再次點(diǎn)擊搜索這時(shí)我的page應(yīng)該從1開始才對(duì),可實(shí)驗(yàn)證明,page會(huì)從上一次的計(jì)數(shù)值往上加1,而不是從1開始。

搜索.jpg

下面粘上部分相關(guān)代碼

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,
        };
   }
   
   // 請(qǐng)求數(shù)據(jù)
     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,
            });
        }
    }
    
    // 點(diǎn)擊搜索
     submitKeyWords(text) {
        this.setState({
            showEmptyView:false,
            dataSource:undefined,
            data:undefined,,
            page:0
        });
        this.fetchData(text);
    }

我先搜索武侯祠相關(guān)數(shù)據(jù),請(qǐng)求三頁(yè)

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

接著我不修改任何關(guān)鍵字,再次點(diǎn)擊搜索

[tid:com.facebook.react.JavaScript] '--->> page', 4
2017-01-12 14:31:57.153670 [5377:3542880] '--->> page', 4

page并沒有如我所愿的從1開始。也就是說(shuō)我在submitKeyWords(text)方法中,在fetchData之前給將state中的page設(shè)為0,并沒有起作用。說(shuō)不起作用可能有些不妥,只能說(shuō)在我用state的page的時(shí)候它至少還不為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

好了,找到問(wèn)題的原因了,那怎么解決呢?如果在OC中我一定會(huì)聲明一個(gè)屬性來(lái)做這件事,那不妨也在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 一下,成功!每當(dāng)我點(diǎn)擊搜索按鈕page都會(huì)先被置為0。

問(wèn)題四:

你是怎么設(shè)施Text組件的背景色透明呢?

一開始我用了一種很笨的方法 backgroundColor: App.Color.white.alpha(0) 簡(jiǎn)直要被自己蠢哭了

有次無(wú)意中看到別人的方法
backgroundColor: 'transparent'
學(xué)習(xí)了!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 公司打算用react-native開發(fā)APP,初始RN遇到了很多坑,搭建了一個(gè)小的項(xiàng)目框架,結(jié)合redux根據(jù)公司...
    45b645c5912e閱讀 741評(píng)論 0 5
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,523評(píng)論 25 708
  • 1.badgeVaule氣泡提示 2.git終端命令方法> pwd查看全部 >cd>ls >之后桌面找到文件夾內(nèi)容...
    i得深刻方得S閱讀 4,770評(píng)論 1 9
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,991評(píng)論 19 139
  • 越是不守規(guī)則的人,越喜歡中國(guó)這個(gè)法制社會(huì)。因?yàn)橹袊?guó)的法制,其實(shí)是向弱者傾斜的弱者有理制度。 在這個(gè)體制下,不懂法沒...
    maofay閱讀 361評(píng)論 1 8