獲取網絡數據
在上一節,我們已經通過模擬數據,並將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返回的數據對象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有入下圖這些類型
其中常用到的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(必填參數),因此你運行之後會收到一個黃色警告。
最後再給這個組件優化一下(養成編寫代碼邊思考邊優化的習慣!),兩點:
- 在組件請求網絡並解析數據時,給它一個loading界面,加載完成後再顯示結果頁面。
- 由於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等。