【Vue】Vue项目实战2
2019-10-21 本文已影响0人
LM林慕
此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!
引言
此篇文章主要涉及以下内容:
- 数据和状态管理实践
-
vuex
模块化 -
vue
动画设计(项目中有购物时,跳的小球动画,很值得学习) - 全局组件实现与原理
- 全局回退管理
学习资源
- TarBar导航栏使用
- 轮播组件使用
- vuex模块化
- vue动画设计
- 动态全局组件创建
- vue渲染函数原理
- vue的$mount函数
轮播图和商品列表
- 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; }
你的赞是我前进的动力
求赞,求评论,求分享...