此文項(xiàng)目代碼:https://github.com/bei-yang/I-want-to-be-an-architect
碼字不易,辛苦點(diǎn)個(gè)star,感謝!
引言
此篇文章主要涉及以下內(nèi)容:
- 數(shù)據(jù)和狀態(tài)管理實(shí)踐
-
vuex模塊化 -
vue動畫設(shè)計(jì)(項(xiàng)目中有購物時(shí),跳的小球動畫,很值得學(xué)習(xí)) - 全局組件實(shí)現(xiàn)與原理
- 全局回退管理
學(xué)習(xí)資源
- TarBar導(dǎo)航欄使用
- 輪播組件使用
- vuex模塊化
- vue動畫設(shè)計(jì)
- 動態(tài)全局組件創(chuàng)建
- vue渲染函數(shù)原理
- vue的$mount函數(shù)
輪播圖和商品列表
- 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)的動力
求贊,求評論,求分享...