最近学习了一个网上的React Native项目,利用React Native制作一个类似于美团的App,项目属于对之前React Native常用组件的基本使用,但是仍有一些关键点值得记录。最后做成的效果如下:
可以看到这个App大致分为四个板块:主页、商家、我的、更多,分别对应四个标签导航,可以利用React native提供的TabNavigator来实现四个标签页的导航。每个页面对应一个组件,新建一个专门的文件夹component用于存放这些组件。
import HomeStack from './component/Home/HomeScreen' import ShopStack from './component/Shop/ShopScreen' import MoreScreen from './component/MoreScreen' import MineScreen from './component/MineScreen' const Main=TabNavigator ( { Home:{screen:HomeStack}, //标签页Home对应HomeScreen组件 Shop:{screen:ShopStack}, Mine:{screen:MineScreen}, More:{screen:MoreScreen}, },
以上是一级页面,在点击其中的图标后可以跳转到响应的二级详情页面,可以用React Navigation中的StackNavigator来实现详情页的跳转。相应的,对于复杂的模块,可能不止包含一个组件模块,这就需要为它新建文件夹来存放它的组件了,例如主页Home文件夹下包含了主页面HomeScreen及详情页HomeDetail,以及其他的组件。
使用React开发网页最便捷的地方就是它可以把相同的部分提炼成组件,通过组件化的思路来渲染页面,可以免去很多重复的代码。首先实现最简单的页面:更多
可以看到这个页面的元素类型都是单一的选项条,只不过显示的文字不同,可以把每一条抽象成为一个类Cell,当传入不同的属性时,显示不同的文字
class Cell extends Component{ render(){ return( <TouchableOpacity activeOpacity={0.5} onPress={this.props.cellFunction}> <View style={styles.cellBar}> <Text style={{fontSize:16}}>{this.props.title}Text> <Image style={styles.cellImage} source={{uri:'icon_cell_rightarrow'}} /> View> TouchableOpacity> ) } }在页面调用Cell组件:
<Cell title="消息提醒"/> <Cell title="邀请好友"/> <Cell title="清空缓存"/>
在类似的组件中,难免存在不同的地方,需要根据不同的情况进行分别处理,例如接下来实现的“我的”页面:
页面中也存在许多类似的选项条:“我的订单”、“钱包”、“抵用券”......它们整体结构分为左边图标、标题,右边文字、箭头,但仔细看“今日推荐”右边没有文字,而是一个图标“new”。这就需要在调用时传入属性来规定是否渲染图标以及渲染怎样的图标。在react中会根据是否传入参数来决定是否渲染,例如在组件定义时将右边的文字、图标都写上:
{/*右边的文字*/} <Text>{this.props.infoText}Text> {/*右边的图标*/} <Image source={{uri:this.props.badgeSrc}} style={{width:30,height:18}} /> {/*右边的箭头*/} <Image style={styles.cellArrow} source={{uri:'icon_cell_rightarrow'}} />但在调用组件时有的组件需要文字,就传入属性infoText,有的组件需要图标,就传入badgeSrc,不传入的属性就不会被渲染:
<Cell title='今日推荐' iconSrc="icon_mine_recommend" badgeSrc="icon_cell_new" /> <Cell title='我要合作' iconSrc="icon_mine_corporation" infoText="轻松开店" />
通过flex可以很快捷地将组件布局为想要的样式,例如通过flexDirection:'row',justifyContent:'space-between'可以将元素分布于左边、右边,而不是设置float浮动:
在渲染一组相似结构的数据时,可以利用FlatList,我们只需要定义其中一个元素的渲染方法,就可以把一组数据渲染出来。
当希望页面可以上下或左右滚动时,需要使用ScrollView。例如下面主页的菜单:
每个菜单选项都是一个图标加一个文字,可以通过flatlist来渲染,菜单可以左右滑动来切换两个FlatList,需要在外面包装一个ScrollView组件。
<View style={styles.menu}> <ScrollView horizontal={true} showsHorizontalScrollIndicator={false} pagingEnabled={true} onMomentumScrollEnd={(e)=>this.slideMenu(e)} > <FlatList data={this.props.listData[0]} style={styles.menuList} keyExtractor = {(item, index) => index.toString()} renderItem={this.renderMenuItem} numColumns={5} columnWrapperStyle={styles.menuColumn} /> <FlatList data={this.props.listData[1]} style={styles.menuList} keyExtractor = {(item, index) => index.toString()} renderItem={this.renderMenuItem} numColumns={5} columnWrapperStyle={styles.menuColumn} /> ScrollView> <View style={styles.indicateBar}> {/*渲染底部指示标签点*/} {this.renderIndicate()} View> View>
App中的数据不可能是写死的,而是从网络上随时动态请求的,但是在页面中呈现的格式却是固定的。我们可以动态的从网络上请求数据,然后将这些数据通过属性传递给对应的组件模块,就可以实现动态的数据渲染。
在组件挂载之后利用fetch请求数据并保存到state中:
componentDidMount() { let shopUrl="http://api.meituan.com/group/v2/recommend/homepage/city/20?userId=160495643..."; fetch(shopUrl).then((res)=>res.json()) .then((resJson)=>{ this.setState({ shopList:resJson.data }); }).catch((err)=>{ console.log(err); }) }
例如以下为一条请求的数据,其中包括渲染标题的颜色、主标题、副标题、图片地址、对应的跳转链接等,
{
"position": 0,
"typeface_color": "#ff9900",
"id": 7486,
"share": {
"message": "1元能吃肯德基",
"url": "http://i.meituan.com/firework/kfchanbao"
},
"title": "1元吃肯德基",
"module": false,
"maintitle": "1元肯德基",
"tplurl": "imeituan://www.meituan.com/web?url=http://i.meituan.com/firework/kfchanbao",
"type": 1,
"imageurl": "http://p0.meituan.net/w.h/groupop/9aa35eed64db45aa33f9e74726c59d938450.png",
"solds": 0,
"deputytitle": "新用户专享"
}
接着就需要把这些数据填充到界面上,界面上的显示模块是固定的,例如主页中的活动模块如下:
可以看到活动广告模块可以分为三类:MediumBlock(左上角粉色框)、SmallBlock(绿色框)、LargeBlock(蓝色框),可以将这三类框分别抽象为组件,然后排布到页面上。例如SmallBlock.js:
export default class SmallBlock extends Component { render() { return ( <TouchableOpacity style={styles.container}> <View> <Text style={[{color:this.props.data.typeface_color},styles.title]}> {this.props.data.title} Text> <Text>{this.props.data.deputytitle}Text> View> <Image source={{uri:this.handleUrl(this.props.data.imageurl)}} style={styles.image}/> TouchableOpacity> ) } handleUrl(url){ let imageUrl=''; if(url.indexOf('w.h')===-1){ imageUrl=url; }else {//美团的图片url中有w.h字段,代表图片的长与宽,需要替换后才能得到图片 imageUrl=url.replace('w.h','60.60'); } return imageUrl; } }在页面中调用组件,并填充数据:
<SmallBlock data={this.props.shopList[4]}/>
例如将商家页面shopScreen.js中的“购物中心”封装成为一个组件ShopCenter,
当点击它时跳转到详情页shopDetail.js,但是在每个ShopCenter组件中是没办法处理跳转事件的,只有在ShopScreen类中才可以访问到navigation对象,实现跳转。因此需要在ShopScreen中调用ShopCenter组件时,为其绑定一个事件属性onClick(这个属性名可自定),然后在ShopCenter组件中点击时调用该属性触发父组件中对应的事件:
例如父组件中调用子组件ShopCenter以及绑定onClick属性为jumpDetail函数:
<ShopCenter key={index} data={item} onClick={this.jumpDetail}/> ... jumpDetail(url){ navigation.navigate('Detail',url); }其中变量navigation是this.props.navigation,是由StackNavigator传递给它的子组件的,我直接使用时,会报错this.props未定义,于是我把它保存到一个全局变量navigation中,然后再调用其navigate方法。
在子组件ShopCenter中点击触发jumpTo函数来调用父组件属性onClick
export default class ShopCenter extends Component { render() { return ( <TouchableOpacity style={styles.container} onPress={()=>this.jumpTo(this.props.data.detailurl)} > <Image source={{uri:this.props.data.img}} style={styles.image} /> <Text style={styles.imageLabel}>{this.props.data.showtext.text}Text> <Text style={styles.name}>{this.props.data.name}Text> TouchableOpacity> ) } jumpTo(detailurl){ let url=detailurl;//对url进行处理,去掉url前面没用的部分 url=detailurl.replace('imeituan://www.meituan.com/web/?url=',''); this.props.onClick({url:url});//触发父组件onOnclick,并传入url参数 } }
App中并不是所有的页面都是写死的,这样很不易于维护与更新。一些页面是通过网页来实现的,在App中点击时跳转到对应的网页。当我们想要修改时,只需要更新在服务器端网页就可以,而不必更新App、重新发布等。这种思维就是一种Hybrid混合开发的思维。
例如当点击购物中心时跳转到ShopDetail页面,并通过navigation传入对应网页的url,在ShopDetail中只需通过
ShopDetail.js就只有很短几行用于呈现WebView:
export default class ShopDetail extends Component { static navigationOptions={ title:'商场详情', headerStyle:{ //导航栏样式设置 backgroundColor:'#8bffce', }, }; render() { let url=this.props.navigation.state.params.url+ '?uuid=5C7B6342814C7B496D836A69C872'; return ( <WebView source={{uri: url}} javaScriptEnabled={true} domStorageEnabled={true} /> ) } }
之前一直通过debug来将react native安装到手机上,如果需要发行则需要打包生成apk。
Android要求所有应用都有一个数字签名才会被允许安装在用户手机上,所有首先需要生成一个签名密钥。要通过keytool生成密钥,首先进入jdk下的bin目录,打开cmd输入如下命令
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
其中my-release-key为密钥库的名字,my-key-alias为密钥库别名可以自定义,接着会出现命令行提示,要求输入相关信息,并设置相关密码,之后会在当前目录下生成my-release-key.keystore文件。
把该文件拷贝到react native工程下的android/app目录下
在C:\Users\你的用户名\.gradle目录下新建gradle.properties文件,并在其中输入如下内容:
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore 密钥库的名字
MYAPP_RELEASE_KEY_ALIAS=my-key-alias 密钥库别名
MYAPP_RELEASE_STORE_PASSWORD=***** 密钥库密码
MYAPP_RELEASE_KEY_PASSWORD=***** 密钥密
我的密钥密码与库密码一致
打开react native项目下的android/app/build.gradle文件,添加如下内容
android {
...
defaultConfig { ... }
signingConfigs {
release {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
}
进入react native项目的android目录下执行cmd命令:
gradlew assembleRelease
生成的apk文件位于项目的
android/app/build/outputs/apk/app-release.apk。
在GitHub上的代码仓库为:https://github.com/SuperTory/React-Native-ECommerce