【Vue】Vue項(xiàng)目實(shí)戰(zhàn)2

此文項(xiàng)目代碼:https://github.com/bei-yang/I-want-to-be-an-architect
碼字不易,辛苦點(diǎn)個(gè)star,感謝!

引言


此篇文章主要涉及以下內(nèi)容:

  1. 數(shù)據(jù)和狀態(tài)管理實(shí)踐
  2. vuex模塊化
  3. vue動畫設(shè)計(jì)(項(xiàng)目中有購物時(shí),跳的小球動畫,很值得學(xué)習(xí))
  4. 全局組件實(shí)現(xiàn)與原理
  5. 全局回退管理

學(xué)習(xí)資源


輪播圖和商品列表


  • mock數(shù)據(jù),vue.config.js
    • 分析/api/goods接口數(shù)據(jù)結(jié)構(gòu)
  • goods服務(wù),service/goods.js
     import axios from 'axios'
    
     export default {
        getGoodsInfo(){
            return axios.get('/api/goods')
            .then(res=>{
                const {code, data: goodsInfo, slider, keys} = res.data;
                // 數(shù)據(jù)處理
                if (code) {
                    return {goodsInfo, slider, keys}
                } else {
                    return null;
                }
            })
        }
    }
    
  • 定義actions,store.js
    import gs from "@/service/goods";
    
    export default {
      state: {
        slider: [],
        keys: [],
        goodsInfo: {}
      },
      mutations: {
        setGoodsInfo(state, { slider, keys, goodsInfo }) {
          state.slider = slider;
          state.keys = keys;
          state.goodsInfo = goodsInfo;
        }
      },
      getters: { // 添加一個(gè)goods屬性,轉(zhuǎn)換對象形式為數(shù)組形式便于循環(huán)渲染
        goods: state => {
          return state.keys
            .map(key => state.goodsInfo[key])
            .reduce((prev, next) => prev.concat(next), []);
        }
      },
      actions: {
        getGoods({ state, commit }) {
            if (!state.keys.length) {
                // 沒有數(shù)據(jù)采去獲取
                gs.getGoodsInfo().then(goodsInfo => {
                    commit('setGoodsInfo', goodsInfo)
                })
            }
        }
      }
    };
    
  • 輪播圖、商品列表模板,Home.vue
    <template>
      <div class="home">
        <!-- 輪播圖 -->
        <cube-slide :data="slider" :interval="5000">
          <cube-slide-item v-for="(item,index) in slider" :key="index">
            <router-link :to="`/detail/${item.id}`">
              <img class="slider" :src="item.img">
            </router-link>
          </cube-slide-item>
        </cube-slide>
    
        <!-- 商品列表 -->
        <good-list :data="goods"></good-list>
      </div>
    </template>
    <script>
    import GoodList from "@/components/GoodList.vue";
    export default {
      name: "home",
      components: {
        GoodList,
      }
    };
    </script>
    
  • 輪播圖、商品列表數(shù)據(jù)獲取, Home.vue
      created() {
        this.getGoods(); // 數(shù)據(jù)初始化
      },
      computed: {
        ...mapState({ slider: state => state.goods.slider }),
        ...mapGetters(["goods"])
      },
      methods: {
        ...mapActions(["getGoods"]),
    

購物車

  • 購物車狀態(tài),store.js
    export default {
      state: {
        // 購物車初始狀態(tài)
        cart: JSON.parse(localStorage.getItem("cart")) || []
      },
      mutations: {
        addcart(state, item) {
          // 添加商品至購物車
          const good = state.cart.find(v => v.title == item.title);
          if (good) {
            good.cartCount += 1;
          } else {
            state.cart.push({
              ...item,
              cartCount: 1
            });
          }
        },
        cartremove(state, index) {
          // count-1
          if (state.cart[index].cartCount > 1) {
            state.cart[index].cartCount -= 1;
          }
        },
        cartadd(state, index) {
          // count+1
          state.cart[index].cartCount += 1;
        }
      },
      getters: {
        cartTotal: state => {
          // 商品總數(shù)
          let num = 0;
          state.cart.forEach(v => {
            num += v.cartCount;
          });
          return num;
        },
        total: state => {
          // 總價(jià)
          return state.cart.reduce(
            (total, item) => total + item.cartCount * item.price,
            0
          );
        }
      }
    };
    
  • 購物車顯示,Cart.vue
  • 在導(dǎo)航欄里顯示購物數(shù)量,App.vue
    import {mapGetters} from 'vuex'
    
    computed:{
    ...mapGetters(['cartTotal'])
    }
    
    <cube-tab-bar v-model="selectLabel" :data="tabs" @change="changeHandler">
      <cube-tab v-for="(item, index) in tabs" 
                :icon="item.icon" :label="item.value" :key="index">
        <div>{{item.label}}</div>
        <span class="badge" v-if="item.label=='Cart'">{{cartTotal}}</span>
      </cube-tab>
    </cube-tab-bar>
    
  • vuex模塊化
    • 創(chuàng)建store目錄,創(chuàng)建user.js/cart.js/goods.js
    // goods.js
    import gs from "@/service/goods";
    
    export default {
      state: {
        slider: [],
        keys: [],
        goodsInfo: {}
      },
      mutations: {
        setGoodsInfo(state, { slider, keys, goodsInfo }) {
          state.slider = slider;
          state.keys = keys;
          state.goodsInfo = goodsInfo;
        }
      },
      getters: { // 添加一個(gè)goods屬性,轉(zhuǎn)換對象形式為數(shù)組形式便于循環(huán)渲染
        goods: state => {
          return state.keys
            .map(key => state.goodsInfo[key])
            .reduce((prev, next) => prev.concat(next), []);
        }
      },
      actions: {
        getGoods({ state, commit }) {
            if (!state.keys.length) {
                // 沒有數(shù)據(jù)采去獲取
                gs.getGoodsInfo().then(goodsInfo => {
                    commit('setGoodsInfo', goodsInfo)
                })
            }
        }
      }
    };
    // cart.js
    export default {
      state: {
        // 購物車初始狀態(tài)
        cart: JSON.parse(localStorage.getItem("cart")) || []
      },
      mutations: {
        addcart(state, item) {
          // 添加商品至購物車
          const good = state.cart.find(v => v.title == item.title);
          if (good) {
            good.cartCount += 1;
          } else {
            state.cart.push({
              ...item,
              cartCount: 1
            });
          }
        },
        cartremove(state, index) {
          // count-1
          if (state.cart[index].cartCount > 1) {
            state.cart[index].cartCount -= 1;
          }
        },
        cartadd(state, index) {
          // count+1
          state.cart[index].cartCount += 1;
        }
      },
      getters: {
        cartTotal: state => {
          // 商品總數(shù)
          let num = 0;
          state.cart.forEach(v => {
            num += v.cartCount;
          });
          return num;
        },
        total: state => {
          // 總價(jià)
          return state.cart.reduce(
            (total, item) => total + item.cartCount * item.price,
            0
          );
        }
      }
    };
    
    • 將store.js移進(jìn)去,重命名為index.js
    import Vue from "vue";
    import Vuex from "vuex";
    import user from './user'
    import goods from './goods'
    import cart from './cart'
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
    modules:{
      user, goods, cart
    }
    });
    
    • 代碼中只有state映射需要修改
    // home.vue
    ...mapState({
      slider:state=>state.goods.slider
    })
    // cart.vue
    ...mapState({cart:state=>state.cart.cart})
    

動畫設(shè)計(jì)


  • vue動畫
  • 頁面切換動畫,App.vue
  • 添加購物車動畫
    • 創(chuàng)建購物車動畫組件,CartAnim.vue
    <template>
      <div class="ball-wrap">
        <transition @before-enter="beforeEnter"
                    @enter="enter"
                    @afterEnter="afterEnter">
          <div class="ball"
              v-show="show">
            <div class="inner">
              <div class="cubeic-add"></div>
            </div>
          </div>
        </transition>
      </div>
    </template>
    <script>
    export default {
      name: "cartAnim",
      data () {
        return {
          show: false
        }
      },
      methods: {
        start (el) { // 啟動動畫接口,傳遞點(diǎn)擊按鈕元素  
          this.el = el;
          // 使.ball顯示,激活動畫鉤子
          this.show = true;
        },
        beforeEnter (el) {
          // 把小球移動到點(diǎn)擊的dom元素所在位置
          const rect = this.el.getBoundingClientRect();
          // 轉(zhuǎn)換為用于絕對定位的坐標(biāo)
          const x = rect.left - window.innerWidth / 2;
          const y = -(window.innerHeight - rect.top - 10 - 20);
          // ball只移動y
          el.style.transform = `translate3d(0,${y}px,0)`;
          // inner只移動x
          const inner = el.querySelector('.inner');
          inner.style.transform = `translate3d(${x}px,0,0)`;
        },
        enter (el, done) {
          // 獲取offsetHeight就會重繪
          document.body.offsetHeight;
          // 指定動畫結(jié)束位置
          el.style.transform = `translate3d(0,0,0)`;
          const inner = el.querySelector('.inner');
          inner.style.transform = `translate3d(0,0,0)`;
          el.addEventListener('transitionend', done)
        },
        afterEnter (el) {
          // 動畫結(jié)束,開始清理工作
          this.show = false;
          el.style.display = 'none';
          this.$emit('transitionend');
        }
      }
    }
    </script>
    <style lang="stylus" scoped>
    .ball-wrap {
      .ball {
        position: fixed;
        left: 50%;
        bottom: 10px;
        z-index: 100000;
        color: red;
        transition: all 0.5s cubic-bezier(0.49, -0.29, 0.75, 0.41);
    
        .inner {
          width: 16px;
          height: 16px;
          transition: all 0.5s linear;
    
          .cubeic-add {
            font-size: 22px;
          }
        }
      }
    }
    </style>
    
  • 使用動畫,Home.vue
 <good-list @cartanim='$refs.ca.start($event)'></good-list>
 <cart-anim ref='ca'></cart-anim>
 import CartAnim from '@/componets/CartAnim.vue'
 components: { CartAnim }
  • 觸發(fā)動畫,GoodList.vue
 <i class='cubeic-add'
    @click.stop.prevent="addCart($event,item)"></i>
 addCart (e, item) {  // 需要傳遞事件目標(biāo)
   this.$store.commit("addcart", item);
   // 觸發(fā)動畫時(shí)間
   this.$emit('startcartanim', e.target)
 }

動畫有兩個(gè)問題:
1. 使用比較麻煩
2. 不能生成多個(gè)動畫實(shí)例

動態(tài)全局組件設(shè)計(jì)與實(shí)現(xiàn)


  • 使用cube-ui的create-api

    • 注冊
    import {createAPI} from 'cube-ui'
    import CartAnim from '@/components/CartAnim'
    
    createAPI(Vue,BallAnim,['transitionend'])
    
    • 調(diào)用api,Home.vue
    <good-list :data='goods' @startcartanim='startCartAnim'></good-list>
    methods:{
      startCartAnim(el){
        const anim=this.$createCartAnim({
          onTransitionend(){
            anim.remove();
          }
        });
        anim.start(el);
      }
    }
    

    create-api的原理是動態(tài)創(chuàng)建組件并全局掛載至body中,下面我們自己實(shí)現(xiàn)一下

    • 組件動態(tài)創(chuàng)建并掛載的具體實(shí)現(xiàn)
      • 定義動態(tài)創(chuàng)建函數(shù):./utils/create.js
      import Vue from 'vue';
      
      // 創(chuàng)建函數(shù)接收要創(chuàng)建組件定義
      function create(Component, props) {
        // 創(chuàng)建一個(gè)Vue新實(shí)例
        const instance = new Vue({
          render(h) {
            // render函數(shù)將傳入組件配置對象轉(zhuǎn)換為虛擬dom
            console.log(h(Component, {
              props
            }));
            return h(Component, {
              props
            });
          }
        }).$mount(); // 執(zhí)行掛載函數(shù),但未指定掛載目標(biāo),表示只執(zhí)行初始化、編譯等工作
        // 將生成dom元素追加至body
        document.body.appendChild(instance.$el)
        // 給組件實(shí)例添加銷毀方法
        const comp = instance.$children[0];
        comp.remove = () => {
          document.body.removeChild(instance.$el);
          instance.$destroy();
        };
        return comp;
      }
      
      // 暴露調(diào)用接口
      export default create;
      
      • 掛載到vue實(shí)例上,main.js
      import create from '@/utils/create'
      Vue.prototype.$create=create;
      
      • 調(diào)用,Home.vue
      startCartAnim(el){
        const anim=this.$create(CartAnim);
        anim.start(el);
        anim.$on('transitionend',anim.remove);
      }
      
      • 還可以傳遞屬性到組件,增加組件可用性
      // Home.vue
      const anim=this.$create(CartAnim,{
        pos:{left:'45%',bottom:'10px'}
      });
      // CartAnim.vue
      <div class='ball' v-show='show' :style='pos'>
      props:['pos']
      

頁頭組件


  • 組件定義,Header.vue
    <template>
      <div class="header">
        <h1>{{title}}</h1>
        <i v-if="$routerHistory.canBack()" @click="back" class="cubeic-back"></i>
        <div class="extend">
          <slot></slot>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        title: {
          type: String,
          default: "",
          required: true
        },
        showback: {
          type: Boolean,
          default: false
        }
      },
      methods: {
        back() {
          this.$router.goBack();
        }
      }
    };
    </script>
    <style lang="stylus" scoped>
    .header {
      position: relative;
      height: 44px;
      line-height: 44px;
      text-align: center;
      background: #edf0f4;
    
      .cubeic-back {
        position: absolute;
        top: 0;
        left: 0;
        padding: 0 15px;
        color: #fc915b;
      }
    
      .extend {
        position: absolute;
        top: 0;
        right: 0;
        padding: 0 15px;
        color: #fc915b;
      }
    }
    </style>
    
  • 使用,Home.vue
    <k-header title="XXX">
      <i class="cubeic-tag"></i>
    </k-header>
    
    import KHeader from '@/components/Header.vue';
    components:{
      KHeader
    }
    
  • 返回按鈕狀態(tài)自動判斷:history.length是不可靠的,它既包含了vue app路由記錄,也包括其他頁面的??梢蕴砑右粋€(gè)自定義的歷史記錄管理?xiàng)?,?chuàng)建./utils/history.js
    const History = {
      _history: [], // 歷史記錄堆棧
      install(vue) {
        // vue插件要求的安裝方法
        Object.defineProperty(Vue.prototype, "$routerHistory", {
          get() {
            return History;
          }
        });
      },
      push(path) {
        // 入棧
        this._current += 1;
        this._history.push(path);
      },
      pop() {
        // 出棧
        this._current -= 1;
        return this._history.pop();
      },
      canBack() {
        return this._history.length > 1;
      }
    }
    export default History
    
  • router.js中引入,添加一個(gè)后退方法并監(jiān)聽afterEach從而管理記錄
    import History from './utils/history';
    import router from './【Vue】Vue項(xiàng)目實(shí)戰(zhàn)2/vue-mart/src/router';
    
    Vue.use(History);
    
    router.prototype.goBack=function(){
      this.isBack=true;
      this.back();
    };
    
    router.afterEach((to,from)=>{
      if(router.isBack){
        History.pop();
        router.isBack=false;
        router.transitionName='route-back';
      }else{
        History.push(to.path);
        router.transitionName='route-forward';
      }
    })
    
  • 使用,Header.vue
    <i v-if='$routerHistory.canBack()'></i>
    methods:{
      back(){this.$router.goBack()}
    }
    
  • 后退動畫,App.vue
    // 動態(tài)設(shè)置名稱
    <transition :name='transitionName'>
      <router-view class='child-view'></router-view>
    </transition>
    watch: {
    // 動態(tài)設(shè)置動畫方式
    this.transitionName = this.$router.transitionName
    },
    .route-forward-enter {
    transform: translate3d(-100%, 0, 0);
    }
    .route-back-enter {
    transform: translate3d(100%, 0, 0);
    }
    /* 出場后 */
    .route-forward-leave-to {
    transform: translate3d(100%, 0, 0);
    }
    .route-back-leave-to {
    transform: translate3d(-100%, 0, 0);
    }
    .route-forward-enter-active,
    .route-forward-leave-active,
    .route-back-enter-active,
    .route-back-leave-active {
    transition: transform 0.3s;
    }
    

你的贊是我前進(jìn)的動力

求贊,求評論,求分享...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容