React Native 实战外卖购物车(一)

孔乙己便涨红了脸,额上的青筋条条绽出,争辩道,‘窃书不能算偷……窃书!……读书人的事,能算偷么?’

简介

是不是觉得外卖App的点单体验很好呢,有颜值,有身材,还可以任意摩擦。今天我们就逐步讲解怎样用React Native开发一个。本实战分为三部分,第一部分实现基本的无选项商品选购,第二部分加入商品选项的支持和购物车功能,最后一部分将通过加入列表section优化商品陈列界面。先看第一部分成品效果:

商店主页

布局分析

这个页面的布局划分还是比较清晰,可以把整个页面看做三大块:

  • 最上方的导航栏,由于本次实战并不涉及页面跳转和路由,所以也就没有在导航上做文章,仅仅起到显示商店名称的作用。
  • 中间部分的ScrollView(为什么不用FlatList呢?卖个关子下节再说),用于用户通过上下滑动来浏览商品,占据着屏幕绝大部分面积。其中的每个商品则可根据商品数据循环添加至ScrollView中。
  • 固定在最下方的购物车区域,又包含数量计数、总价、和确认按钮。

随着这个思路,这个页面的代码结构即可出炉:

render() {
    let shopItems = this.state.shopItems.map((shopItem) => {
        return 
            <ShopItem/>
    })
    return (
        <View style={styles.shopContainer}>
            <NavigationBar />
            <View style={styles.shopItemListContainer}>
                <ScrollView>
                    {shopItems}
                </ScrollView>
            </View>
            <View style={styles.cartContainerContainer}>
                <View style={styles.cartSummaryContainer}>
                    <View style={styles.cartSummaryIconContainer}>
                        <Icon name={"ios-cart-outline"} size={25} color={"#666"} />
                    </View>
                    <Text style={styles.cartSummaryText}>¥{this.state.total_price}</Text>
                </View>
                <Button style={styles.checkoutButton} textStyle={styles.checkoutButtonText}>
                    选好了
                </Button>
            </View>
        </View>
    );
}

可以看到ScrollView中被塞入了ShopItem组件的列表,这个组件是什么我们一会说,通过以上简单的组件结构,即可把整个页面勾勒出来。这里除了React Native自带的组件,还用到了几个第三方组件库:

import NavigationBar from 'react-native-navbar';
import Badge from 'react-native-smart-badge';
import Button from 'react-native-smart-button';
import Icon from 'react-native-vector-icons/Ionicons';
  • react-native-navbar:用于导航栏
  • react-native-smart-badge:用于购物车右上方的商品数量标记
  • react-native-smart-button:用于左下的按钮
  • react-native-vector-icons/Ionicons:用于购物车图标

感兴趣的小伙伴们可以自行查阅它们的用法。

添加样式

看到没有样式的页面我是拒绝的。

在添加了以下样式之后,页面总算颜值上线。

const styles = StyleSheet.create({
    shopContainer: { flex: 1, backgroundColor: "#eee" },
    shopItemListContainer: { flex: 1, },
    cartContainerContainer: {
        height: 50, backgroundColor: "white", flexDirection: 'row',
        justifyContent: 'space-between', alignItems: 'center'
    },
    cartSummaryContainer: {
        flex: 1, paddingHorizontal: 10, paddingVertical: 5, flexDirection: 'row'
    },
    cartSummaryIconContainer: {
        height: 40, width: 40, backgroundColor: '#eee', borderRadius: 20, padding: 8
    },
    cartSummaryBadge: { marginLeft: -10 },
    cartSummaryText: {
        fontSize: 20, alignSelf: 'center', marginLeft: 10, color: '#1E1EEB'
    },
    checkoutButton: {
        height: 50, width: 150, backgroundColor: '#1E1EEB', justifyContent: 'center',
    },
    checkoutButtonText: {
        fontSize: 17, color: '#ffffff'
    },
});

这里着重说明几个属性:

  • flex属性描述了组件在父级组件之下的比重,所有的组件的默认flex: 0,其值越大,比重也就越大。在上述页面中,由于导航栏和购物车区域flex都为0,所以使得flex: 1的中间部分沾满了所有其余位置
  • flexDirection属性控制了组件中所包含组件的排列方向,所有组件默认flexDirection: 'column'(与React相反)。在购物车区域中需要把这个属性设为row使得所包含的内容横向排列
  • justifyContent属性描述了组件中所包含组件的分布方式,购物车区域中将此属性设为space-between使所包含组件等距间隔分开

业务数据

在说明ShopItem组件之前,先介绍下商店主页面所需的数据:

this.state = {
    total_price: 0,
    item_count: 0,
    shopItems: [{
        id: 1, name: "提拉米苏", price: 10, avatar_url: "https://domain.com/img",
    }, ... ]
},

本例中商品数据为假数据,结构如上,实际项目中需自行做出相应调整。在商店主页中使用ShopItem组件时会把商品信息全部带入。

let shopItems = this.state.shopItems.map((shopItem) => {
    return 
        <ShopItem
            key={shopItem.id} ref={shopItem.id} shopItem={shopItem}
            onItemRemoved={this._onItemRemoved}
            onItemAdded={this._onItemAdded}/>
})

同时还会带入两个商品添加/移除的函数用于计算购物车中商品数量和总价。

商品组件

商店页面攒好之后,下面来看看商品组件是何许人也,又是如何使用通过this.props所带入的参数进行商品选购的。

页面布局与样式

整个页面虽然结构纵横交错,但仍然可以层层递进,并将其大卸八块:

return (
    <View style={styles.shopItemContainer}>
        <View style={styles.shopItemAvatarContainer}>
            <Image style={styles.shopItemAvatar}
                   source={{uri: this.props.shopItem.avatar_url}}/>
        </View>
        <View style={styles.shopItemInfoContainer}>
            <Text style={styles.shopItemNameText}>{this.props.shopItem.name}</Text>
            <Text style={styles.shopItemPriceText}>¥{this.props.shopItem.price}</Text>
        </View>
        <View style={styles.shopItemCounterContainer}>
            <Icon.Button name="ios-remove-circle-outline" size={25}
                         color="black" backgroundColor="white" onPress={this._onItemRemovePress}
                         display={this.state.count === 0 ? 'none' : 'flex'}/>
            <View display={this.state.count === 0 ? 'none' : 'flex'} width={25} alignItems="center">
                <Text style={styles.shopItemCountText}>{this.state.count}</Text>
            </View>
            <Icon.Button name="ios-add-circle-outline" size={25}
                         color="black" backgroundColor="white" onPress={this._onItemAddPress}/>
        </View>
    </View>
)

加之以样式:

const styles = StyleSheet.create({
    shopItemContainer: {
        flex: 1, borderTopColor: "#999", borderTopWidth: 0.5, 
        backgroundColor: "#fff", flexDirection: 'row', padding: 10,
    },
    shopItemAvatar: {width: 65, height: 65, resizeMode: 'center'},
    shopItemAvatarContainer: {
        width: 65, height: 65, borderWidth: 0.5, borderColor: "#999"
    },
    shopItemInfoContainer: {
        flex: 1, justifyContent: 'space-around', paddingHorizontal: 10
    },
    shopItemNameText: { fontSize: 18 },
    shopItemPriceText: { fontSize: 14, color: '#1E1EEB' },
    shopItemCounterContainer: { flexDirection: 'row', alignItems: 'center'}
})

如上的页面即可把商品信息很好的展示出来。

业务逻辑

之前我们搭建了一个很漂亮的页面,并通过组件嵌套的方式把商品数据展示出来。然而这个外卖商店并没什么卵用,因为我们还不能进行商品的选购。万事俱备,只欠东风,最后我们要让购物车动起来。

我们在商品组件中为添加/移除按钮分别绑定了本地处理函数,还记得在商店页面调用ShopItem时带入的两个商品添加/移除函数吗,这里只需要将本地函数和传参函数关联,即可将ShopItem组件中的操作信息传递至商店主页面,从而进行购物车物品信息汇总:

_onItemRemovePress  = () => {
    this.setState({
        count: this.state.count - 1
    }, () => {
        this.props.onItemRemoved(this.props.shopItem)
    })
}

_onItemAddPress = () => {
    this.setState({
        count: this.state.count + 1
    }, () => {
        this.props.onItemAdded(this.props.shopItem)
    })
}

如此这般就应该可以做出本文开头动图的效果了。什么?商品添加的时候没有球球动画?这个就交给读者自己研究吧。(友情链接

目前这个外卖店还只能卖一些没有选项的物品,如果一杯咖啡的大/中/小杯分别都以不同商品的形式罗列出来,岂不谬哉!这个问题我们下回解决。