一個例子入門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 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。webpack中文網
說完技術棧的介紹,我們說下我們的專案的功能,我們要做的是一個有兩個頁面的購物車專案,一個頁面是產品頁面,一個頁面是購物車頁面。產品頁面我們可以看到產品的資訊,然後可以新增產品到購物車;購物車頁面則對產品的總額進行結算,最後點選結賬,成功則清空購物車。
demo最後的效果如下:
很簡單的功能,我們來看下具體實現的步驟和細節,關鍵是如何進行模組化。
專案結構如下:
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的樣式,比如本例中修改了產品,購物車處於啟用狀態的背景色為金色。