理解vue
引用一段官方的原話:
Vue.js(讀音 /vju?/,類似于 view) 是一套構(gòu)建用戶界面的漸進(jìn)式框架。與其他重量級(jí)框架不同的是,Vue 采用自底向上增量開(kāi)發(fā)的設(shè)計(jì)。Vue 的核心庫(kù)只關(guān)注視圖層凤覆,它不僅易于上手,還便于與第三方庫(kù)或既有項(xiàng)目整合姆怪。另一方面叛赚,當(dāng)與單文件組件和 Vue 生態(tài)系統(tǒng)支持的庫(kù)結(jié)合使用時(shí)澡绩,Vue 也完全能夠?yàn)閺?fù)雜的單頁(yè)應(yīng)用程序提供驅(qū)動(dòng)稽揭。
知乎也有相應(yīng)問(wèn)答:Vue2.0 中,“漸進(jìn)式框架”和“自底向上增量開(kāi)發(fā)的設(shè)計(jì)”這兩個(gè)概念是什么肥卡?溪掀,而我的理解就是:vue提供了最核心的的部分,開(kāi)發(fā)者則可以通過(guò)“增量”(即增加其它方案)進(jìn)行開(kāi)發(fā)步鉴,往里面疊加各種插件與組件揪胃,來(lái)完成功能需求。如果單純地使用vue也能達(dá)到想要的功能需求氛琢,但可能會(huì)讓代碼編寫起來(lái)會(huì)異常的混亂喊递,就不能體現(xiàn)vue的“輕”了。但如果接入其它方案阳似,一則代碼簡(jiǎn)單易讀骚勘,二則方便維護(hù),三則容易疊加式地接入更多方案。這就是vue “漸進(jìn)式框架” 的真諦之所在俏讹。
vue環(huán)境配置
vue官網(wǎng)給初學(xué)者的建議是以標(biāo)簽的方式引入vue.js庫(kù)当宴,直接上手使用。不過(guò)作為一個(gè)es2015的狂熱愛(ài)好者泽疆,Coding當(dāng)然得使用新標(biāo)準(zhǔn)了户矢。所以,第一步殉疼,開(kāi)發(fā)環(huán)境必須得先搭一個(gè)梯浪。而vue的開(kāi)發(fā)環(huán)境搭建也幾個(gè)命令的事。
我有兩種方法推薦:
1瓢娜、官網(wǎng)推薦vue-cli
npm i vue-cli -g
安裝完vue-cli驱证,則可以直接初始化項(xiàng)目。
vue init webpack vue-demo
安裝依賴包
npm i
運(yùn)行開(kāi)發(fā)模式
npm run dev
對(duì)于我來(lái)說(shuō)恋腕。我更不愿意花時(shí)間在學(xué)習(xí)搭建環(huán)境之上抹锄。所以我使用cooking進(jìn)行搭建開(kāi)發(fā)環(huán)境。
2荠藤、使用cooking搭建
安裝cooking-cli
npm i cooking-cli -g
安裝vue腳手架伙单,也可以安裝react,static之類的腳手架
npm i slush-cooking-vue -g
在用戶目錄~/.cooking
目錄上安裝腳手架依賴(gulp相關(guān)包,注意此步驟不能少)
npm i -D gulp-install gulp gulp-rename gulp-template gulp-conflict
導(dǎo)入腳手架
cooking import vue -t
初始化項(xiàng)目(以上的步驟在之后的項(xiàng)目初始化則不需要重新做哈肖,則以后要初始化項(xiàng)目吻育,直接使用下面的步驟)。
cooking create vue-demo vue
初始化過(guò)程淤井,可以根據(jù)自己的需要配置相應(yīng)的參數(shù)布疼。因?yàn)閏ooking實(shí)際使用的是webpack的配置,只不過(guò)簡(jiǎn)化了配置項(xiàng)與配置過(guò)程币狠。在學(xué)習(xí)與開(kāi)發(fā)階段游两,完全可以無(wú)視這些過(guò)多的配置。
初始化項(xiàng)目的目錄結(jié)構(gòu)一般如下:
vue-demo
|-- src
|-- assets
|-- components
|-- pages
|-- app.vue
|-- main.js
|-- static
|-- .babelrc
|-- .eslintrc
|-- packege.json
|-- cooking.conf.js
|-- Makefile
我以一個(gè)最簡(jiǎn)單的計(jì)數(shù)器需求進(jìn)行我的介紹漩绵。
需求: 定義一個(gè)小組件贱案,有加、減兩個(gè)按扭止吐,加+1宝踪,減-1,一個(gè)顯示結(jié)果的標(biāo)簽
vue組件化
在vue2.0時(shí)代碍扔,代碼的構(gòu)建方式瘩燥,分為獨(dú)立構(gòu)建與運(yùn)行構(gòu)建兩種方式。
1不同、獨(dú)立構(gòu)建:獨(dú)立構(gòu)建包含模板編譯器并支持 template 選項(xiàng)厉膀。 它也依賴于瀏覽器的接口的存在,所以你不能使用它來(lái)為服務(wù)器端渲染。
2站蝠、運(yùn)行時(shí)構(gòu)建:運(yùn)行時(shí)構(gòu)建不包含模板編譯器汰具,因此不支持 template 選項(xiàng),只能用 render 選項(xiàng)菱魔,但即使使用運(yùn)行時(shí)構(gòu)建留荔,在單文件組件中也依然可以寫模板,因?yàn)閱挝募M件的模板會(huì)在構(gòu)建時(shí)預(yù)編譯為 render 函數(shù)澜倦。運(yùn)行時(shí)構(gòu)建比獨(dú)立構(gòu)建要輕量30%聚蝶,只有 17.14 Kb min+gzip大小。
實(shí)現(xiàn)差別只在于
new Vue()
的時(shí)候藻治,獨(dú)立構(gòu)建可以掛載到el屬性上碘勉,模板依賴于dom,而運(yùn)行時(shí)構(gòu)建則通過(guò)render函數(shù)桩卵,將單文件組件作為render的回調(diào)函數(shù)的參數(shù)验靡。
運(yùn)行時(shí)構(gòu)建可以在服務(wù)端渲染,并且壓縮的體積更小雏节,則優(yōu)勢(shì)更明顯胜嗓,以下編碼皆使用運(yùn)行時(shí)構(gòu)建方式來(lái)構(gòu)建我的應(yīng)用。
獨(dú)立功能組件
把Counter組件自身的所有功能都寫在一個(gè)單獨(dú)文件里面钩乍,實(shí)現(xiàn)單文件組件辞州,相當(dāng)于組件自給自足,處理相應(yīng)的方法與邏輯寥粹。
// ./components/Counter.vue
<template>
<div>
<div>{{ count }}</div>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
export default {
name: 'counter',
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
};
</script>
然后再app.vue引入Counter組件变过,此組件不需要傳入任何參數(shù)與方法。
// ./app.vue
<template>
<counter></counter>
</template>
<script>
import Counter from './components/Counter'
export default {
name: 'app',
components: {
Counter
}
};
</script>
在入口函數(shù)main.js文件中涝涤,引入所有組件媚狰,執(zhí)行render函數(shù)。
import Vue from 'vue';
import App from './app';
new Vue({
el: '#app',
render: h => h(App)
});
需求完成妄痪。
可復(fù)用組件
處于可維護(hù)性與擴(kuò)展性上來(lái)說(shuō)哈雏,組件不能寫死楞件,組件的初始化數(shù)據(jù)往往來(lái)自后端傳過(guò)來(lái)的數(shù)據(jù)衫生。所以對(duì)于組件,一般將其靜態(tài)化土浸。即罪针,初始值及數(shù)據(jù)的變更邏輯都由父組件決定。
將組件靜態(tài)部分抽出來(lái):
// ./components/Counter.vue
<template>
<div>
<div>{{ count }}</div>
<button @click="$emit('increment')">+</button>
<button @click="$emit('decrement')">-</button>
</div>
</template>
<script>
export default {
props: {
count: Number
}
}
</script>
這樣黄伊,我們傳入了不可變更的屬性值props泪酱,然后方法集由$emit分發(fā)到父組件,由父組件處理數(shù)據(jù)狀態(tài)的變更邏輯。
// ./app.vue
<template>
<counter
:count="count"
@increment="increment"
@decrement="decrement">
</counter>
</template>
<script>
import Counter from './Counter'
export default {
name: 'app',
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
}
},
components: {
Counter
}
};
</script>
這樣做的好處可以讓UI層的組件不需要處理狀態(tài)的管理墓阀,只負(fù)責(zé)UI渲染即可毡惜,將揉合在起的數(shù)據(jù)結(jié)構(gòu)從功能層面上分離開(kāi)來(lái),達(dá)到了基本的顯示與邏輯分離斯撮。傳到子組件的數(shù)據(jù)经伙,需要全部屬性都得傳入,會(huì)增加很多的樣板代碼勿锅。觸發(fā)事件$emit會(huì)將對(duì)應(yīng)的方法傳到父組件帕膜,讓父組件來(lái)處理相應(yīng)的事件方法。不過(guò)溢十,如果子組件觸發(fā)的事件邏輯非常之多垮刹,并且都帶著負(fù)載對(duì)象參數(shù)什么的話,我們的組件邏輯就會(huì)寫得非常龐大张弛,并且難以維護(hù)荒典。
vuex引入
基于狀態(tài)管理的問(wèn)題,我們使用vue很難去維護(hù)吞鸭。官方推薦使用vuex進(jìn)行管理种蝶。我們可以通過(guò)vuex提供的store進(jìn)行管理我們的數(shù)據(jù)。而vue只負(fù)責(zé)UI渲染的問(wèn)題瞒大。數(shù)據(jù)的狀態(tài)變更螃征,由vuex來(lái)接管。
我們這樣大體上將vue與vuex作一個(gè)對(duì)比透敌,可以理解這樣的一個(gè)概念盯滚,state對(duì)應(yīng)的是data數(shù)據(jù)結(jié)構(gòu),而mutations對(duì)應(yīng)的是methods方法集酗电。
store魄藕、state
先定義一個(gè)store“數(shù)據(jù)庫(kù)”,將其掛載到app根節(jié)點(diǎn)上撵术”陈剩可以達(dá)到管控整個(gè)應(yīng)用的所有數(shù)據(jù)層。
// ./main.js
import Vue from 'vue';
import Vuex from 'vuex';
import App from './app';
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
}
})
new Vue({
el: '#app',
store,
render: h => h(App)
});
實(shí)際上Vuex.Store()方法接收的是一個(gè)對(duì)象作為參數(shù)嫩与。而對(duì)象包含state,mutations,actions,getters四個(gè)屬性寝姿。而我們使用state來(lái)初始化count的狀態(tài)值。
我們的靜態(tài)組件不需要調(diào)整划滋,只需要將父組件中的狀態(tài)處理邏輯交由store接管即可饵筑。
<template>
<counter
:count="$store.state.count"
@increment="increment"
@decrement="decrement">
</counter>
</template>
<script>
import Counter from './Counter'
export default {
name: 'app',
methods: {
increment() {
this.$store.state.count++
},
decrement() {
this.$store.state.count--
}
},
components: {
Counter
}
};
</script>
如此達(dá)到了數(shù)據(jù)層與UI層的分離,我們的業(yè)務(wù)處理則可以寫到父給件上处坪,由$store對(duì)象處理我們的狀態(tài)根资。不過(guò)架专,這種寫法并不是很友好,也是多此一舉的做法玄帕〔拷牛可以將事件邏輯給另一個(gè)參數(shù)mutations來(lái)接管
mutations
state是初始化狀態(tài)值,而狀態(tài)的變化管理方法裤纹,可以交由mutations來(lái)處理睛低。
// ./main.js
import Vue from 'vue';
import Vuex from 'vuex';
import App from './app';
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
new Vue({
el: '#app',
store,
render: h => h(App)
});
而對(duì)應(yīng)的組件,則可以直接在傳入?yún)?shù)上添加對(duì)應(yīng)的處理方法服傍,皆由$store對(duì)象提供钱雷。
// ./app.vue
<template>
<counter
:count="$store.state.count"
@increment="$store.commit('increment')"
@decrement="$store.commit('decrement')">
</counter>
</template>
<script>
import Counter from './Counter'
export default {
name: 'app',
components: {
Counter
}
};
</script>
不過(guò)vuex還提供了更為便捷的方式讓我們更方便處理這樣的數(shù)據(jù)接管。那就是vuex提供的輔助函數(shù)吹零。
mapState罩抗、mapMutations
mapState、mapMutations就相當(dāng)于連接vuex狀態(tài)屬性與vue組件之間的工具函數(shù)灿椅。他們將對(duì)應(yīng)的屬性與方法分別映射到相應(yīng)組件上去套蒂,使組件更專注于處理UI,而vuex更專注于管理狀態(tài)茫蛹,相互之間的聯(lián)系操刀,完全交由他們?nèi)マD(zhuǎn)化。
// ./app.vue
<template>
<counter
:count="count"
@increment="increment"
@decrement="decrement">
</counter>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Counter from './Counter'
export default {
name: 'app',
computed: mapState({ count: state => state.count }),
methods: mapMutations(['increment','decrement']),
components: {
Counter
}
};
</script>
mapState只能給computed賦值婴洼,因?yàn)閙apState返回的是一個(gè)函數(shù)集骨坑,如果賦值給data,則會(huì)報(bào)錯(cuò)柬采。
到目前為此欢唾,功能已經(jīng)實(shí)現(xiàn)了UI、狀態(tài)分管粉捻。也可以很好的維護(hù)了礁遣。有時(shí)候我們需要從 store 中的 state 中派生出一些狀態(tài),例如:
現(xiàn)在需要新增一個(gè)需求:添加一個(gè)顯示當(dāng)前計(jì)數(shù)是“奇數(shù)”還是“偶數(shù)”的提示肩刃。
getters祟霍、mapGetters
因?yàn)轱@示當(dāng)前計(jì)數(shù)是否“奇偶“,這個(gè)狀態(tài)值盈包,并非存在于原始state里面沸呐,而是由store派生出來(lái)的。
// ./main.js
import Vue from 'vue';
import Vuex from 'vuex';
import App from './app';
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
isEvenOrOdd(state) {
return state.count % 2 === 0 ? 'even' : 'odd'
}
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
new Vue({
el: '#app',
store,
render: h => h(App)
});
同樣续语,添加上mapGetters對(duì)應(yīng)的映射關(guān)系垂谢。
// ./app.vue
<template>
<counter
:count="count"
:isEvenOrOdd="isEvenOrOdd"
@increment="increment"
@decrement="decrement">
</counter>
</template>
<script>
import { mapState, mapMutations, mapGetters } from 'vuex'
import Counter from './Counter'
console.log(mapState({ count: state => state.count }));
export default {
name: 'app',
computed:{
...mapState({ count: state => state.count }),
...mapGetters(['isEvenOrOdd'])
},
methods: mapMutations(['increment','decrement']),
components: {
Counter
}
};
</script>
// ./components/Counter.vue
<template>
<div>
<div>{{ count }},this value is {{ isEvenOrOdd }}</div>
<button @click="$emit('increment')">+</button>
<button @click="$emit('decrement')">-</button>
</div>
</template>
<script>
export default {
props: {
count: Number,
isEvenOrOdd: String
}
}
</script>
絕大多時(shí),我們的應(yīng)用其實(shí)都不是居于同步的基礎(chǔ)上的疮茄。還有很多是異步的應(yīng)用滥朱。也就是我們與服務(wù)端進(jìn)行交互。而我們使用vuex能處理的狀態(tài)力试,目前為此徙邻,都是在同步基礎(chǔ)上執(zhí)行的。為了解決異步問(wèn)題畸裳。我們需要使用到actions進(jìn)行處理異步邏輯缰犁。
上面的邏輯都是基于同步的操作。假設(shè)又新增了需求:新增一個(gè)異步點(diǎn)擊按扭“async+”怖糊,當(dāng)點(diǎn)擊時(shí)帅容,等1秒后執(zhí)行。
actions伍伤、mapActions
Action 類似于 mutation并徘,不同在于:
- Action 提交的是 mutation,而不是直接變更狀態(tài)扰魂。
- Action 可以包含任意異步操作麦乞。
Action 函數(shù)接受一個(gè)與 store 實(shí)例具有相同方法和屬性的 context 對(duì)象,因此你可以調(diào)用 context.commit 提交一個(gè) mutation劝评。實(shí)踐中姐直,我們會(huì)經(jīng)常會(huì)用到 ES2015 的 參數(shù)解構(gòu) 來(lái)簡(jiǎn)化代碼。
// ./main.js
...
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
isEvenOrOdd(state) {
return state.count % 2 === 0 ? 'even' : 'odd'
}
},
actions: {
incrementIfAsync({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment')
resolve()
}, 1000)
});
}
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
...
我們?cè)诋惒教幚砗瘮?shù)中蒋畜,返回的是一個(gè)Promise声畏,在Promise里面執(zhí)行increment的提交。
實(shí)際上姻成,我們還有更方便的寫法砰识,那就是async/await寫異步函數(shù),那樣可以寫出干凈整潔的代碼佣渴。
...
const getData = time => new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, time)
});
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
isEvenOrOdd(state) {
return state.count % 2 === 0 ? 'even' : 'odd'
}
},
actions: {
async incrementIfAsync({ commit }) {
commit('increment', await getData(1000))
}
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
...
這里需要注意一下辫狼,await的參數(shù)只能是Promise對(duì)象。
// ./app.vue
<template>
<counter
:count="count"
:isEvenOrOdd="isEvenOrOdd"
@increment="increment"
@decrement="decrement"
@incrementIfAsync="incrementIfAsync">
</counter>
</template>
<script>
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex'
import Counter from './Counter'
console.log(mapState({ count: state => state.count }));
export default {
name: 'app',
computed:{
...mapState({ count: state => state.count }),
...mapGetters(['isEvenOrOdd'])
},
methods: {
...mapMutations(['increment','decrement']),
...mapActions(['incrementIfAsync'])
},
components: {
Counter
}
};
</script>
// ./components/Counter.vue
<template>
<div>
<div>{{ count }},this value is {{ isEvenOrOdd }}</div>
<button @click="$emit('increment')">+</button>
<button @click="$emit('incrementIfAsync')">async+</button>
<button @click="$emit('decrement')">-</button>
</div>
</template>
<script>
export default {
props: {
count: Number,
isEvenOrOdd: String
}
}
</script>
有時(shí)辛润,當(dāng)前膨处,我們所有的狀態(tài)管理邏輯都是寫在main.js里面。但在大型應(yīng)用開(kāi)發(fā)的過(guò)程中砂竖,往往都將store集中寫到一個(gè)文件夾store里面真椿。
store文件夾
store
|-- index.js
|-- modules
|-- counter.js
|-- getters.js
|-- actions.js
|-- mutation-types.js
將其拆開(kāi)為各個(gè)文件的目的就是可以讓狀態(tài)管理更具體。
// ./store/modules/counter.js
// async(or ajax) function
const getData = time => new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, time)
});
// init state
const state = { count: 0 }
// mutations
const mutations = {
[types.INCREMENT](state) {
state.count++
},
[types.DECREMENT](state) {
state.count--
}
}
// actons
const actions = {
increment: ({ commit }) => commit(types.INCREMENT),
decrement: ({ commit }) => commit(types.DECREMENT),
async incrementIfAsync({ commit }) {
commit(types.INCREMENT, await getData(1000))
}
}
// getters
const getters = {
isEvenOrOdd(state) {
return state.count % 2 === 0 ? 'even' : 'odd'
}
}
export default {
state,
getters,
actions,
mutations
}
然后其它文件如下:
// ./store/getters.js
export {}
// ./store/actions.js
export {}
// ./store/mutations.js
export {}
// ./store/mutation-types.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
// ./store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import * as mutations from './mutations'
import counter from './modules/counter'
Vue.use(Vuex)
export default new Vuex.Store({
actions,
getters,
mutations,
modules: {
counter
}
})
事實(shí)上乎澄,我們已經(jīng)將所有狀態(tài)管理邏輯都按照state,getters,actions,mutations,modules的模式構(gòu)建突硝。而modules里面的每一個(gè)子模塊,也有自己?jiǎn)为?dú)的state,getters,actions,mutations置济。他們是在自己獨(dú)立的命名空間內(nèi)工作解恰,與外不影響锋八。如果要取得父結(jié)構(gòu)的狀態(tài),則可以通過(guò)參數(shù)rootState取得护盈。
如此挟纱,在大型應(yīng)用的開(kāi)發(fā)中,可以很方便找到對(duì)應(yīng)的模塊腐宋,處理單獨(dú)子模塊內(nèi)的狀態(tài)管理紊服。關(guān)于vuex狀態(tài)管理更多的內(nèi)容,可以查看文檔
這時(shí)胸竞,入口文件main.js可以以固定格式書寫
import Vue from 'vue';
import store from './store'
import App from './app';
new Vue({
el: '#app',
store,
render: h => h(App)
});
總結(jié)
從Counter計(jì)數(shù)器的實(shí)現(xiàn)過(guò)程欺嗤,我一直在變換著寫法。實(shí)際上我的目的就是把vue從單純的組件把功能與數(shù)據(jù)揉合捆綁卫枝,一步一步地進(jìn)行拆解煎饼,并且給他裝上vuex方案,以達(dá)到組件+狀態(tài)分離剃盾。實(shí)現(xiàn)了UI與數(shù)據(jù)的完美組合腺占。
而vue與vuex的對(duì)應(yīng)關(guān)系,我理解成這樣的結(jié)構(gòu):
// 整體就是一個(gè)應(yīng)用痒谴,則整個(gè)應(yīng)用的數(shù)據(jù)狀態(tài)都?xì)wstore管控
UI —— Store //渲染層UI組件與store對(duì)象之間是存在著一種形象的“樹(shù)型結(jié)構(gòu)”
data —— state
computed —— actions衰伯、getters
methods —— mutations
components —— modules
通過(guò)如此的對(duì)應(yīng)關(guān)系,就可以很方便地將應(yīng)用的狀態(tài)邏輯狀態(tài)交由store去處理积蔚。如此意鲸,vue負(fù)責(zé)UI渲染,而vuex負(fù)責(zé)數(shù)據(jù)變更尽爆,達(dá)到完美組合怎顾。