1. 程式人生 > >一個例子入門Vue2.X+vue-router+Vuex+Webpack單頁面應用程式

一個例子入門Vue2.X+vue-router+Vuex+Webpack單頁面應用程式

本篇博文講解如何使用Vue2.X+vue-router+VueX+Webpack實現一個模組化的單頁面應用程式,新手向。

1.功能實現

使用Vue2.X的理由是它屬於輕量級的JS庫,對於流量敏感的移動端來說更友好;容易上手,具有完備的中文文件,學習曲線較平緩;日前新興起的與傳統APP形態不同的快應用,其前端技術棧使用的就是Vue的語法。Vue.js官方文件

vue-router的作用就是將Vue元件(components)對映到路由(routes),然後告訴 vue-router 在哪裡渲染它們。結合vue使用,建立單頁面應用程式十分簡單。Vue-Router官方文件

Vuex是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

VueX官方文件

webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。webpack中文網

說完技術棧的介紹,我們說下我們的專案的功能,我們要做的是一個有兩個頁面的購物車專案,一個頁面是產品頁面,一個頁面是購物車頁面。產品頁面我們可以看到產品的資訊,然後可以新增產品到購物車;購物車頁面則對產品的總額進行結算,最後點選結賬,成功則清空購物車。
demo最後的效果如下:

https://lucyzlu.github.io/shopping_demo/index.html#/
很簡單的功能,我們來看下具體實現的步驟和細節,關鍵是如何進行模組化。
專案結構如下:
這裡寫圖片描述

index.html是最後輸出的頁面,包含了打包的js,css等檔案,而main.js是我們的入口檔案(webpack建立依賴圖的開始模組):這是在webpack.config.js檔案中指定的entry

module.exports = {
  entry: './main.js',
  ...

main.js程式碼如下:

main.js

import Vue from "vue"
import VueRouter from
"vue-router" import routes from "./router" import app from "./App.vue" import store from "./src/store" Vue.use(VueRouter) const router = new VueRouter({ routes // (縮寫)相當於 routes: routes }) //根例項 new Vue({ el:"#app", store,//將狀態從根元件“注入”到每一個子元件中, router,//通過 router 配置引數注入路由, 從而讓整個應用都有路由功能 render:h=>h(app)//渲染App.vue到index.html中id為app的結點 })

建立根例項,掛載例項到DOM上,將狀態從元件注入到每一個子元件中,通過 router 配置引數注入路由,從而讓整個應用都有路由功能,最後render函式返回了一個編譯後的模板。
我們看到main.js依賴了store模組(狀態管理),router模組(路由功能),App模組(元件)。下面會一一講解其內容。

這裡h是createElement的簡寫形式,createElement函式接收的引數如下:

這裡寫圖片描述

createElement的引數是App.vue元件選項物件,App.vue是我們接觸的第一個單檔案元件,它由模板(template標籤)、邏輯(script)和樣式(style)組成。
我們來看一下這個匯出的物件是什麼,App.vue的程式碼如下:

App.vue

<template>
    <div id="mainpage">
        <ul>
            <router-link to="/">產品</router-link>
            <router-link to="/cart">購物車</router-link>
        </ul>
        <router-view></router-view>
    </div>
</template>

<style>
    html,
    body {
        background: burlywood;
        padding: 0;
        margin: 0;
        font: 25px 'Courier New', Courier, monospace;
    }

    ul li {
        display: inline-block;
        width: 45%;
        box-sizing: border-box;
        text-align: center;
        margin: 5px auto;
    }

    button {
        cursor: pointer;
        border-radius: 15%;
    }

    a {
        display:inline-block;
        text-decoration: none;
        width:45%;
        text-align:center;
    }

    .router-link-exact-active {
        background: gold;
    }
</style>

App.vue中定義了router-link和router-view標籤,這是vue-router中的內容:使用 router-link 元件(最後會被編譯為a連結標籤)來導航,通過傳入 to 屬性指定連結,通過router-view指定路由匹配到的元件將渲染在這裡。我們來看router.js中怎麼定義router-link的指向的:

router.js

import Vue from "vue"
import VueRouter from "vue-router"
import ProductLists from "./src/pages/ProductLists.vue"
import ShoppingCart from "./src/pages/ShoppingCart.vue"

export default [
    {path:"/",component:ProductLists},
    {path:"/cart",component:ShoppingCart}
]

匯出一個數組,第一個物件path:”/”指向預設的根路徑,指向ProductLists即產品元件,第二個物件path:”/cart”指向ShoppingCart即購物車元件。
我們來看下兩個元件的定義:
首先是Products單檔案元件,這裡面定義了產品的列表,顯示每個產品的名字,價格以及一個新增到購物車的按鈕,程式碼:

ProductLists.vue

<template>
    <ul class="product">
        <li v-for="product in products">
            <div class="container">
                <img :src="imagePath+product.src">
            </div>
            {{product.title}}-{{product.price}}
            <br>
            <button type="button" @click="addProductToCart(product)">
                新增到購物車
            </button>
        </li>
    </ul>
</template>
<script>
    import { mapGetters, mapActions } from "vuex";
    export default {
        data: function () {
            return {
                imagePath: "./src/images/"
            }
        },
        computed: mapGetters({
            products: "allProducts" //將store 中的 getters.allProducts 對映到區域性計算屬性products
        }),
        methods: mapActions(["addProductToCart"]),
        created() {
            this.$store.dispatch('getAllProducts')//在元件中使用 this.$store.dispatch('xxx') 分發 action
        }
    };
</script>

<style scoped>
    button {
        width: 4rem;
        height: 1rem;
        background-color: aquamarine;

    }

    .product li {
        display: block;
        text-align: center;
    }

    .container {
        text-align: center;
        vertical-align: middle;
    }

    img {
        width: 200px;
        height: 200px;
    }
</style>

這裡我們用到了vuex狀態管理中的store例項,我們應該先看看vuex的核心store例項裡都有什麼,下圖是vuex的資料流示意圖
這裡寫圖片描述
store例項包括了state,mutations,actions,以及getters,modules五個屬性。分別對應響應式的應用層級狀態,狀態更改事件(有一個字串的事件型別和回撥函式,必須是同步函式),類似mutation的action,其並不直接更改state,而是通過提交mutation更改狀態,並且回撥函式內可以包含非同步操作。getters是一系列state的計算屬性,modules是store的模組子物件。vuex核心概念由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,store 物件就有可能變得相當臃腫。為了解決以上問題,Vuex 允許我們將 store 分割成模組(module)。每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組——從上至下進行同樣方式的分割:
這裡寫圖片描述

因此在我們的應用中,store的結構可以是這樣的:包含一個index.js主體檔案進行匯入匯出,以及cart和products兩個模組,分別處理購物車的狀態和產品的狀態。
我們先看下index.js主體檔案

/store/index.js

import Vue from "vue"
import Vuex from "vuex"
import products from "./modules/products"
import cart from "./modules/cart.js"
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
    modules: {
        products,
        cart
    },
    strict: debug
});

modules我們知道了,是store的模組,strict屬性表示的是在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤。這能保證所有的狀態變更都能被除錯工具跟蹤到。但是不要在釋出環境下啟用嚴格模式!嚴格模式會深度監測狀態樹來檢測不合規的狀態變更——請確保在釋出環境下關閉嚴格模式,以避免效能損失。類似於外掛,我們可以讓構建工具來處理這種情況,檢測當前環境是否是釋出環境process.env.NODE_ENV !== 'production',如果不是,才啟動嚴格模式。

下面我們來看下兩個store模組的內容。首先是產品的內容product.js,(shop是個api,有一個產品資訊列表,以及getProducts和buyProducts方法,分別是獲取產品列表和結賬操作,模擬了伺服器端,我們最後再講):

/store/modules/product.js

import shop from "../../api/shop"

const state = {
    all: []
}

const getters = {
    allProducts: state => state.all
}

const actions = {
    getAllProducts({ commit }) {
        shop.getProducts(products => {
            commit("setProducts", products);
        });
    }
}

const mutations = {
    setProducts(state, products) {
        state.all = products;
    },

    decrementProductInventory(state, { id }) {
        const product = state.all.find(product => product.id === id)
        product.inventory--
    }
}

export default {
    state,
    getters,
    actions,
    mutations
}

products有一個儲存所有產品資訊的區域性狀態all陣列,它是通過觸發getAllProducts這個action,來從名為shop的api中獲取資料的,獲取到的資料通過回撥函式儲存到products引數中,再通過提交setProducts這個mutation來設定狀態all為products。
action 函式的引數:action函式接受一個與 store 例項具有相同方法和屬性的 context 物件,因此你可以呼叫 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。

下面看下購物車狀態模組,這個模組稍微複雜一點

/store/modules/cart.js

import shop from '../../api/shop'

// initial state
// shape: [{ id, quantity }]
const state = {
  added: [],
  checkoutStatus: null
}

// getters
const getters = {
  checkoutStatus: state => state.checkoutStatus,

  cartProducts: (state, getters, rootState) => {
    return state.added.map(({ id, quantity }) => {
      const product = rootState.products.all.find(product => product.id === id)
      return {
        title: product.title,
        price: product.price,
        quantity
      }
    })
  },

  cartTotalPrice: (state, getters) => {
    return getters.cartProducts.reduce((total, product) =>
      total + product.price * product.quantity,0)//第二個引數必須指定,因為cartProducts最開始是空陣列
  }
}

// actions
const actions = {
  checkout ({ commit, state }, products) {
    const savedCartItems = [...state.added]
    commit('setCheckoutStatus', null)
    // empty cart
    commit('setCartItems', { items: [] })
    shop.buyProducts(
      products,
      () => commit('setCheckoutStatus', 'successful'),
      () => {
        commit('setCheckoutStatus', 'failed')
        // rollback to the cart saved before sending the request
        commit('setCartItems', { items: savedCartItems })
      }
    )
  },

  addProductToCart ({ state, commit }, product) {
    commit('setCheckoutStatus', null)
    if (product.inventory > 0) {
      const cartItem = state.added.find(item => item.id === product.id)
      if (!cartItem) {
        commit('pushProductToCart', { id: product.id })
      } else {
        commit('incrementItemQuantity', cartItem)
      }
      // remove 1 item from stock
      commit('decrementProductInventory', { id: product.id })
      alert("成功新增商品到購物車!");
    }
  }
}

// mutations
const mutations = {
  pushProductToCart (state, { id }) {
    state.added.push({
      id,
      quantity: 1
    })
  },

  incrementItemQuantity (state, { id }) {
    const cartItem = state.added.find(item => item.id === id)
    cartItem.quantity++
  },

  setCartItems (state, { items }) {
    state.added = items
  },

  setCheckoutStatus (state, status) {
    state.checkoutStatus = status
  }
}

export default {
  state,
  getters,
  actions,
  mutations
}

cart模組定義了兩個區域性狀態物件,一個是added表示新增到購物車的產品,一個是結賬狀態checkoutstatus,
三個getters,(因為是在模組內部,getters接收的三個引數,第一個引數state是模組的區域性狀態物件,第二個引數getters是當前模組的getters,第三個引數rootState是store根例項的狀態,是全域性狀態),第一個getter,checkoutStatus依賴於state中的checkoutStatus,第二個gettere,cartProducts返回購物車中的產品名字,單價和數量,第三個getter,cartTotalPrice遍歷cartProducts並根據產品單價和數量返回總價。
定義了兩個action(對於模組內部的 action,區域性狀態通過 context.state 暴露出來,根節點狀態則為 context.rootState),一個是checkout進行結賬操作,另一個addProductToCart新增產品到購物車,如果購物車(added)沒有該物品,提交pushProductToCart新增物品,如果購物車有該物品,則提交incrementItemQuantity增加購物車中該物品的數量;最後提交decrementProductInventory減少物品庫存。
定義了四個mutations,pushProductToCart向added陣列中新增產品物件,包括產品的id和數量,incrementItemQuantity增加added中對應產品的數量,setCartItems和setCheckoutStatus分別設定added和checkoutStatus(記住更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation
最後匯出該store模組。

那麼我們來看一下有了store狀態管理以後我們怎麼定義product元件和cart元件吧!

首先是Products元件,

/pages/ProductLists.vue

<template>
    <ul class="product">
        <li v-for="product in products">
            <div class="container">
                <img :src="imagePath+product.src">
            </div>
            {{product.title}}-{{product.price}}
            <br>
            <button type="button" @click="addProductToCart(product)">
                新增到購物車
            </button>
        </li>
    </ul>
</template>
<script>
    import { mapGetters, mapActions } from "vuex";
    export default {
        data: function () {
            return {
                imagePath: "./src/images/"
            }
        },
        computed: mapGetters({
            products: "allProducts" //將store 中的 getters.allProducts 對映到區域性計算屬性products
        }),
        methods: mapActions(["addProductToCart"]),//addProductToCart是store模組cart.js裡的action
        created() {
            this.$store.dispatch('getAllProducts')//getAllProducts是store模組product.js裡的,在元件中使用 this.$store.dispatch('xxx') 分發 action
        }
    };
</script>

<style scoped>
    button {
        width: 4rem;
        height: 1rem;
        background-color: aquamarine;

    }

    .product li {
        display: block;
        text-align: center;
    }

    .container {
        text-align: center;
        vertical-align: middle;
    }

    img {
        width: 200px;
        height: 200px;
    }
</style>

這裡我們使用了Vuex中的store例項的mapGetters輔助函式和mapActions輔助函式,mapGetters 輔助函式僅僅是將 store 中的 getter 對映到區域性計算屬性,mapActions 輔助函式將元件的 methods 對映為 store.dispatch 呼叫(就是提交store中的mutation,更改store的狀態)。Vuex 使用單一狀態樹,因此每個應用僅僅包含一個store例項,但是單狀態樹和模組化並不衝突,在Products模組中我們就使用了store中的getteres和actions(通過mapGetters和mapActions)。
這裡計算屬性products也可以不使用mapGetters輔助函式,使用this.$store.getters訪問store例項的getters,可以直接寫成

computed: {
            products:function(){return this.$store.getters.allProducts}
        },

再來看下另一個元件shoppingcart元件:

/pages/ShoppingCart.vue

<template>
    <div class="cart">
        <ul>
            <li v-for="product in products">
                {{product.title}}-{{product.price}} x <span class="number">{{product.quantity}}</span>
            </li>
        </ul>
        <p>總額:{{total}}</p>
        <p>
            <button :disabled="!products.length" @click="checkout(products)">結賬</button>
        </p>
        <p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
    </div>
</template>
<script>
    import { mapGetters } from "vuex"

    export default {
        computed: {
            ...mapGetters({
                products: 'cartProducts',
                checkoutStatus: 'checkoutStatus',
                total: 'cartTotalPrice'
            })
            //可以加入其他區域性computed屬性,所以上面mapGetters要使用展開運算子
        },
        methods: {
            checkout(products) {
                this.$store.dispatch('checkout', products)
            }
        }
    }
</script>

<style scoped>
    .cart{
        margin:0 auto;
        text-align:center;
    }

    .cart ul li{
        display:block;
        text-align:right;
        background:skyblue;
    }

    span.number{
        color:crimson;
    } 

    button{
        background-color:chartreuse;
        width:4rem;
        height:1rem;
    }
</style>

shoppingcart元件,顯示購買的產品資訊,每一條包括產品的名字,單價和數量,最後顯示一個計算屬性。

最後定義了一個api,定義了產品列表,返回產品資訊和進行結賬操作:

/api/shop.js

/**
 * Mocking client-server processing
 */
const _products = [
    {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2,"src":"ipad.jpg"},
    {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10,"src":"t-shirt.jpg"},
    {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5,"src":"charli.jpg"}
  ]

  export default {
    getProducts (cb) {
      setTimeout(() => cb(_products), 100)
    },

    buyProducts (products, cb, errorCb) {
      setTimeout(() => {
        // simulate random checkout failure.
        (Math.random() > 0.5 || navigator.userAgent.indexOf('PhantomJS') > -1)
          ? cb()
          : errorCb()
      }, 100)
    }
  }

到此所有檔案都寫完啦,可以執行npm run dev看下效果!

2.問題總結

  • 如果介面沒有顯示,也沒有報錯,看看是不是變數名寫錯了

這裡寫圖片描述

引入的Vuex的store變數名不對是不會有錯誤提示的,但是這個例項其實是沒有掛載到DOM上面的。

  • vscode外掛安裝

讓vscode格式化vue檔案中的template:安裝vetur外掛,檔案->首選項->設定,設定使用者設定.json檔案,新增一條:"vetur.format.defaultFormatter.html": "js-beautify-html",這樣右鍵選單格式化vue檔案就可以格式化template了
這裡寫圖片描述

  • 讓vue中template程式碼自動補全

    檔案->首選項->設定,在使用者設定裡新增如下程式碼,意思是讓vue檔案使用html程式碼補全功能

"files.associations": {"*.vue":"html"}
  • TypeError: Reduce of empty array with no initial value
    reduce函式可選的第二個引數作為total的初始值,如果省略該引數,陣列的第一個值將作為total的初始值;如果陣列是空陣列,一定要新增第二個引數,否則會報上面的引數型別錯誤,因為它訪問了空陣列不存在的第一個元素。
<style scoped>
button {
width: 4rem;
height: 1rem;
background-color: aquamarine;
border-radius: 20%;
cursor: pointer;
}

.product li {
display: block;
}
</style>

通過為style標籤新增scoped屬性,你的Style就只會應用到當前的元件了,不會影響其他元件。
具體怎麼做到的呢?就是用PostCSS為元件中的相應CSS選擇器選中的元素新增自定義屬性(data-v-XX),然後為作用元素的樣式新增屬性選擇器。

這裡寫圖片描述
- router-link-exact-active的樣式

router-link會被編譯為a標籤,並帶有class,
.router-link-exact-active表示當前正處於啟用狀態的(該連結被點選了以後,且只有一個router-link處於該狀態,不同於visited)要修改其狀態,可以設定.router-link-exact-active的樣式,比如本例中修改了產品,購物車處於啟用狀態的背景色為金色。