【Vue】Vue项目实战2

此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!

引言


此篇文章主要涉及以下内容:

  1. 数据和状态管理实践
  2. vuex模块化
  3. vue动画设计(项目中有购物时,跳的小球动画,很值得学习)
  4. 全局组件实现与原理
  5. 全局回退管理

学习资源


轮播图和商品列表


  • mock数据,vue.config.js
    • 分析/api/goods接口数据结构
  • goods服务,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;
                // 数据处理
                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: { // 添加一个goods属性,转换对象形式为数组形式便于循环渲染
        goods: state => {
          return state.keys
            .map(key => state.goodsInfo[key])
            .reduce((prev, next) => prev.concat(next), []);
        }
      },
      actions: {
        getGoods({ state, commit }) {
            if (!state.keys.length) {
                // 没有数据采去获取
                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>
    
  • 轮播图、商品列表数据获取, Home.vue
      created() {
        this.getGoods(); // 数据初始化
      },
      computed: {
        ...mapState({ slider: state => state.goods.slider }),
        ...mapGetters(["goods"])
      },
      methods: {
        ...mapActions(["getGoods"]),
    

购物车

  • 购物车状态,store.js
    export default {
      state: {
        // 购物车初始状态
        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 => {
          // 商品总数
          let num = 0;
          state.cart.forEach(v => {
            num += v.cartCount;
          });
          return num;
        },
        total: state => {
          // 总价
          return state.cart.reduce(
            (total, item) => total + item.cartCount * item.price,
            0
          );
        }
      }
    };
    
  • 购物车显示,Cart.vue
  • 在导航栏里显示购物数量,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模块化
    • 创建store目录,创建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: { // 添加一个goods属性,转换对象形式为数组形式便于循环渲染
        goods: state => {
          return state.keys
            .map(key => state.goodsInfo[key])
            .reduce((prev, next) => prev.concat(next), []);
        }
      },
      actions: {
        getGoods({ state, commit }) {
            if (!state.keys.length) {
                // 没有数据采去获取
                gs.getGoodsInfo().then(goodsInfo => {
                    commit('setGoodsInfo', goodsInfo)
                })
            }
        }
      }
    };
    // cart.js
    export default {
      state: {
        // 购物车初始状态
        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 => {
          // 商品总数
          let num = 0;
          state.cart.forEach(v => {
            num += v.cartCount;
          });
          return num;
        },
        total: state => {
          // 总价
          return state.cart.reduce(
            (total, item) => total + item.cartCount * item.price,
            0
          );
        }
      }
    };
    
    • 将store.js移进去,重命名为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})
    

动画设计


  • vue动画
  • 页面切换动画,App.vue
  • 添加购物车动画
    • 创建购物车动画组件,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) { // 启动动画接口,传递点击按钮元素  
          this.el = el;
          // 使.ball显示,激活动画钩子
          this.show = true;
        },
        beforeEnter (el) {
          // 把小球移动到点击的dom元素所在位置
          const rect = this.el.getBoundingClientRect();
          // 转换为用于绝对定位的坐标
          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;
          // 指定动画结束位置
          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) {
          // 动画结束,开始清理工作
          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 }
  • 触发动画,GoodList.vue
 <i class='cubeic-add'
    @click.stop.prevent="addCart($event,item)"></i>
 addCart (e, item) {  // 需要传递事件目标
   this.$store.commit("addcart", item);
   // 触发动画时间
   this.$emit('startcartanim', e.target)
 }

动画有两个问题:
1. 使用比较麻烦
2. 不能生成多个动画实例

动态全局组件设计与实现


  • 使用cube-ui的create-api

    • 注册
    import {createAPI} from 'cube-ui'
    import CartAnim from '@/components/CartAnim'
    
    createAPI(Vue,BallAnim,['transitionend'])
    
    • 调用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的原理是动态创建组件并全局挂载至body中,下面我们自己实现一下

    • 组件动态创建并挂载的具体实现
      • 定义动态创建函数:./utils/create.js
      import Vue from 'vue';
      
      // 创建函数接收要创建组件定义
      function create(Component, props) {
        // 创建一个Vue新实例
        const instance = new Vue({
          render(h) {
            // render函数将传入组件配置对象转换为虚拟dom
            console.log(h(Component, {
              props
            }));
            return h(Component, {
              props
            });
          }
        }).$mount(); // 执行挂载函数,但未指定挂载目标,表示只执行初始化、编译等工作
        // 将生成dom元素追加至body
        document.body.appendChild(instance.$el)
        // 给组件实例添加销毁方法
        const comp = instance.$children[0];
        comp.remove = () => {
          document.body.removeChild(instance.$el);
          instance.$destroy();
        };
        return comp;
      }
      
      // 暴露调用接口
      export default create;
      
      • 挂载到vue实例上,main.js
      import create from '@/utils/create'
      Vue.prototype.$create=create;
      
      • 调用,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
    }
    
  • 返回按钮状态自动判断:history.length是不可靠的,它既包含了vue app路由记录,也包括其他页面的。可以添加一个自定义的历史记录管理栈,创建./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中引入,添加一个后退方法并监听afterEach从而管理记录
    import History from './utils/history';
    import router from './【Vue】Vue项目实战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
    // 动态设置名称
    <transition :name='transitionName'>
      <router-view class='child-view'></router-view>
    </transition>
    watch: {
    // 动态设置动画方式
    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;
    }
    

你的赞是我前进的动力

求赞,求评论,求分享...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,319评论 0 10
  • vue-cli搭建项目 确保安装了node与npm 再目标文件夹下打开终端 执行cnpm i vue-cli -g...
    Akiko_秋子阅读 3,219评论 1 22
  • 深入响应式 追踪变化: 把普通js对象传给Vue实例的data选项,Vue将使用Object.defineProp...
    冥冥2017阅读 4,856评论 6 16
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,899评论 1 4
  • Vue官方文档以下内容作为本人日常学习使用,不作为参考 一、Vue环境搭建以及vue-cli的使用 Vue多页面应...
    好一只帅卤蛋阅读 739评论 0 1