Vue+Vuex一步一步入坑指南

理解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负责数据变更,达到完美组合。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容