用React-Native+Mobx作一個迷你水果商城APP

前言

最近一直在學習微信小程序,在學習過程當中,看到了wxapp-mall這個微信小程序的項目,以爲很不錯,UI挺小清新的,便clone下來研究研究,在看源碼過程當中,發現並不複雜,用很少的代碼來實現豐富的功能確實令我十分驚喜,因而,我就想,若是用react-native來作一個相似這種小項目難不難呢,況且,寫一套代碼還能同時跑android和ios(小程序也是。。。),要不寫一個來玩玩?有了這個想法,我便直接react-native init 一個project來寫一下吧(๑•̀ㅂ•́)و✧html

先來張動圖,dengdengdeng~~vue

                           

技術框架以及組件

  • react "16.0.0"
  • react-native "0.51.0"
  • mobx: "3.4.1"
  • mobx-react: "4.3.5"
  • react-navigation: "1.0.0-beta.21"
  • react-native-scrollable-tab-view: "0.8.0"
  • react-native-easy-toast: "1.0.9"
  • react-native-loading-spinner-overlay: "0.5.2"

爲何要用Mobx?

Mobx是可擴展的狀態管理工具,比react-redux要簡單,上手也比較快。在這個小項目中,由於沒有後臺服務接口,用的都是本地的假數據,爲了模擬實現 瀏覽商品 =>加入購物車=>結帳=>清空購物車=>還原商品原始狀態 這麼一個流程,便用Mobx來管理全部的數據以及商品的狀態(有沒有選中,有沒有加入購物車),這樣,全部的頁面均可以共享數據以及改變商品的狀態,頁面之間的數據和商品狀態都是同步更新的。具體用Mobx怎麼來實現這流程,在下面會分享使用感覺和遇到的一些小坑。node

開始

先react-native init一個project,而後用yarn或者npm裝好全部的依賴和組件。由於使用Mobx會用到ES7中裝飾器,因此還要安裝babel-plugin-transform-decorators-legacy這個插件,而後在.babelrc文件下添加一下內容便可。
react

{  
    "presets": ["react-native"],  
    "plugins": ["transform-decorators-legacy"]
}複製代碼

項目結構

|-- android 
|-- ios
|-- node_modules
|-- src
    |-- common // 公用組件
    |-- img // 靜態圖片
    |-- mobx // mobx store
        |-- newGoods.js // 首頁新品數據
        |-- cartGoods.js // 購物車數據
        |-- categoryGoods.js // 分類頁數據
        |-- store.js // store倉庫,管理數據狀態    
    |-- scene 
        |-- Cart // 購物車頁面
        |-- Category // 分類頁
        |-- Home // 首頁
        |-- ItemDetail // 商品信息頁
        |-- Mine // 個人頁面   
    |-- Root.js // root.js主要內容是配置react-navigation(導航器)
|-- index.js // 主入口

複製代碼

在Root.js文件中,有關react-navigation的配置和使用方法能夠參考下 官方文檔 和 這篇博客 ,裏面都寫得十分詳細,有關react-navigation的疑問我都在這2篇文章中找到答案,在這裏相關react-navigation配置,使用方法和項目裏面頁面佈局,組件寫法,在這裏不打算細說,由於都比較簡單,更多的是討論Mobx實現功能的一些邏輯和方法,screen文件夾下的組件都寫有註釋的(°ー°〃)android

主要仍是來聊聊Mobx吧

先來看看用Mobx實現的具體流程,看下面的動圖(⊙﹏⊙)ios

ps: 可能圖片太大,加載有點慢,請稍等......git

                         

1.數據存儲和獲取

這些都是用假數據來模擬實現的,在最開始,先寫好假數據的數據結構,例如:es6

"data":
    [{ 
        "name": "那麼大西瓜",
        "price": "2.0", 
        "image": require('../img/a11.png'),        
        "count": 0, 
        "isSelected": true
        },...]複製代碼

Mobx文件夾下的store.js,在這裏主要是存儲和管理app用到的全部商品的數據,將邏輯狀態從組件中移至一個獨立的,可測試的單元,這個單元在每一個頁面下均可以用到github

import { observable, computed, action } from 'mobx'
import cartGoods from './cartGoods'
import newGoods from './newGoods'
import categoryGoods from './catetgoryGoods'

/** 
* 根store 
* @class RootStore 
* CartStore 爲購物車頁面的數據 
* NewGoodsStore 爲首頁的數據 
* categoryGoodsStore 爲分類頁的數據 
*/

class RootStore {       
    constructor() {     
      this.CartStore = new CartStore(cartGoods,this)  
      this.NewGoodsStore = new NewGoodsStore(newGoods,this)   
      this.categoryGoodsStore = new categoryGoodsStore(categoryGoods,this)  
}}

Class CartStore{
    @observable  allDatas = {}    
    constructor(data,rootStore) {    
    this.allDatas = data 
    this.rootStore = rootStore 
    }
}

Class NewGoodsStore{
   ...跟上面同樣
}

Class categoryGoodsStore{
  ...跟上面同樣
}
// 返回RootStore實例  
export default new RootStore()複製代碼

這裏用了RootStore來實例化全部了stores(購物車,首頁,分類頁分別擁有各自的store),npm

這樣,能夠經過RootStore 來管理和操做stores,從而實現它們之間的相互通訊,共享引用。

其次,存儲數據用了Mobx的@observable方法,就是把數據成爲觀察者,當用戶操做視圖,致使數據發生變化時,注意,配合react-mobx提供的@observer能夠自動更新視圖,很是方便。

此外,爲了把Mobx 的Rootstore注入到react-native的組件中,要經過mobx-react提供的Provider實現,在Root.js下,我是這麼寫的:

// 全局註冊並注入mobx的Rootstore實例,首頁新品,分類頁,商品詳情頁,購物車頁面都要用到store
import {Provider} from 'mobx-react'
// 獲取store實例
import store from './mobx/store' 
const  Navigation = () => {   
 return (     
 <Provider rootStore={store}> 
 <Navigator/> 
 </Provider> 
)}
複製代碼

把Rootstore實例注入到組件樹中後,那麼,是否是在組件中直接使用this.props.rootStore就能夠取到了呢?

‘’不是的」,咱們還須要在要用到Rootstore的組件裏,要加點小玩意,在HomeScreen.js(首頁)中這麼寫:

import { inject, observer } from 'mobx-react'
@inject('rootStore') // 緩存rootStore,也就是在Root.js注入的
@observer // 將react組件轉變爲響應式組件, 數據改變自動觸發render函數
export default class HomeScreen extends Component {
     ......
}
複製代碼

加上了@inject('rootStore'),咱們就能夠愉快地使用 this.props.rootStore 來拿到咱們想要的數據啦^_^ ,一樣,在商品信息,分類頁,購物車頁面js下,也須要使用@inject('rootStore')來實現數據的獲取,而後再一步步地把數據傳到它們的子組件中。

2. 加入購物車的實現

在首頁和分類頁中,均可以點擊跳轉到商品信息頁,而後再加入到購物車裏

                      

實現方法

在itemDetail.js下,也就是商品信息頁面下,加入購物車的邏輯是這樣子的:

addCart(value) {
 if(this.state.num == 0) { 
    this.refs.toast.show('添加數量不能爲0哦~')
     return; 
}        
// 加入購物車頁面的列表上 
// 點一次,購物車數據同步刷新 
this.updateCartScreen(value)
this.refs.toast.show('添加成功^_^請前往購物車頁面查看')
}
// 同步更新購物車頁面的數據
updateCartScreen (value) { 
    let name = this.props.navigation.state.params.value.name;
    // 判斷購物車頁面是否存在一樣名字的物品 
    let index;
    if(this.props.rootStore.CartStore)
    index = this.props.rootStore.CartStore.allDatas.data.findIndex(e => (e.name === name))
    // 不存在
    if(index == -1) {
    this.props.rootStore.CartStore.allDatas.data.push(value) 
    // 加入CartStore裏
    // 並讓購物車icon更新
    let length = this.props.rootStore.CartStore.allDatas.data.length 
    this.props.rootStore.CartStore.allDatas.data[length - 1].count += this.state.num}
    else { 
    // 增長對應name的count
    this.props.rootStore.CartStore.allDatas.data[index].count += this.state.num  
    }
}複製代碼

簡單的說,先獲取水果的名稱name,而後再去判斷Mobx的CartStore裏面是否存在一樣的名稱的水果,若是有就增長對應name的數量count,若是沒有,就往CartStore中增長數據,切換到購物車頁面時,視圖會同步刷新,看到已加入購物車的水果。

3.改變商品狀態同步更新視圖

當用戶在購物車頁面操做商品狀態時,數據改變時,視圖會跟着同步刷新。

例如,商品的增長數量,減小數據,選中狀態,商品全選和商品刪除,總價格都會隨着商品的數量變化而變化。

圖又來了~~

                                

實現上面的功能,主要用到了Mobx提供的action方法,action是用來修改狀態的,也就是用action來修改商品的各類狀態(數量,選中狀態...),這些action,我是寫在store.jsCartStore類中的,下面貼出代碼

// 購物車store
class CartStore {
    @observable allDatas = {}
    constructor(data,rootStore) { 
    this.allDatas = data
    this.rootStore = rootStore
}
     //加
    @action
    add(money) { 
    this.allDatas.totalMoney += money 
}

    // 減
    @action
    reduce(money) { 
    this.allDatas.totalMoney -= money 
}
    // checkbox true 
    @action
    checkTrue(money) {
        this.allDatas.totalMoney += money
    }  
    // checkbox false
    @action
    checkFalse(money) {
    if(this.allDatas.totalMoney <=0 ) 
    return 
    this.allDatas.totalMoney -= money
}
    // 全選
    @action
    allSelect() {
    if(this.allDatas.isAllSelected) {
    // 重置totalMoney 
    this.allDatas.totalMoney = 0 
    this.allDatas.data.forEach(e=> {
    this.allDatas.totalMoney += e.count * e.price})}
    else { 
    this.allDatas.totalMoney = 0 
}}
    // check全選    
    @action 
    check() { 
    // 全部checkbox爲true時全選才爲true 
    let allTrue = this.allDatas.data.every(v => ( v.isSelected === true ))
    if(allTrue) { 
    this.allDatas.isAllSelected  = true 
    }else { 
    this.allDatas.isAllSelected = false 
}}
    // 刪 
    @action
    delect(name) { 
    this.allDatas.data = this.allDatas.data.filter (e => (e.name !== name ))
}
    // 總價格
    @computed get totalMoney() { 
    let money = 0;
    let arr =  this.allDatas.data.filter(e => (e.isSelected === true))
    arr.forEach(e=> (money += e.price * e.count))
    return money
}}複製代碼

全部修改商品狀態的邏輯都在上面代碼裏面,其中,totalMoney是用了Mobx的@computed方法,totalMoney是依賴於CartStore的data數據,也就是商品數據,但data的值發生改變時,它會從新計算返回。若是瞭解vue的話,這個就至關於vue的計算屬性。

4.結算商品

商品結算和清空購物車的邏輯都寫在CartCheckOut.js裏面,實現過程很簡單,貼上代碼吧:

// 付款
    pay() { 
    Alert.alert('您好',`總計:¥ ${this.props.mobx.CartStore.totalMoney}`, 
    {text: '確認支付', onPress: () => this.clear()},
    {text: '下次再買', onPress: () => null}],{ cancelable: false })}
    // 清空購物車 
    clear() { 
    this.setState({visible: !this.state.visible})
    setTimeout(()=>{ 
    this.setState({ loadText: '支付成功!歡迎下次光臨!' }) 
        setTimeout(()=> { this.setState({ visible: false },
        ()=>{ this.props.mobx.CartStore.allDatas.data = []
        // 把全部商品count都變爲0 
        this.props.mobx.NewGoodsStore.allDatas.data.forEach(e=> e.count = 0)
        this.props.mobx.categoryGoodsStore.allDatas.data.forEach( e => { 
        e.detail.forEach(value => { value.count = 0 }) 
  })
    })},1500)},2000)}複製代碼

這裏主要用了setTimeout和一些方法來模擬實現 支付中 => 支付完成 => 清空購物車 => 還原商品狀態。

好了,這個流程就搞定了,哈哈。

5.遇到的小坑

1.我寫了一個數組的亂序方法,裏面有用到Array.isArray()這個方法來判斷是否爲數組,可是,我用這個亂序函數時,想用來搞亂store裏面的數組時,發現一直沒有執行,以爲很奇怪。而後我直接用Array.isArray()這個方法來判斷store裏面的數組,返回的一直都是false。。。因而我就懵了。。。後來,我去看了Mobx官方文檔,終於找到了答案。原來,store裏面存放的數組,並非真正的數組,而是obverableArray,若是要讓Array.isArray()判斷爲true,就要在取到store的數組時,加個.slice()方法,或者Array.from()均可以。

2.一樣,也是obverableArray的問題。在購物車頁面時,我用了FlatList來渲染購物車的item,起初,當我增長商品到購物車,發現購物車頁面並無刷新。有了上面的踩坑經驗,我認爲是obverableArray引發的,由於FlatList的data接收的是real Array,因而,我用這樣的方法:

@computed get dataSource() { 
    return this.props.rootStore.CartStore.allDatas.data.slice();
}
...
<FlatList  data={this.dataSource} .../>複製代碼

因而,購物車視圖就能夠自動地刷新了,在官方文檔上也有寫到。

3.還有一個就是本身粗心形成的。我寫完這個項目後,和朋友出去玩時,順便發給朋友看看,他在刪除商品時發現,從上往下刪刪不了,從下往上刪就能夠。後來我用模擬器測試也是如此,因而就去看看刪除商品的邏輯,發現沒有問題,再去看store的數據,發現也是能夠同步更新的,只是視圖沒有更新,因而我又在FlatList去找緣由,終於,緣由找到了,主要是在keyExtractor裏面,用數組的index是不能夠的,要用name來做爲key,也就是說這裏的key值,要足夠穩定的,不能用index(索引)去綁定key,這也是react的語法之一。由於我刪除商品方法實際上是根據name來刪的,而不是index,因此用index來做爲FlatList的Item的key時是會出現bug的。

_keyExtractor = (item,index)=> { 
    // 千萬別用index,否則在刪購物車數據時,若是從第一個item開始刪會產生節點渲染錯亂的bug 
    return item.name
}複製代碼

寫在最後

總結

斷斷續續花了差很少一個星期才寫好,總得來講,我感受用react-native來寫這麼一個商城項目要比小程序實現要複雜點,主要是在寫組件上花的時間要多一點,和這裏用Mobx來模擬實現購物流程也花了我些時間。Android打包成apk能夠在個人模擬器上和我朋友的android手機上完美運行,還沒發現什麼bug,IOS的由於我沒MAC,因此暫時還沒打包測試T.T,但願有條件的小夥伴能夠clone下來,幫我測測,有Issue的話能夠提下,多謝多謝ヽ(✿゚▽゚)ノ

附上github項目地址: github.com/shooterRao/…  (若是感興趣,但願能點下Star,給予點鼓勵,謝謝!)

致謝

這個小項目的靈感出於wxapp-mall,在此款小程序的基礎上,優化了購物邏輯和一些交互上的修改。有些UI和Icon也沿用了此款小程序,我也獲得了原做者的容許,很是感謝。此外,我還要特別感謝肖JerryShaw幫我做的水果圖和App的logo,還有也要感謝Keson幫忙測試和提供建議。

此次是我第一次在掘金上發博客,也算是我第一次開源項目吧,有不足的地方,但願你們能多多包涵,給點建議,謝謝!

還有,今天是平安夜,Happy Christmas Eve~

相關文章
相關標籤/搜索