React Native 项目实战 -- DoubanProject

引言:本文是我研究react-native时写的一个简单的demo,代码里有详细的注释,好废话不多说,直接上代码。

1.项目目录

2.index.android.js

/**
 * index.android.js 入口文件
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */
// icon={require("image!book")}
// icon={require("image!movie")}

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  Image,
  StatusBar
} from 'react-native';

// 导入导航器
var Navigation = require("./android_views/common/navigation");
// 导入BookList
var BookList = require("./android_views/book/book_list");
// 导入MovieList
var MovieList = require("./android_views/movie/movie_list");

// tab组件
import TabNavigator from 'react-native-tab-navigator';

// 隐藏状态栏
StatusBar.setHidden(true);

// TabNavigator管理两个模块:图书、电影
var DoubanProject = React.createClass({
  getInitialState: function() {
    return {
      selectedTab: "图书"
    };
  },
  render: function() {
    return (
      <TabNavigator>
        <TabNavigator.Item
          // 标题
          title="图书"
          // 设置选中的位置
          selected={this.state.selectedTab=="图书"}
          // 点击Event
          onPress={() => {
            this.setState({
              selectedTab:"图书"
            })
          }}
          //图标
          renderIcon={() => <Image style={styles.icon} source={require("./res/images/book.png")} />}
          //选中时图标
          renderSelectedIcon={() => <Image style={[styles.icon,{tintColor:'#2f8fe6'}]} source={require("./res/images/book.png")} />}>
          <Navigation component={BookList}/>
        </TabNavigator.Item>
          
        <TabNavigator.Item
          // 标题
          title="电影"
          // 设置选中的位置
          selected={this.state.selectedTab=="电影"}
          // 点击Event
          onPress={() => {
            this.setState({
              selectedTab:"电影"
            })
          }}
          //图标
          renderIcon={() => <Image style={styles.icon} source={require("./res/images/movie.png")} />}
          //选中时图标
          renderSelectedIcon={() => <Image style={[styles.icon,{tintColor:'#2f8fe6'}]} source={require("./res/images/movie.png")} />}>
          <Navigation component={MovieList}/>
        </TabNavigator.Item>
      </TabNavigator>
    )
  }
});

const styles = StyleSheet.create({
    icon: {
         22,
        height: 22
    }
});

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

3.service.js

/*
	1.接口 API
	基于豆瓣开放API的图书、电影
*/

var BaseURL = "https://api.douban.com/v2/";

var Douban_APIS = {
	/*
	图书搜索

	image  图书缩略图
	title  图书名称
	publisher  出版社
	author  作者
	price  价格
	pages  图书总页数
	*/
	book_search: BaseURL + "book/search",

	/*
	图书详情

	image  图书缩略图
	title  图书名称
	publisher  出版社
	author  作者
	price  价格
	pages  图书总页数
	summary  图书简介
	author_intro  作者简介
	*/
	book_detail_id: BaseURL + "book/",

	/*
	电影搜索

	images.medium  电影图像
	title  电影名称
	casts  电影演员  数据需要再处理
	rating.average  电影评分
	year  电影上映时间
	genres  电影标签
	alt  电影详情url
	*/
	movie_search: BaseURL + "movie/search",
}

// 导出
module.exports = Douban_APIS;

4.util.js

/*
	2.定义工具类

	实现功能:定义多个属性,在项目中会使用的一些功能。包括:获取屏幕尺寸、loading组件、GET请求方法

	包含组件:

	外部引用:

		GET请求方法需要从外部引入url、请求成功的回调方法、请求失败的回调方法。
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Dimensions, // 用于获取设备屏幕的宽高
	ActivityIndicator // loading组件
} from 'react-native';

// 定义对象,将提供的功能作为属性存放
var Util = {
	// 屏幕尺寸
	windowSize: {
		 Dimensions.get("window").width,
		height: Dimensions.get("window").height
	},

	// 基于fetch的get方法  只负责下载数据,下载后的处理操作在回调方法中实现
	// successCallback 数据下载成功的回调方法,在组件中实现
	// failCallback 数据下载失败的回调方法,在组件中实现
	getRequest: function(url, successCallback, failCallback) {
		fetch(url)
			.then((response) => response.json())
			.then((responseData) => successCallback(responseData))
			.catch((error) => failCallback(error));
	},

	// loading效果
	loading: <ActivityIndicator style={{marginTop:200}} />
}

// 导出
module.exports = Util;

5.searchBar.js

/*
	3.实现功能:封装搜索栏组件,包括文本输入框和搜索按钮

	包含组件:

	外部传入:
		输入框和按钮的属性设置由外部引入。例如:placeholder、onPress、onChangeText
		使用...this.props将外部传入的属性设置给TextInput和TouchableOpacity

		注意:指定高度、边框颜色、边框线框
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	TextInput,
	TouchableOpacity
} from 'react-native';

// 定义组件
var SearchBar = React.createClass({
	render: function() {
		return (
			<View style={styles.container}>
				<View style={styles.inputContainer}>
					<TextInput style={styles.input} {...this.props} />
				</View>

				<TouchableOpacity style={styles.btn} {...this.props}>
					<Text style={styles.search}>搜索</Text>
				</TouchableOpacity>
			</View>
		);
	}
});

var styles = StyleSheet.create({
	container: {
		flexDirection: "row",
		justifyContent: "flex-end",
		alignItems: "center",
		height: 44,
		marginTop: 10
	},
	inputContainer: {
		flex: 1,
		marginLeft: 5
	},
	input: {
		flex: 1,
		height: 44,
		borderWidth: 1,
		borderRadius: 4,
		borderColor: "#CCC",
		paddingLeft: 5
	},
	btn: {
		 55,
		height: 44,
		marginLeft: 5,
		marginRight: 5,
		backgroundColor: "#23BEFF",
		borderRadius: 4,
		justifyContent: "center",
		alignItems: "center"
	},
	search: {
		flex: 1,
		color: "#fff",
		fontSize: 15,
		fontWeight: "bold",
		textAlign: "center",
		lineHeight: 34
	}
});

module.exports = SearchBar;

6.left_icon.js

/*
	4.实现功能:封装返回按钮图标,不使用图片

	包含组件:

	外部传入:
*/
import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View
} from 'react-native';

// 定义组件 返回图标
var Icon = React.createClass({
	render: function() {
		return (
			<View>
				<View style={styles.go}></View>
			</View>
		);
	}
});

var styles = StyleSheet.create({
	go: {
		 15,
		height: 15,
		borderLeftWidth: 2,
		borderBottomWidth: 2,
		borderColor: "#fff",
		marginLeft: 10,
		transform: [{rotate: "45deg"}] // 将一个矩形框旋转了45度
	}
});

module.exports = Icon;

7.header.js

/*
5.实现功能:封装header,在头部展示标题和返回按钮

包含组件:

外部传入:
	navigator 点击返回按钮返回上一级页面
	initObj(backName、barTitle)  返回按钮的名称、标题
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	TouchableOpacity
} from 'react-native';

// 导入左侧按钮
var Icon = require("./left_icon");

var Header = React.createClass({
	render: function() {
		// 获取obj对象,包括:backName(按钮名称)、barTitle
		var headerContent = this.props.initObj;

		return (
			<View style={styles.header}>
				<TouchableOpacity style={styles.left_btn} onPress={this._pop}>
					<Icon />
					<Text style={styles.btn_next}>{headerContent.backName}</Text>
				</TouchableOpacity>

				<View style={styles.title_container}>
					<Text style={styles.title} numberOfLines={1}>{headerContent.barTitle}</Text>
				</View>
			</View>
		);
	},
	// 返回按钮事件处理方法
	_pop: function() {
		this.props.navigator.pop();
	}
});

var styles = StyleSheet.create({
	header: {
		height: 44,
		backgroundColor: "#3497FF",
		flexDirection: "row",
		justifyContent: "center",
		alignItems: "center"
	},
	left_btn: {
		flexDirection: "row",
		justifyContent: "center",
		alignItems: "center"
	},
	btn_text: {
		color: "#fff",
		fontSize: 17,
		fontWeight: "bold"
	},
	title_container: {
		flex: 1,
		justifyContent: "center",
		alignItems: "center"
	},
	title: {
		color: "#fff",
		fontSize: 18,
		fontWeight: "bold",
		lineHeight: 18,
		 200
	}
});

module.exports = Header;

  

8.navigation.js

/*
	6.实现功能:封装导航器初始化设置

	包含组件:Navigator

	外部传入:
		component  需要展示的页面组件
		route对象  必须添加component属性; 如果需要传值可以添加passProps属性
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View
} from 'react-native';

// npm install react-native-deprecated-custom-components --save

import CustomerComponents, { Navigator } from 'react-native-deprecated-custom-components';

var Navigation = React.createClass({
	render: function() {
		// 创建route对象,约定格式
		var rootRoute = {
			component: this.props.component,
			passProps: {
				
			}
		};

		return (
			<Navigator
				initialRoute={rootRoute}
				configureScene={() => {return Navigator.SceneConfigs.PushFromRight}}
				renderScene={(route,navigator) => {
					var Component = route.component;
					return (
						<View style={{flex:1}}>
							<Component
								navigator={navigator}
								route={route}
								{...route.passProps}/>
						</View>
					);
				}}/>
		);
	}
});

module.exports = Navigation;

  

9.customWebView.js

/*
	7.实现功能:封装WebView,根据传入的url展示网页信息

	包含组件:Header、WebView

	外部传入:
		给Header设置:navigator、initObj(backName、title)
		给WebView设置:source(url)
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	WebView
} from 'react-native';

var Header = require("./header");

var CustomWebView = React.createClass({
	render: function() {
		return (
			<View style={{backgroundColor:"white",flex:1}}>
				<Header 
					navigator={this.props.navigator}
					initObj={{
						backName:this.props.backName,
						barTitle:this.props.title
					}}/>
				<WebView
					startInLoadingState={true}
					contentInset={{top:-44,bottom:-120}}
					source={{uri:this.props.url}}/>
			</View>
		);
	}
});

module.exports = CustomWebView;

  

10.book_item.js

/*
	8.图书列表 item

	实现功能:展示图书信息,点击item进入图书详情页面

	包含组件:基本组件

	外部传入:

	book  图书对象
	onPress事件处理方法  通过...this.props绑定,需要设置参数,即图书id

	需要使用的字段:

		image  图书缩略图
		title  图书名称
		publisher  出版社
		author  作者
		price  价格
		pages  图书总页数
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Image,
	TouchableOpacity
} from 'react-native';

var BookItem = React.createClass({
	render: function() {
		var book = this.props.book;

		return (
			<TouchableOpacity style={styles.item} {...this.props}>
				{/* 图书图像 */}
				<View style={styles.imageContainer}>
					<Image style={styles.image} source={{uri:book.image}}/>
				</View>
				{/* 图书信息 */}
				<View style={styles.contentContainer}>
					<View style={styles.textContainer}>
						<Text numberOfLines={1}>{book.title}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.publisher_author} numberOfLines={1}>{book.publisher}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.publisher_author} numberOfLines={1}>{book.author}</Text>
					</View>
					<View style={{flexDirection:"row",flex:1,alignItems:"center"}}>
						<Text style={styles.price}>{book.price}</Text>
						<Text style={styles.pages}>{book.pages}页</Text>
					</View>
				</View>
			</TouchableOpacity>
		)
	}
});

var styles = StyleSheet.create({
	item: {
		flexDirection: "row",
		height: 120,
		padding: 10
	},
	imageContainer: {
		justifyContent: "center",
		alignItems: "center"
	},
	image: {
		 80,
		height: 100
	},
	contentContainer: {
		flex: 1,
		marginLeft: 15
	},
	textContainer: {
		flex: 1,
		justifyContent: "center"
	},
	publisher_author: {
		color: "#A3A3A3",
		fontSize: 13
	},
	price: {
		color: "#2BB2A3",
		fontSize: 16
	},
	pages: {
		marginLeft: 10,
		color: "#A7A0A0"
	}
});

module.exports = BookItem;

  

11.book_list.js

/*
	9.图书列表模块:搜索栏、图书列表
	图书列表的内容:通过调用图书搜索接口获得多条图书数据
	图书列表Item是单独封装的
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Image,
	TouchableOpacity,
	ListView,
	ScrollView
} from 'react-native';

// 从common模块导入内容
var Util = require("./../common/util");
var SearchBar = require("./../common/searchBar");
var ServiceURL = require("./../common/service");
var BookItem = require("./book_item");
var BookDetail = require("./book_detail");

var BookList = React.createClass({
	getInitialState: function() {
		var ds = new ListView.DataSource({
			rowHasChanged: (oldRow, newRow) => oldRow !== newRow
		});

		return {
			// dataSource
			dataSource: ds,
			// 网络请求状态标识
			show: false,
			// 搜索关键字
			// 作用:1.搜索接口需要设置搜索内容 2.点击搜索按钮时,修改关键字内容,重新请求数据,重新渲染
			keywords: "react"
		};
	},
	getData: function() {
		// 开启loading,每次搜索时都需要重新下载显示数据
		this.setState({
			show: false
		});

		// 请求数据
		var that = this;
		var url = ServiceURL.book_search + "?count=20&q=" + this.state.keywords;

		Util.getRequest(url, function(data) {
			// 请求成功回调函数
			/*
				如果没有相关书籍,使用alert提示
				https://api.douban.com/v2/book/search?count=20&q=react
				{"count":0,"start":0,"total":0,"books":[]}
			 */

			if (!data.books || data.books.length == 0) {
				return alert("未查询到相关书籍");
			}

			// 设置下载状态和数据源
			var ds = new ListView.DataSource({
				rowHasChanged: (oldRow, newRow) => oldRow !== newRow
			});

			that.setState({
				show: true,
				dataSource: ds.cloneWithRows(data.books)
			});

		}, function(error) {
			// 请求失败回调函数
			alert(error);
		})
	},
	// TextInput的onChangeText事件处理方法
	_changeText: function(text) {
		this.setState({
			keywords: text
		});
	},
	_searchPress: function() {
		this.getData();
	},
	_showDetail:function(bookID){
		var detailRoute = {
			component: BookDetail,
			passProps:{
				bookID: bookID
			}
		}

		this.props.navigator.push(detailRoute);
	},
	// 布局
	render: function() {
		return (
			<ScrollView>
				<SearchBar
					placeholder="请输入图书的名称"
					onPress={this._searchPress}
					onChangeText={this._changeText}/>
				{
					// 请求数据时显示loading,数据请求成功后显示ListView
					this.state.show ?
						<ListView
							dataSource={this.state.dataSource}
							initialListSize={10}
							renderRow={this._renderRow}
							renderSeparator={this._renderSeparator}/>
					: Util.loading
				}
			</ScrollView>
		);
	},
	componentDidMount: function() {
		// 请求数据
		this.getData();
	},
	_renderRow: function(book) {
		return <BookItem book={book} onPress={this._showDetail.bind(this,book.id)}/>
	},
	// 分割线
	_renderSeparator: function(sectionID: number, rowID: number) {
		var style = {
			height: 1,
			backgroundColor: "#CCCCCC"
		}

		return <View style={style} key={sectionID+rowID} />
	}
});

var styles = StyleSheet.create({
	//
});

module.exports = BookList;

  

12.book_detail.js

/*
	10.图书详情

	实现功能:展示图书详情,包括:图书信息、图书简介、作者简介

	包含组件:基本组件、BookItem(图书信息使用BookItem展示)

	外部传入:

	需要使用的字段:

		image  图书缩略图
		title  图书名称
		publisher  出版社
		author  作者
		price  价格
		pages  图书总页数
		summary  图书简介
		author_intro  作者简介
*/

import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Image,
	ScrollView
} from 'react-native';

// 引入
var ServiceURL = require("./../common/service");
var Util = require("./../common/util");
var Header = require("./../common/header");
var BookItem = require("./book_item");

// 创建组件类
var BookDetail = React.createClass({
	getInitialState:function() {
		return {
			bookData:null // 图书对象详情信息
		}
	},
	getData:function(){
		// 获取图书信息
		var that = this;
		var url = ServiceURL.book_detail_id + this.props.bookID;
		Util.getRequest(url,function(data){
			that.setState({
				bookData:data
			});
		},function(error){
			alert(error);
		});
	},
	render:function(){
		return (
			<ScrollView style={styles.container}>
				{
					this.state.bookData ?
						<View>
							<Header
								initObj={{backName:"图书",barTitle:this.state.bookData.title}}
								navigator={this.props.navigator}/>
							<BookItem book={this.state.bookData}/>
							<View>
								<Text style={styles.title}>图书简介</Text>
								<Text style={styles.text}>{this.state.bookData.summary}</Text>
							</View>
							<View style={{marginTop:10}}>
								<Text style={styles.title}>作者简介</Text>
								<Text style={styles.text}>{this.state.bookData.author_intro}</Text>
							</View>
							<View style={{height:55}}></View>
						</View>
					: Util.loading
				}
			</ScrollView>
		);
	},
	// 组件挂载以后,进行网络请求
	componentDidMount:function(){
		// 请求图书详情
		this.getData();
	}
});

var styles = StyleSheet.create({
	container:{
		flex:1,
		backgroundColor:"white"
	},
	title:{
		fontSize:16,
		marginTop:10,
		marginLeft:10,
		marginBottom:10,
		fontWeight:"bold"
	},
	text:{
		marginLeft:10,
		marginRight:10,
		color:"#000D22"
	}
});

module.exports = BookDetail;

  

13.movie_item.js

/*
	11.电影列表item

	实现功能:展示电影信息,点击item进入电影详情页面

	包含组件:基本组件

	外部传入:

	movie  电影对象
	onPress  通过...this.props绑定,需要设置参数:电影名称、电影详情页面url

	需要使用的字段:
		images.medium  电影图像
		title  电影名称
		casts  电影演员  数据需要再处理
		rating.average  电影评分
		year  电影上映时间
		genres  电影标签
		alt  电影详情url
*/
import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Image,
	TouchableOpacity
} from 'react-native';

var MovieItem = React.createClass({
	render:function() {
		var movie = this.props.movie;

		// 提取演员姓名
		// 原始数据结构:数组元素是描述演员的对象,对象中包含演员名字
		// 需要遍历数组,把每个演员的名字存在一个新的数组中
		var actors = [];
		for(var i in movie.casts){
			actors.push(movie.casts[i].name);
		}

		return (
			<TouchableOpacity style={styles.item} {...this.props}>
				<View style={styles.imageContainer}>
					<Image style={styles.image} resizeMode="contain" source={{uri:movie.images.medium}}/>
				</View>
				<View style={styles.contentContainer}>
					<View style={styles.textContainer}>
						<Text style={styles.text} numberOfLines={1}>名称:{movie.title}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.text} numberOfLines={1}>演员:{actors}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.text} numberOfLines={1}>评分:{movie.rating.average}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.text} numberOfLines={1}>时间:{movie.year}</Text>
					</View>
					<View style={styles.textContainer}>
						<Text style={styles.text} numberOfLines={1}>标签{movie.genres}</Text>
					</View>
				</View>
			</TouchableOpacity>
		);
	}
});

var styles = StyleSheet.create({
	item:{
		flexDirection:"row",
		height:120,
		padding:10
	},
	imageContainer:{
		justifyContent:"center",
		alignItems:"center"
	},
	image:{
		80,
		height:110
	},
	contentContainer:{
		flex:1,
		marginLeft:15
	},
	textContainer:{
		flex:1,
		justifyContent:"center"
	},
	text:{
		color:"black"
	}
});

module.exports = MovieItem;

14.movie_list.js

/*
	12.电影列表模块:搜索框、电影列表
	电影列表的内容:通过调用电影搜索接口获得多条电影数据
	电影列表Item是单独封装的
*/
import React, { Component } from 'react';
import {
	AppRegistry,
	StyleSheet,
	Text,
	View,
	Image,
	TouchableOpacity,
	ListView,
	ScrollView
} from 'react-native';

var SearchBar = require("./../common/searchBar");
var Util = require("./../common/util");
var ServiceURL = require("./../common/service");
var MovieItem = require("./movie_item");
var MovieWebView = require("./../common/customWebView");

var MovieList = React.createClass({
	getInitialState:function() {
		var ds = new ListView.DataSource({
			rowHasChanged:(oldRow,newRow) => oldRow!==newRow
		});

		return {
			dataSource: ds,
			show: false,
			keywords:"哈利波特"
		};
	},
	_changeText:function(text){
		this.setState({
			keywords:text
		});
	},
	_searchPress:function(){
		this.getData();
	},
	_showDetail:function(title,url){
		var detailRoute = {
			component:MovieWebView,
			passProps:{
				backName:"电影",
				title:title,
				url:url
			}
		};

		// 推出
		this.props.navigator.push(detailRoute);
	},
	getData:function(){
		this.setState({
			show:false
		});

		var that = this;
		var url = ServiceURL.movie_search + "?count=20&q=" + this.state.keywords;

		/*
			https://api.douban.com/v2/movie/search?count=20&q=哈利波特
			{"count":0,"start":0,"total":0,"books":[]}
		 */

		Util.getRequest(url,function(data){
			if(!data.subjects||data.subjects.length==0){
				return alert("未找到相关电影");
			}

			var ds = new ListView.DataSource({
				rowHasChanged:(oldRow,newRow) => oldRow!==newRow
			});

			var movies = data.subjects;

			that.setState({
				show:true,
				dataSource:ds.cloneWithRows(movies)
			});
		},function(error){
			alert(error);
		});

	},
	render:function(){
		return (
			<ScrollView>
				<SearchBar
					placeholder="请输入电影的名称"
					onPress={this._searchPress}
					onChangeText={this._changeText}/>
				{
					this.state.show ?
						<ListView
							dataSource={this.state.dataSource}
							initialListSize={10}
							renderRow={this._renderRow}
							renderSeparator={this._renderSeparator}/>
					: Util.loading
				}
			</ScrollView>
		);
	},
	componentDidMount:function(){
		// 请求数据
		this.getData();
	},
	_renderRow:function(movie){
		return <MovieItem movie={movie} onPress={this._showDetail.bind(this, movie.title, movie.alt)}/>;
	},
	// 分割线
	_renderSeparator:function(sectionID:number,rowID:number){
		var style = {
			height: 1,
			backgroundColor:"#CCCCCC"
		};

		return <View style={style} key={sectionID+rowID}></View>
	}
});

var styles = StyleSheet.create({

});

module.exports = MovieList;

15.效果图

原文地址:https://www.cnblogs.com/crazycode2/p/7146779.html