【稀飯】react native 實戰系列教程之影片數據獲取並解析

獲取網絡數據

在上一節,我們已經通過模擬數據,並將UI展示出來。這節我們將獲取網絡數據。數據來源於網絡,僅用於學習使用。

fetch介紹

fetch是react native的一個網絡請求庫,使用該庫不用引入模塊,可以直接使用。一個簡單的請求如下:

fetch('http://facebook.github.io/react-native/movies.json')

發起請求之後,我們還需要對它的響應進行處理,只要這樣

fetch('http://facebook.github.io/react-native/movies.json')
.then((response)=>{
        console.log(response)
    }
)
.catch((e)=>{
        console.log(e)
    }
)

在瀏覽器中打開調試工具,在Console下輸入以上代碼:

fetch命令

從上圖可以看出fetch返回的數據對象Response包含body、headers、status等。

Response常用的兩個函數是

  • json() - 返回一個JSON格式.

  • text() - 返回一個文本.

fetch還可以構造複雜一點的

fetch('https://mywebsite.com/endpoint/', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    firstParam: 'yourValue',
    secondParam: 'yourOtherValue',
  })
})

可以配置請求的方法method,頭部headers和body。

上面的請求都是異步的,也可以使用同步操作,如下

async getMoviesFromApi() {
    try {
      let response = await fetch('http://facebook.github.io/react-native/movies.json');
      let responseJson = await response.json();
      return responseJson.movies;
    } catch(error) {
      console.error(error);
    }
  }

整個方法是異步的,但是內部的fetch請求是同步的,使用await 會等待fetch返回結果response再執行下一步。記得try catch任何異常。

更多fetch相關,可以查看官方文檔

使用fetch獲取數據

打開DramaComponent.js文件,定義一個方法fetchData

fetchData(){
    var url = 'http://www.y3600.com/hanju/new';
    fetch(url)
        .then((res)=> res.text())
        .then((html)=>{
            console.log(html);
        })
        .catch((e)=>{
            console.log(e);
        }).done();
}

//在最初的render方法調用之後立即調用。
//網絡請求、事件訂閱等操作可以在這個方法中調用。 
//作用相同與Fragment生命週期中的onViewCreate方法。
componentDidMount(){
    this.fetchData();
}

這樣我們就獲取到網頁html數據,接下來我們要解析html獲取想要的數據。使用到的解析庫是cheerio。

使用cheerio解析html獲取影視信息

cheerio屬於第三方模塊,我們要使用它首先要先把它安裝到我們的項目中來。
cheerio依賴events模塊,所以events也要安裝進來。不知道依賴關係也沒事,在你運行程序的時候,它就會提示你缺少了哪個module,再安裝下就可以了。

使用命令行cd到我們的跟目錄下,然後執行命令

npm install cheerio --save
npm install events --save

等待安裝完畢之後,在DramaComponent.js中引入該模塊

import Cheerio from 'cheerio';

然後將html加載到cheerio解析器裏,利用cheerio API進行數據提取,通讀cheerio API

var $ = Cheerio.load(html);

我們要分析提取的網站地址是http://www.y3600.com/hanju/new。打開該網站,右擊查看網頁源代碼,先自己靜態分析下,該如何通過html標籤獲取篩選到數據。

通過分析,我們發現影片列表信息存放在class爲m-ddone的div標籤下,並且ul的每一li標籤代表一部影片,然後繼續分析下去獲取每一部的詳細信息即可,這裏就不再詳細分析了。我們聲明一個方法來解析這一個過程,代碼如下:

//解析html
resolveHtml(html){
    var $ = Cheerio.load(html);
    var body = $('div.m-ddone').find('ul');//ui
    var datas = [];//影視列表數據集合
    body.each((index,item)=>{//li
        var dramaItem ={
            name:'',//影片名稱
            title:'',//標題
            actor:'',//演員
            pic:'',//圖片地址
            url:'',//詳情鏈接
        };
        var link = $(item).find('a');
        link.each((i,a)=>{//獲取影片名稱
            var aTag = $(a);
            if(i===0){
                dramaItem.pic = aTag.find('img').attr('src');
                dramaItem.url = aTag.attr('href');
                dramaItem.title = aTag.find('label.tit').text();
            }else if(i===1){
                dramaItem.name = aTag.text();
            }
        });

        var actor = $(item).find('li.zyy').text();

        dramaItem.actor = actor;
        //
        datas.push(dramaItem);
    });
    //最後記得刷新一下數據
    this.setState({
        movies:this.state.movies.cloneWithRows(datas),
    });
}

然後在剛纔fetchData那裏獲取到的html傳遞和調用resolveHtml就可以了。

fetchData(){
    var url = 'http://www.y3600.com/hanju/new';
    fetch(url)
        .then((res)=> res.text())
        .then((html)=>{
            //console.log(html);
            this.resolveHtml(html);
        })
        .catch((e)=>{
            console.log(e);
        }).done();
}

ok,刷新一下界面,現在已經獲取到數據並顯示了,如下

數據

上拉加載更多

然後,你會發現,怎麼好像只有一頁的數據。嗯,沒錯,我們還要優化一下,讓數據和ListView支持分頁功能。

我們在多分析下網站的源代碼,需要的信息有:總頁數、當前頁、下一頁的鏈接地址,因此,我們的數據結構修改定義爲,如下:

dramaList:{
    totalPage:1,//總頁數
    currPage:0,//當前頁
    pages:[],//頁碼信息
    datas:[],//影片信息列表數據
}

此時,constructor方法內

constructor(props) {
    super(props);
    this.state = {
        movies:new ListView.DataSource({
            rowHasChanged:(r1,r2) => r1!=r2,
        }),
        dramaList:{
            totalPage:1,//總頁數
            currPage:0,//當前頁
            pages:[{index:1,url:'http://www.y3600.com/hanju/new'}],//頁碼信息
            datas:[],//影片信息列表數據
        },
    }
}

由於我們初始訪問的是http://www.y3600.com/hanju/new 這個地址,因此初始化時頁碼信息也給初始化第一頁數據。

解析頁碼信息的關鍵代碼如下:

//解析頁碼信息
var page = $('div.pages').find('a');
page.each((i,item)=>{
    if(!$(item).hasClass('next')){
        dramaList.totalPage++;
        dramaList.pages.push({
            index:$(item).text(),
            url:$(item).attr('href'),
        });
    }
});

dramaList就是this.state.dramaList,因此數據結構改變了,我們也要把之前的datas字段改爲dramaList.datas。所以此時resolveHtml方法的完整代碼如下:

//解析html
resolveHtml(html){
    var $ = Cheerio.load(html);
    var dramaList = this.state.dramaList;
    //解析劇集列表
    var body = $('div.m-ddone').find('ul');//ui

    body.each((index,item)=>{//li
        var dramaItem ={
            name:'',//影片名稱
            title:'',//標題
            actor:'',//演員
            pic:'',//圖片地址
            url:'',//詳情鏈接
        };
        var link = $(item).find('a');
        link.each((i,a)=>{//獲取影片名稱
            var aTag = $(a);
            if(i===0){
                dramaItem.pic = aTag.find('img').attr('src');
                dramaItem.url = aTag.attr('href');
                dramaItem.title = aTag.find('label.tit').text();
            }else if(i===1){
                dramaItem.name = aTag.text();
            }
        });

        var actor = $(item).find('li.zyy').text();

        dramaItem.actor = actor;
        //
        dramaList.datas.push(dramaItem);
    });
    //解析頁碼信息
    dramaList.currPage++;
    var page = $('div.pages').find('a');
    page.each((i,item)=>{
        if(!$(item).hasClass('next')){
            dramaList.totalPage++;
            dramaList.pages.push({
                index:$(item).text(),
                url:$(item).attr('href'),
            });
        }
    });
    //刷新一下數據
    this.setState({
        movies:this.state.movies.cloneWithRows(dramaList.datas),
        dramaList:dramaList,
    });
}

由於每一頁的html解析過程都一樣,所以我們改造一下fetchData方法,讓它傳入一個url地址
,url參數化。

fetchData(url){
    url = HOST_URL+url;
    ....//省略其它代碼
}

HOST_URL是一個const,是該網站的根地址http://www.y3600.com

然後還記得在介紹ListView的時候,有個方法_onEndReached是在它拉到底部會調用,是的,我們就在這個方法下去,加載下一頁,實現如下:

_onEndReached(){
    var dramaList = this.state.dramaList;
    var totalPage = dramaList.totalPage;
    var currPage = dramaList.currPage;
    var nextPage = currPage+1;
    if(nextPage <= totalPage){
        this.fetchData(dramaList.pages[currPage].url);
    }
}

記得ListView的onEndReached要調用bind(this),否則_onEndReached的this.state.dramaList會報undefined異常

最後,在componentDidMount改下調用方法

componentDidMount(){
    var url = '/hanju/new';
    this.fetchData(url);
}

重新執行下代碼,就可以看到分頁效果了,如果/hanju/new地址的數據沒有分頁,你可以把url改爲其他,比如‘人氣’頁/hanju/renqi/,它們的解析過程都一樣的。

寫完加載更多,還有下拉刷新呢!下面我們就來講講下拉刷新。

下拉刷新

ListView有個refreshControl來設置刷新的狀態,效果和android的SwipeRefreshLayout一樣。需要額外在’react-native’ import RefreshControl組件,代碼如下:

import{
    .....//省略其它代碼
    RefreshControl,
}from 'react-native';
//刷新
_onRefresh(){

}

<ListView
    dataSource = {this.state.movies}
    renderRow = {this._renderMovieView.bind(this)}
    style = {styles.listview}
    initialListSize = {10}
    pageSize = {10}
    onEndReachedThreshold = {5}
    onEndReached = {this._onEndReached.bind(this)}
    enableEmptySections = {true}
    contentContainerStyle = {styles.grid}
    refreshControl = {
        <RefreshControl
            refreshing = {this.state.isRefreshing}
            onRefresh = {this._onRefresh.bind(this)}
            colors = {['#f74c31', '#f74c31', '#f74c31','#f74c31']}
            progressBackgroundColor = '#ffffff'
        />
    }
/>

RefreshControl內有個refreshing布爾值屬性,我們需要通過state來設置這個是否正在刷新的狀態。

constructor(props) {
    super(props);
    this.state = {
        movies:new ListView.DataSource({
            rowHasChanged:(r1,r2) => r1!=r2,
        }),
        dramaList:{
            totalPage:1,//總頁數
            currPage:0,//當前頁
            pages:[{index:1,url:'http://www.y3600.com/hanju/new'}],//頁碼信息
            datas:[],//影片信息列表數據
        },
        isRefreshing:false,//RefreshControl是否正在刷新
    }
}

接着,我們要處理刷新邏輯。當下拉刷新時,要將列表數據清空,初始化到最初的狀態。在resolveHtml裏添加如下代碼:

resolveHtml(html){
    var $ = Cheerio.load(html);
    var dramaList = this.state.dramaList;
    if(this.state.isRefreshing){
        dramaList.currPage = 0;
        dramaList.datas = [];
    }
    //解析劇集列表
    ....//省略其它代碼
    //刷新一下數據
    this.setState({
        movies:this.state.movies.cloneWithRows(dramaList.datas),
        dramaList:dramaList,
        isRefreshing:false,
    });
}

解析完數據之後,將isRefreshing狀態置爲false,在刷新回調的方法裏fetch初始的地址

//刷新
_onRefresh(){
    this.setState({
        isRefreshing: true
    });
    this.fetchData('/hanju/new');
}

組件參數化

上面我們已經將DramaComponent組件的數據獲取解析全部實現了,但是我們解析的這個地址是固定寫死的,這樣一來這個組件就不能提供給別的組件重複使用了,所以我們要將這個地址參數化,由外部調用該組件的時候傳入,具體實現如下。

組件的參數是通過props設置的,我們通過propTypes定義一個string類型的url,還可以通過defaultProps設置默認初始值。

static propTypes = {
    url:React.PropTypes.string.isRequired,
}
static defaultProps = {
    url: '/hanju/new',
}

PropType有入下圖這些類型

PropTypes

其中常用到的string\any\array\bool\func\number 關於PropType介紹

接着,將初始的url都替換成this.props.url。兩個地方要修改,一個是constructor裏的state初始數據,和componentDidMount調用的fetchData

constructor(props) {
    super(props);
    this.state = {
        movies:new ListView.DataSource({
            rowHasChanged:(r1,r2) => r1!=r2,
        }),
        dramaList:{
            totalPage:1,//總頁數
            currPage:0,//當前頁
            pages:[{index:1,url:this.props.url}],//頁碼信息
            datas:[],//影片信息列表數據
        },
        isRefreshing:false,//RefreshControl是否正在刷新
    }
}

componentDidMount(){
    this.fetchData(this.props.url);
}

最後,我們打開程序入口index.android.js,給組件DramaComponent設置一個url值

class XiFan extends Component {

  render(){
    return(
        <DramaComponent url='/hanju/new'/>
    );
  }
}

AppRegistry.registerComponent('XiFan', () => XiFan);

如果你組件沒有設置url參數,並且組件內沒有defaultProps,那麼由於DramaComponent組件把url設置成了isRequired(必填參數),因此你運行之後會收到一個黃色警告。

警告

最後再給這個組件優化一下(養成編寫代碼邊思考邊優化的習慣!),兩點:

  1. 在組件請求網絡並解析數據時,給它一個loading界面,加載完成後再顯示結果頁面。
  2. 由於fetchData方法是內部重複循環調用,但是並不是每次都需要去解析頁碼信息的,只有第一次沒有數據的時候要去解析獲取頁碼數據。

state增加loaded和hasPage參數

constructor(props) {
    super(props);
    this.state = {
        movies:new ListView.DataSource({
            rowHasChanged:(r1,r2) => r1!=r2,
        }),
        dramaList:{
            totalPage:1,//總頁數
            currPage:0,//當前頁
            pages:[{index:1,url:this.props.url}],//頁碼信息
            datas:[],//影片信息列表數據
            hasPage:false,//是否有分頁
        },
        isRefreshing:false,//RefreshControl是否正在刷新
        loaded:false,//是否初始加載完成
    }
}

增加加載中頁面和邏輯

//加載中頁面
_renderLoadingView(){
    return(
        <View style = {{flex:1,justifyContent:'center',alignItems:'center'}}>
            <Text>加載中,請稍後...</Text>
        </View>
    );
}
render(){
    if(!this.state.loaded){
        return this._renderLoadingView();
    }
    return(
        <ListView 
            ...//省略其它代碼
        />
        );
}

修改解析頁碼邏輯,並設置loaded狀態

//解析html
resolveHtml(html){
    ...//省略其它代碼
    //解析頁碼信息
    dramaList.currPage++;
    if(!dramaList.hasPage) {
        dramaList.hasPage = true;
        var page = $('div.pages').find('a');
        page.each((i, item)=> {
            if (!$(item).hasClass('next')) {
                dramaList.totalPage++;
                dramaList.pages.push({
                    index: $(item).text(),
                    url: $(item).attr('href'),
                });
            }
        });
    }
    //刷新一下數據
    this.setState({
        movies:this.state.movies.cloneWithRows(dramaList.datas),
        dramaList:dramaList,
        isRefreshing:false,
        loaded:true,
    });
}

OK!本節的內容就講完了。如果要完整的代碼,可以查看 我的github

總結

本節,完成了一個自定義組件的構建過程,並抽象成一個公共組件。下一節,我們將利用該組件完成首頁的功能,涉及到的內容是TitleBar、選項卡、ViewPagerAndroid等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章