美文网首页vue
Vue实战(四) - 实战项目(下) - 路由初步/复杂状态管理

Vue实战(四) - 实战项目(下) - 路由初步/复杂状态管理

作者: ElliotG | 来源:发表于2021-05-10 20:35 被阅读0次

0. 实战场景介绍

通过Vuex构建全局状态管理的购物车。
通过路由功能实现购物车的跳转。

 

1. 创建空白购物车

src/components文件夹下新建组件,先做个简单的占位。
ShoppingCart.vue

<template>
    <h4 class="bg-primary text-white text-center p-2">
        Placeholder for Cart
    </h4>
</template>

 

2. 路由

2-1) 配置路由

新建src/router文件夹, 新增文件index.js
如下

import Vue from "vue";
import VueRouter  from "vue-router";
import Home from "../components/Home";
import ShoppingCart from "../components/ShoppingCart";

Vue.use(VueRouter);
export default new VueRouter({
    mode: "history",
    routes: [
        { path: "/", component: Home },
        { path: "/cart", component: ShoppingCart },
        { path: "*", redirect: "/"}
    ] 
})

代码解释:

1. VueRouter通过Vue.use注册。
2. 路由通过路径指定组件。

2-2) 引用路由

一般在main.js中引用路由,如下:

...
import router from "./router";

new Vue({
    render: h => h(App), 
    store,
    router
}).$mount('#app')

2-3) 显示路由

一般在App.vue中增加一个路由视图,如下:

<template>
    <router-view />
</template>

<script>
  //import Home from "./components/Home";
  import { mapActions } from "vuex";

  export default {
      name: 'app',
      //components: { Home },
      methods: {
          ...mapActions(["getData"])
      },
      created() {
          this.getData();
      }
} </script>

代码解释:

可以看到,我们注释了原来显式地显示首页组件的代码,替换成了路由视图。

效果展示:
当我们访问http://localhost:8080/cart的时候,显示如下界面

image.png

 

3. 实现购物车跳转功能

3-1) 新增单独的购物车状态存储模块

src/store文件夹下新增cart.js

export default {
    namespaced: true,
    state: {
      lines: [] 
    },
    getters: {
        itemCount: state => state.lines.reduce(
          (total, line) => total + line.quantity, 0),
        
        totalPrice: state => state.lines.reduce(
          (total, line) => total + (line.quantity * line.product.price), 0),
    },
    mutations: {
        addProduct(state, product) {
            let line  = state.lines.find(line => line.product.id == product.id);
            if (line != null) {
                line.quantity++;
            } else {
                state.lines.push({ product: product, quantity:1 });
            }
        },
        changeQuantity(state, update) {
            update.line.quantity = update.quantity;
        },
        removeProduct(state, lineToRemove) {
            let index  = state.lines.findIndex(line => line == lineToRemove);
            if (index > -1) {
                state.lines.splice(index, 1);
            }
        } 
    }
}

代码解释:

1. namespaced设置为true是为了让state, getters, mutations这些属性在整个状态存储中是独立的。

3-2) 在主状态中引用子状态模块(引用购物车状态存储)

修改src/store文件夹下的index.js
如下

...
import CartModule from "./cart";

...
export default new Vuex.Store({
    strict: true,
    modules:  { cart: CartModule },
    state: {
        ...
    },
    ...
})

3-3) 将商品添加到购物车

在产品列表组件中新增添加到购物车按钮。
ProductList.vue

<template>
    <div>
        <div v-for="p in products" v-bind:key="p.id" ...>
            ...
            <div class="card-text bg-white p-1">
                {{ p.description }}
                <button class="btn btn-success btn-sm float-right"
                        v-on:click="handleProductAdd(p)">
                    Add To Cart
                </button>
            </div>
        </div>
        <page-controls />
    </div>
</template>

<script>
import { mapGetters, mapMutations } from "vuex";
...

export default {
    ...
    methods: {
        ...mapMutations({ addProduct: "cart/addProduct" }),
        handleProductAdd(product) {
            this.addProduct(product);
            this.$router.push("/cart");
        }
    }   
}
</script>

代码解释:

通过this.$router.push方法来改变当前路由。

效果如下:

image.png

 

4. 实现购物车显示功能

我们知道,购物车是我们购物的清单。
既然是清单,它应该是由许多条目组成的。
每个条目是一个独立的组件。

4-1) 实现购物车条目组件

ShoppingCartLine.vue

<template>
    <tr>
        <td>
            <input type="number" class="form-control-sm"
                style="width:5em"
                v-bind:value="qvalue"
                v-on:input="sendChangeEvent"/>
        </td>
        <td>{{ line.product.name }}</td>
        <td class="text-right">
            {{ line.product.price | currency }}
        </td>
        <td class="text-right">
            {{ (line.quantity * line.product.price) | currency }}
        </td>
        <td class="text-center">
            <button class="btn btn-sm btn-danger"
                    v-on:click="sendRemoveEvent">
                Remove
            </button>
        </td> 
    </tr>
</template>

<script>
    export default {
        props: ["line"],
        data: function() {
            return {
                qvalue: this.line.quantity
            }
        }, 
        methods: {
            sendChangeEvent($event) {
                if ($event.target.value > 0) {
                    this.$emit("quantity", Number($event.target.value));
                    this.qvalue = $event.target.value;
                } else {
                    this.$emit("quantity", 1);
                    this.qvalue = 1;
                    $event.target.value = this.qvalue;
                } 
            },
            sendRemoveEvent() {
                this.$emit("remove", this.line);
            } 
        }
    } 
</script>

代码解释:

1. 组件的props属性用于父子组件传递数据,这里我们由父组件(ShoppingCart)来传递line数据给条目组件。
2. this.$emit方法用于向父组件发送事件。

4-2) 购物车组件显示条目子组件

修改src/components文件夹下的ShoppingCart.vue

<template>
    <div class="container-fluid">
        ...
        <div class="row">
            ...
            <h2 class="text-center">Your Cart</h2>
            <table class="table table-bordered table-striped p-2">
                <thead> <tr>
                    <th>Quantity</th><th>Product</th>
                    <th class="text-right">Price</th>
                    <th class="text-right">Subtotal</th>
                </tr>
                </thead>
                <tbody>
                    <tr v-if="lines.length == 0">
                        <td colspan="4" class="text-center">
                            Your cart is empty
                        </td>
                    </tr>
                    <cart-line v-for="line in lines" v-bind:key="line.product.id"
                        v-bind:line="line"
                        v-on:quantity="handleQuantityChange(line, $event)"
                        v-on:remove="remove" />
                </tbody>
                ...
            </table>
            ...
        </div>
        ...
    </div>
</template>

<script>
import { mapState, mapMutations, mapGetters } from "vuex";
import CartLine from "./ShoppingCartLine";

export default {
    components: { CartLine },
    computed: {
        ...mapState({ lines: state => state.cart.lines }),
        ...mapGetters({  totalPrice : "cart/totalPrice"  })
    },
    methods: {
        ...mapMutations({
            change: "cart/changeQuantity",
            remove: "cart/removeProduct"
        }),
        handleQuantityChange(line, $event) {
            this.change({ line, quantity: $event});
        } 
    }
} 
</script>

重要代码段:

<cart-line v-for="line in lines" v-bind:key="line.product.id"
    v-bind:line="line"
    v-on:quantity="handleQuantityChange(line, $event)"
    v-on:remove="remove" />

代码解释:

1. v-bind指令用于父组件绑定子组件的line属性。
2. v-on用于父组件接收子组件传递过来的事件。

效果如下:

image.png

 

5. 购物车持久化

购物车需要持久化,这样我们在刷新浏览器或关闭网页下次进去的时候依旧可以看到我们购物车的内容。
可以持久化到服务器,也可以持久化到客户端Cookie之类的。
这里我们持久化到浏览器的本地存储(localStorage)。

5-1) 给cart状态存储增加客户端持久化

修改src/store文件夹下的cart.js,如下:

export default {
    ...
    mutations: {
        ...
        setCartData(state, data) {
            state.lines = data;
        }
    },
    actions: {
        loadCartData(context) {
            let data = localStorage.getItem("cart");
            if (data != null) {
                context.commit("setCartData", JSON.parse(data));
            }
        },
        storeCartData(context) {
            localStorage.setItem("cart", JSON.stringify(context.state.lines));
        },
        clearCartData(context) {
            context.commit("setCartData", []);
        },
        initializeCart(context, store) {
            context.dispatch("loadCartData");
            store.watch(state => state.cart.lines,
                    () => context.dispatch("storeCartData"), { deep: true});
        }
    }
}

代码解释:

1. cart的核心状态保存在lines属性,但是action不允许直接操作状态属性,
    因此我们引入了setCartData这个mutation方法。
2. initializeCart为初始化购物车,该方法我们计划在程序的入口组件App.vue中调用。
3. localStorage中我们存放的是lines属性的JSON序列化字符串。
4. loadCartData这个action中我们通过context.commit来调用setCartData这个mutation
    来把反序列化以后的lines设置回去。
5. store.watch方法并设置deep: true,监控状态发生改变则调用storeCartData来更新localStorage。

5-2) 购物车的初始化加载

修改src根目录下的App.vue,如下:

...
<script>
  ...
  export default {
      ...
      methods: {
          ...mapActions({
            getData: "getData",
            initializeCart: "cart/initializeCart"
          }) 
      },
      created() {
          this.getData();
          this.initializeCart(this.$store);
      } 
  }
</script>

代码解释:

1. 我们在methods中增加了一条方法initializeCart,它调用cart状态存储下的initializeCart方法。
2. created方法的最后调用刚才的initializeCart,在组件初始化的最后初始化购物车。

 

5. 实现购物车价格汇总功能

5-1) 新建购物车汇总组件

src/components目录下新建CartSummary.vue,如下:

<template>
    <div class="float-right">
        <small>
            Your cart:
            <span v-if="itemCount > 0">
                {{ itemCount }} item(s) {{ totalPrice | currency }}
            </span>
            <span v-else>
                (empty)
            </span>
        </small>
        <router-link to="/cart" class="btn btn-sm bg-dark text-white"
                v-bind:disabled="itemCount == 0">
            <i class="fa fa-shopping-cart"></I>
        </router-link>
    </div>
</template>

<script>
    import { mapGetters } from 'vuex';

    export default {
        computed: {
            ...mapGetters({
                itemCount: "cart/itemCount",
                totalPrice: "cart/totalPrice"
            }) 
        }
    } 
</script>

5-2) 在首页组件添加购物车汇总组件

修改src/components文件夹下的Home.vue,如下:

<template>
    <div class="container-fluid">
        <div class="row">
            <div class="col bg-dark text-white">
                <a class="navbar-brand">SPORTS STORE</a>
                <cart-summary />
            </div>
        </div>
        ...
    </div>
</template>

<script>
    ...
    import CartSummary from "./CartSummary";

    export default {
        components: { ..., CartSummary }
    } 
</script>

效果图如下:


image.png

相关文章

网友评论

    本文标题:Vue实战(四) - 实战项目(下) - 路由初步/复杂状态管理

    本文链接:https://www.haomeiwen.com/subject/uvivdltx.html