理解vue
引用一段官方的原话:
Vue.js(读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。
知乎也有相应问答:Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么?,而我的理解就是:vue提供了最核心的的部分,开发者则可以通过“增量”(即增加其它方案)进行开发,往里面叠加各种插件与组件,来完成功能需求。如果单纯地使用vue也能达到想要的功能需求,但可能会让代码编写起来会异常的混乱,就不能体现vue的“轻”了。但如果接入其它方案,一则代码简单易读,二则方便维护,三则容易叠加式地接入更多方案。这就是vue “渐进式框架” 的真谛之所在。
vue环境配置
vue官网给初学者的建议是以标签的方式引入vue.js库,直接上手使用。不过作为一个es2015的狂热爱好者,Coding当然得使用新标准了。所以,第一步,开发环境必须得先搭一个。而vue的开发环境搭建也几个命令的事。
我有两种方法推荐:
1、官网推荐vue-cli
npm i vue-cli -g
安装完vue-cli,则可以直接初始化项目。
vue init webpack vue-demo
安装依赖包
npm i
运行开发模式
npm run dev
对于我来说。我更不愿意花时间在学习搭建环境之上。所以我使用cooking进行搭建开发环境。
2、使用cooking搭建
安装cooking-cli
npm i cooking-cli -g
安装vue脚手架,也可以安装react,static之类的脚手架
npm i slush-cooking-vue -g
在用户目录~/.cooking
目录上安装脚手架依赖(gulp相关包,注意此步骤不能少)
npm i -D gulp-install gulp gulp-rename gulp-template gulp-conflict
导入脚手架
cooking import vue -t
初始化项目(以上的步骤在之后的项目初始化则不需要重新做,则以后要初始化项目,直接使用下面的步骤)。
cooking create vue-demo vue
初始化过程,可以根据自己的需要配置相应的参数。因为cooking实际使用的是webpack的配置,只不过简化了配置项与配置过程。在学习与开发阶段,完全可以无视这些过多的配置。
初始化项目的目录结构一般如下:
vue-demo
|-- src
|-- assets
|-- components
|-- pages
|-- app.vue
|-- main.js
|-- static
|-- .babelrc
|-- .eslintrc
|-- packege.json
|-- cooking.conf.js
|-- Makefile
我以一个最简单的计数器需求进行我的介绍。
需求: 定义一个小组件,有加、减两个按扭,加+1,减-1,一个显示结果的标签
vue组件化
在vue2.0时代,代码的构建方式,分为独立构建与运行构建两种方式。
1、独立构建:独立构建包含模板编译器并支持 template 选项。 它也依赖于浏览器的接口的存在,所以你不能使用它来为服务器端渲染。
2、运行时构建:运行时构建不包含模板编译器,因此不支持 template 选项,只能用 render 选项,但即使使用运行时构建,在单文件组件中也依然可以写模板,因为单文件组件的模板会在构建时预编译为 render 函数。运行时构建比独立构建要轻量30%,只有 17.14 Kb min+gzip大小。
实现差别只在于
new Vue()
的时候,独立构建可以挂载到el属性上,模板依赖于dom,而运行时构建则通过render函数,将单文件组件作为render的回调函数的参数。
运行时构建可以在服务端渲染,并且压缩的体积更小,则优势更明显,以下编码皆使用运行时构建方式来构建我的应用。
独立功能组件
把Counter组件自身的所有功能都写在一个单独文件里面,实现单文件组件,相当于组件自给自足,处理相应的方法与逻辑。
// ./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组件,此组件不需要传入任何参数与方法。
// ./app.vue
<template>
<counter></counter>
</template>
<script>
import Counter from './components/Counter'
export default {
name: 'app',
components: {
Counter
}
};
</script>
在入口函数main.js文件中,引入所有组件,执行render函数。
import Vue from 'vue';
import App from './app';
new Vue({
el: '#app',
render: h => h(App)
});
需求完成。
可复用组件
处于可维护性与扩展性上来说,组件不能写死,组件的初始化数据往往来自后端传过来的数据。所以对于组件,一般将其静态化。即,初始值及数据的变更逻辑都由父组件决定。
将组件静态部分抽出来:
// ./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分发到父组件,由父组件处理数据状态的变更逻辑。
// ./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层的组件不需要处理状态的管理,只负责UI渲染即可,将揉合在起的数据结构从功能层面上分离开来,达到了基本的显示与逻辑分离。传到子组件的数据,需要全部属性都得传入,会增加很多的样板代码。触发事件$emit会将对应的方法传到父组件,让父组件来处理相应的事件方法。不过,如果子组件触发的事件逻辑非常之多,并且都带着负载对象参数什么的话,我们的组件逻辑就会写得非常庞大,并且难以维护。
vuex引入
基于状态管理的问题,我们使用vue很难去维护。官方推荐使用vuex进行管理。我们可以通过vuex提供的store进行管理我们的数据。而vue只负责UI渲染的问题。数据的状态变更,由vuex来接管。
我们这样大体上将vue与vuex作一个对比,可以理解这样的一个概念,state对应的是data数据结构,而mutations对应的是methods方法集。
store、state
先定义一个store“数据库”,将其挂载到app根节点上。可以达到管控整个应用的所有数据层。
// ./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)
});
实际上Vuex.Store()方法接收的是一个对象作为参数。而对象包含state,mutations,actions,getters四个属性。而我们使用state来初始化count的状态值。
我们的静态组件不需要调整,只需要将父组件中的状态处理逻辑交由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>
如此达到了数据层与UI层的分离,我们的业务处理则可以写到父给件上,由$store对象处理我们的状态。不过,这种写法并不是很友好,也是多此一举的做法。可以将事件逻辑给另一个参数mutations来接管
mutations
state是初始化状态值,而状态的变化管理方法,可以交由mutations来处理。
// ./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)
});
而对应的组件,则可以直接在传入参数上添加对应的处理方法,皆由$store对象提供。
// ./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>
不过vuex还提供了更为便捷的方式让我们更方便处理这样的数据接管。那就是vuex提供的辅助函数。
mapState、mapMutations
mapState、mapMutations就相当于连接vuex状态属性与vue组件之间的工具函数。他们将对应的属性与方法分别映射到相应组件上去,使组件更专注于处理UI,而vuex更专注于管理状态,相互之间的联系,完全交由他们去转化。
// ./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赋值,因为mapState返回的是一个函数集,如果赋值给data,则会报错。
到目前为此,功能已经实现了UI、状态分管。也可以很好的维护了。有时候我们需要从 store 中的 state 中派生出一些状态,例如:
现在需要新增一个需求:添加一个显示当前计数是“奇数”还是“偶数”的提示。
getters、mapGetters
因为显示当前计数是否“奇偶“,这个状态值,并非存在于原始state里面,而是由store派生出来的。
// ./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对应的映射关系。
// ./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>
绝大多时,我们的应用其实都不是居于同步的基础上的。还有很多是异步的应用。也就是我们与服务端进行交互。而我们使用vuex能处理的状态,目前为此,都是在同步基础上执行的。为了解决异步问题。我们需要使用到actions进行处理异步逻辑。
上面的逻辑都是基于同步的操作。假设又新增了需求:新增一个异步点击按扭“async+”,当点击时,等1秒后执行。
actions、mapActions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation。实践中,我们会经常会用到 ES2015 的 参数解构 来简化代码。
// ./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--
}
}
})
...
我们在异步处理函数中,返回的是一个Promise,在Promise里面执行increment的提交。
实际上,我们还有更方便的写法,那就是async/await写异步函数,那样可以写出干净整洁的代码。
...
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的参数只能是Promise对象。
// ./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>
有时,当前,我们所有的状态管理逻辑都是写在main.js里面。但在大型应用开发的过程中,往往都将store集中写到一个文件夹store里面。
store文件夹
store
|-- index.js
|-- modules
|-- counter.js
|-- getters.js
|-- actions.js
|-- mutation-types.js
将其拆开为各个文件的目的就是可以让状态管理更具体。
// ./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
}
})
事实上,我们已经将所有状态管理逻辑都按照state,getters,actions,mutations,modules的模式构建。而modules里面的每一个子模块,也有自己单独的state,getters,actions,mutations。他们是在自己独立的命名空间内工作,与外不影响。如果要取得父结构的状态,则可以通过参数rootState取得。
如此,在大型应用的开发中,可以很方便找到对应的模块,处理单独子模块内的状态管理。关于vuex状态管理更多的内容,可以查看文档
这时,入口文件main.js可以以固定格式书写
import Vue from 'vue';
import store from './store'
import App from './app';
new Vue({
el: '#app',
store,
render: h => h(App)
});
总结
从Counter计数器的实现过程,我一直在变换着写法。实际上我的目的就是把vue从单纯的组件把功能与数据揉合捆绑,一步一步地进行拆解,并且给他装上vuex方案,以达到组件+状态分离。实现了UI与数据的完美组合。
而vue与vuex的对应关系,我理解成这样的结构:
// 整体就是一个应用,则整个应用的数据状态都归store管控
UI —— Store //渲染层UI组件与store对象之间是存在着一种形象的“树型结构”
data —— state
computed —— actions、getters
methods —— mutations
components —— modules
通过如此的对应关系,就可以很方便地将应用的状态逻辑状态交由store去处理。如此,vue负责UI渲染,而vuex负责数据变更,达到完美组合。