基于 Laravel + Vue 构建 API 驱动的前后端分离应用系列(九) —— 构建 Vuex 模块

上一篇教程中,我们在 resources/assets/js/api/cafe.js 文件中通过 JavaScript 的 Axios 库构建了一些调用 Laravel 后端 API 路由的方法。在这一篇教程中我们需要将从 API 接口获取的数据保存下来以便在单页面应用中使用,而这正是 Vuex 模块可以大展拳脚的地方。

Vuex 文档中将 Vuex 定位成专为 Vue.js 应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。听上去有点抽象,翻译过来就是可以在多个组件和页面中使用的单点数据。为什么我们需要这个?因为随着构建的应用越来越大,单页面也会变得越来越复杂,在多个地方使用数据会非常麻烦。例如,假设你有一份登录到应用的用户数据,有了 Vuex 模块之后,你无需通过用户名将用户作为参数传递到不同的组件,只需将其保存到 Vuex 模块中,然后在任何地方都可以访问这个模块来获取数据。

Vuex 在跟踪应用程序中的数据状态方面也非常有用。如果你使用了 Vue 开发工具,就可以查看每个模块包含的数据以及如何访问它。

Vuex 在一开始有点难以理解,因为这是一种全新的保存数据的方式,好在 Vuex 官方文档 非常详尽,你可以通过它们深入学习。不是所有的应用都需要 Vuex 来存储数据,但是如果你在构建一个大型的单页面应用,并且需要以更优雅地方式来处理数据,那么 Vuex 是一个不错的选择。

第一步:配置 resources/assets/js/store.js

我们之前已经创建过一个初始化的 store.js 文件,现在需要打开这个文件,并添加一些代码来实现一个初始化的数据存储器。

首先,需要在 store.js 顶部导入 Vue 和 Vuex:

/**
 * Import Vue and Vuex
 */
import Vue from 'vue'
import Vuex from 'vuex'

接下来,要告知 Vue 使用 Vuex 作为数据存储器,这将会扩展 Vue 实例具备使用 Vuex 数据存储器所需要的方法。将下面这行代码放到导入 Vue 和 Vuex 之后的地方:

/**
 * Initializes Vuex on Vue.
 */
Vue.use( Vuex )

最后,我们将会从 store.js 文件导出一个新的 Vuex 数据存储器。这样我们就可以将其应用到 Vue 实例并让所有模块在各个组件和路由中都可以访问。将下面这段代码放到 store.js 末尾:

/**
 * Export the data store.
 */
export default new Vuex.Store({
    modules: {

    }
});

这样我们就有了一个最基本的数据存储器配置,我们可以在其基础上轻松实现模块扩展。

第二步:安装 es6-promise 支持 IE 数据存储

上述配置在 IE 11 下不能正常工作,因为 IE 11 不支持 promise,需要通过 NPM 安装 es6-promise

npm install es6-promise --save-dev

然后在 resources/assets/js/store.js 文件顶部加入如下这段代码:

/**
 * Adds the promise polyfill for IE 11
 */
require('es6-promise').polyfill();

最终版本的 store.js 文件内容如下:

/*
 |-------------------------------------------------------------------------------
 | VUEX store.js
 |-------------------------------------------------------------------------------
 | Builds the data store from all of the modules for the Roast app.
 */

/**
 * Adds the promise polyfill for IE 11
 */
require('es6-promise').polyfill();

/**
 * Import Vue and Vuex
 */
import Vue from 'vue'
import Vuex from 'vuex'

/**
 * Initializes Vuex on Vue.
 */
Vue.use( Vuex )

/**
 * Export our data store.
 */
export default new Vuex.Store({
    modules: {

    }
});

第三步:新增数据存储器到 Vue 实例

现在数据存储器已经构建好了,需要将其添加到 Vue 中,Vue 实例位于 resources/assets/js/app.js,打开该文件,在

import router from './routes.js')

之后添加如下这行代码:

import store from './store.js'

这将会引入我们上几步创建的数据存储器,接下来,我们需要通过数据存储器来扩展 Vue 实例:

new Vue({
    router,
    store
}).$mount('#app');

现在我们就可以在应用中使用整个 Vue 全家桶了!下面我们将新增一些模块并使其可以正常工作。

第四步:新增 Vuex 模块 cafes.js

首先需要在 resources/assets/js/modules 目录下创建一个名为 cafes.js 的文件,我们将在这个文件中管理所有的咖啡店数据,然后在整个应用中使用这些数据。从这里,也可以看到单页面应用的优点:一次加载页面,将数据存储到 Vuex 模块,到处使用,只有在需要重新加载页面时才重新加载。

现在 cafes.js 文件还是空的,下一步就来配置这个文件。

第五步:配置 Vuex 模块的 state 属性

resources/assets/js/modules/cafes.js 文件中,首先从 api 目录下导入咖啡店相关 API,我们将使用其中的 API 请求方法来加载数据:

/*
|-------------------------------------------------------------------------------
| VUEX modules/cafes.js
|-------------------------------------------------------------------------------
| The Vuex data store for the cafes
*/

import CafeAPI from '../api/cafe.js';

现在我们将会导出一个常量作为咖啡店模块,在导入 CafeAPI 的下面添加如下这段代码:

export const cafes = {

}

这就是我们要添加到数据存储器的模块,稍后我们会将其导入到数据存储器。

接下来,我们需要设置上述 Vuex 模块的四个属性(stateactionsmutationsgetters)。

首先,添加一个空的 state 对象:

export const cafes = {
    state: {

    }
}

该状态是所有我们想要跟踪数据的状态,在 cafes 模块中有两个需要跟踪的数据:咖啡店数组,以及存储单个咖啡店的对象。分别对应返回所有咖啡店和单个咖啡店的 API。我们会这样初始化这两个数据:

export const cafes = {
    state: {
        cafes: [],
        cafe: {}
    }
}

根据经验,我们经常遇到的一个问题是显示加载状态。在单页面应用中,加载状态至关重要。HTML/CSS 和其他页面功能通常会在向等待数据加载的用户提供不良UX的数据之前加载。对于我们在状态中跟踪的每个数据,我会为跟踪加载状态的数据状态添加相应的变量。这样我就可以读取这个变量来确定是否显示加载状态。 随着 Vue 被激活,数据被加载后,这个变量会更新,使用该变量的组件也会更新,并相应地显示到页面。相应的状态变量定义如下:

export const cafes = {
    state: {
        cafes: [],
        cafesLoadStatus: 0,
        cafe: {},
        cafeLoadStatus: 0
    },
}

我通常定义状态码如下:

  • status = 0 -> 数据尚未加载
  • status = 1 -> 数据开始加载
  • status = 2 -> 数据加载成功
  • status = 3 -> 数据加载失败

这样我们就可以基于数据加载状态在需要的时候相应的提示信息。

第六步:配置 Vuex 模块的 actions 属性

actions 在模块中用于被调用来修改状态。在本教程中,我们会调用一个 action 用于发起 API 请求并提交 mutationsmutations 我们会在下一步中实现。

actions 对象中我们可以添加方法来加载所有咖啡店和单个咖啡店信息:

export const cafes = {
    state: {
        cafes: [],
        cafesLoadStatus: 0,

        cafe: {},
        cafeLoadStatus: 0
    },

    actions: {
        loadCafes( { commit } ){

        },
        loadCafe( { commit }, data ){

        }
    }
};

上述代码 actions 部分有两个需要注意的地方:

  • 每个方法都包含一个名为 commit 的析构参数,该参数通过 Vuex 传入,允许我们提交 mutations。你还可以传入其他的析构参数,要了解更多关于参数析构的细节,可以参考 lukehoban/es6features 这个 Github 项目。
  • loadCafe 动作包含了一个名为 data 的第二个参数。该参数是一个对象,包含我们想要加载的咖啡店的 ID。

现在,我们来实现这两个方法:

actions: {
    loadCafes( { commit } ){
        commit( 'setCafesLoadStatus', 1 );

        CafeAPI.getCafes()
            .then( function( response ){
                commit( 'setCafes', response.data );
                commit( 'setCafesLoadStatus', 2 );
            })
            .catch( function(){
                commit( 'setCafes', [] );
                commit( 'setCafesLoadStatus', 3 );
            });
    },

    loadCafe( { commit }, data ){
        commit( 'setCafeLoadStatus', 1 );

        CafeAPI.getCafe( data.id )
            .then( function( response ){
                commit( 'setCafe', response.data );
                commit( 'setCafeLoadStatus', 2 );
            })
            .catch( function(){
                commit( 'setCafe', {} );
                commit( 'setCafeLoadStatus', 3 );
            });

    }
},

首先需要注意的是 commit 函数,该函数用于提交一个 mutation,我们会在下一步设置 mutations。再次重申,state 中的每个数据片段都应该有一个与之对应的 mutation。在上面两个方法中,我们都提交了所使用的状态的加载状态,接下来,调用 API 来加载想要加载的指定信息状态,这些 API 调用定义在 resources/assets/js/api/cafe.js 文件中,之后链式调用 thencatch 方法,前者在 API 请求成功后调用,后者在 API 请求失败后调用,response 变量会传递到这两个方法,以便获取响应数据和请求头。

第七步:配置 Vuex 模块的 mutations 属性

mutations 定义了数据的更新方式,每个模块都有 state,每个 state 都需要对应的 mutation 来更新,完整工作流如下:

  • 用户调用一个 action
  • 该 action 加载/计算数据
  • 该 action 提交一个 mutation
  • state 被更新
  • getter 将更新后的 state 返回给组件
  • 组件被更新

以上工作流可以通过多种方式来实现,不过相较于 jQuery 或 vanilla JS 的实现,使用 Vuex 更加简单。

我们已经定义了 stateactions,现在是时候实现 mutations 了,我们在配置 actions 时已经看到了 mutations 的调用,现在只需实现其功能代码即可:

mutations: {
    setCafesLoadStatus( state, status ){

    },

    setCafes( state, cafes ){

    },

    setCafeLoadStatus( state, status ){

    },

    setCafe( state, cafe ){

    }
},

所有 mutations 所做的工作都是设置 state,所以第一个参数是 state,这里的 state 是局部模块 state 而不是全局 state,所以我们在第六步中配置的 state 可以被访问,第二个参数是 state 更新后的数据,所以最终实现如下:

mutations: {
    setCafesLoadStatus( state, status ){
      state.cafesLoadStatus = status;
    },

    setCafes( state, cafes ){
      state.cafes = cafes;
    },

    setCafeLoadStatus( state, status ){
      state.cafeLoadStatus = status;
    },

    setCafe( state, cafe ){
      state.cafe = cafe;
    }
},

在每个 mutation 中,我们将局部模块的 state 数据设置为传入的更新后数据,这也正是每个 mutation 所要做的操作。接下来,我们将会配置 getters

第八步:配置 Vuex 模块的 getters 属性

到目前为止,我们已经有了想要跟踪的 state 数据,从 API 接口获取数据的 actions,以及用于设置 statemutations,现在需要定义 getters 从模块中获取数据。

我们的 getters 对象需要像这样添加到 cafes 模块中:

getters: {

}

接下来,需要为每一个 state 数据的获取定义一个方法:

getters: {
    getCafesLoadStatus( state ){
      return state.cafesLoadStatus;
    },

    getCafes( state ){
      return state.cafes;
    },

    getCafeLoadStatus( state ){
      return state.cafeLoadStatus;
    },

    getCafe( state ){
      return state.cafe;
    }
}

每个 getter 方法都会传入一个局部模块 state 作为参数并返回相应的 state 数据,这就是 getters 所做的全部工作了!现在我们可以在组件中使用所有这些数据了。

至此,我们的 Vuex 模块已经全部定义好了:

/*
 |-------------------------------------------------------------------------------
 | VUEX modules/cafes.js
 |-------------------------------------------------------------------------------
 | The Vuex data store for the cafes
 */

import CafeAPI from '../api/cafe.js';

export const cafes = {
    /**
     * Defines the state being monitored for the module.
     */
    state: {
        cafes: [],
        cafesLoadStatus: 0,

        cafe: {},
        cafeLoadStatus: 0
    },
    /**
     * Defines the actions used to retrieve the data.
     */
    actions: {
        loadCafes( { commit } ){
            commit( 'setCafesLoadStatus', 1 );

            CafeAPI.getCafes()
                .then( function( response ){
                    commit( 'setCafes', response.data );
                    commit( 'setCafesLoadStatus', 2 );
                })
                .catch( function(){
                    commit( 'setCafes', [] );
                    commit( 'setCafesLoadStatus', 3 );
                });
        },

        loadCafe( { commit }, data ){
            commit( 'setCafeLoadStatus', 1 );

            CafeAPI.getCafe( data.id )
                .then( function( response ){
                    commit( 'setCafe', response.data );
                    commit( 'setCafeLoadStatus', 2 );
                })
                .catch( function(){
                    commit( 'setCafe', {} );
                    commit( 'setCafeLoadStatus', 3 );
                });

        }
    },
    /**
     * Defines the mutations used
     */
    mutations: {
        setCafesLoadStatus( state, status ){
            state.cafesLoadStatus = status;
        },

        setCafes( state, cafes ){
            state.cafes = cafes;
        },

        setCafeLoadStatus( state, status ){
            state.cafeLoadStatus = status;
        },

        setCafe( state, cafe ){
            state.cafe = cafe;
        }
    },
    /**
     * Defines the getters used by the module
     */
    getters: {
        getCafesLoadStatus( state ){
            return state.cafesLoadStatus;
        },

        getCafes( state ){
            return state.cafes;
        },

        getCafeLoadStatus( state ){
            return state.cafeLoadStatus;
        },

        getCafe( state ){
            return state.cafe;
        }
    }
};

第九步:将 Vuex 模块添加到数据存储器

最后,我们还要告诉 Vuex 数据存储器使用 cafes 模块,打开 resources/assets/js/store.js 文件,紧随

Vue.use( Vuex )

之后添加如下这段代码:

/**
 * Imports all of the modules used in the application to build the data store.
 */
import { cafes } from './modules/cafes.js'

并且修改默认导出数据存储器代码如下:

export default new Vuex.Store({
    modules: {
        cafes
    }
});

小结

在这篇教程中,我们创建了一个 Vuex 存储器并为咖啡店配置了一个 Vuex 模块。要想看到对应的效果,编译前端资源后(npm run dev),可以在开发环境访问 http://roast.test 并打开开发者工具,切换到 Vue 标签页,在 Vuex 部分就可以看到 stategetters 属性:

Vuex Module in Chrome Dev tools

下一篇教程中我们演示如何在 Vue 组件中使用 Vuex 模块。

项目源码位于 Github 上:nonfu/roastapp

学院君 has written 1197 articles

Laravel学院院长,终身学习者

积分:157467 等级:P12 职业:手艺人 城市:杭州

6 条回复

  1. Ailon Ailon says:

    第三步:新增数据存储器到 Vue 实例

    import router from './routes.js'`)

    多了一个反引号和右括号

  2. ideasource ideasource says:

    请问为什么不在开头统一引入?而是选择了模块式的引入?仅是为了学习者更好的理解吗?

  3. 吴亲库里 吴亲库里 says:

    应该是实例化的是Vue,我有个地方小写,显示difined,但是找不到这个小写的在哪。。

  4. 吴亲库里 吴亲库里 says:

    app.js:56072 Uncaught ReferenceError: vue is not defined ,点进去看报错的地方

    /**

    • Initializes Vuex on Vue. / vue.use(WEBPACK_IMPORTED_MODULE_1_vuex["a" / default */]);

    这里报的错,不懂vue,对着你的代码,还是没找出是什么错

登录后才能进行评论,立即登录?