用React-Native+Mobx做一個迷你水果商城APP
前言
最近一直在學習微信小程式,在學習過程中,看到了wxapp-mall這個微信小程式的專案,覺得很不錯,UI挺小清新的,便clone下來研究研究,在看原始碼過程中,發現並不複雜,用不多的程式碼來實現豐富的功能確實令我十分驚喜,於是,我就想,如果用react-native來做一個類似這種小專案難不難呢,何況,寫一套程式碼還能同時跑android和ios(小程式也是。。。),要不寫一個來玩玩?有了這個想法,我便直接react-native init一個project來寫一下吧(๑•̀ㅂ•́)و✧
先來張動圖,dengdengdeng~~
技術框架以及元件
- 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要簡單,上手也比較快。在這個小專案中,因為沒有後臺服務介面,用的都是本地的假資料,為了模擬實現 瀏覽商品 =>加入購物車=>結賬=>清空購物車=>還原商品原始狀態
開始
先react-native init一個project,然後用yarn或者npm裝好所有的依賴和元件。因為使用Mobx會用到ES7中裝飾器,所以還要安裝babel-plugin-transform-decorators-legacy這個外掛,然後在.babelrc檔案下新增一下內容即可。
{
"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資料夾下的元件都寫有註釋的(°ー°〃)
主要還是來聊聊Mobx吧
先來看看用Mobx實現的具體流程,看下面的動圖(⊙﹏⊙)
ps: 可能圖片太大,載入有點慢,請稍等......
1.資料儲存和獲取
這些都是用假資料來模擬實現的,在最開始,先寫好假資料的資料結構,例如:
"data":
[{
"name": '那麼大西瓜',
"price": '2.0',
"image": require('../img/a11.png'),
"count": 0,
"isSelected": true
},...]
在Mobx資料夾下的store.js,在這裡主要是儲存和管理app用到的所有商品的資料,將邏輯和狀態從元件中移至一個獨立的,可測試的單元,這個單元在每個頁面下都可以用到
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),
這樣,可以通過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注入的
@observerexport 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.js
的CartStore類
中的,下面貼出程式碼
// 購物車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,因為我刪除商品方法其實是根據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~