Vuex


# 概述

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

在 vue 项目下 npm i vuex --save

每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
vuex 只能有一个仓库 (store)

mutations 函数是同步函数,所以不能有异步操作

# 如何创建和使用

  • App.vue

    <template>
      <div id="app">
        <h1>hello vuex</h1>
        <p>获取 store 中的 count: {{ count }}</p>
        <button @click="add">加一</button>
        <button @click="sub">减一</button>
        <button @click="change">将count改成100</button>
      </div>
    </template>
    
    <script>
      export default {
        components: {},
        name: "App",
        computed: {
          // 一般来说搭配计算属性获取 store 数据
          count() {
            return this.$store.state.count;
          },
        },
        methods: {
          add() {
            // this.$store.commit("add");
            this.$store.commit({ type: "add" });
          },
          sub() {
            this.$store.commit("sub");
          },
          change() {
            this.$store.commit("change", 100);
            // this.$store.commit("change", { count: 100 });
            // this.$store.commit({ type: "change", count: 100 });
          },
        },
      };
    </script>
  • src 下新建的 store 文件夹 中的 index.js 文件

    // store
    // 1.如何创建
    import Vue from "vue";
    import Vuex from "vuex";
    
    Vue.use(Vuex);
    
    // Store 这里的S一定要是大写的,这是一个类
    const store = new Vuex.Store({
      // state相当于组件data的返回值
      state: {
        count: 10,
      },
      // store 中的数据要修改的话需要定义 mutations 函数
      // mutation 函数 就是用来修改 store 数据的
      // 每一个 mutation 函数默认第一个参数接收的是 store 内的 state,想要修改哪个数据直接修改即可
      mutations: {
        add(state) {
          state.count++;
        },
        sub(state) {
          state.count > 1 && state.count--;
        },
        // mutation 函数的第二个以及后面的参数叫做载荷数据
        // mutation 函数 只能接收两个参
        change(state, count) {
          state.count = count;
        },
        /* change(state, { count }) {
            state.count = count;
            },// 后两种写法 */
      },
      // 定义好 mutation 函数之后
      // 在组件内可以使用 $store 内的 commit 方法来触发对应 mutation 函数
      // 比如 this.$store.commit('add')
      // 比如 this.$store.commit({type:'add'})
      // mutation 函数传参写法
      // this.$store.commit('add',payload) 接收的时候 payload 就是第二个参数
      // this.$store.commit({type:'add',count:值}) 接收的时候 commit传递的对象就是第二个参数
    });
    // 2.导出
    export default store;
    // 3.组件中使用
    // a.在main.js中导入,然后挂载到 new Vue中;
    // b.在组件内直接使用 this.$store.state.count 访问
  • main.js 文件

    import store from "./store/index"; // 后边的 /index 可以不用引入,因为是 index
    new Vue({
      store,
      render: (h) => h(App),
    }).$mount("#app");

# 评论小案例

  • Article.vue

    <template>
      <div class="article">
        <ArticleContent />
        <ArticleComment />
        <button @click="getCommentById">根据id获取对应的评论数据 log出来</button>
      </div>
    </template>
    
    <script>
      import ArticleContent from "./ArticleContent";
      import ArticleComment from "./ArticleComment";
      export default {
        components: { ArticleContent, ArticleComment },
        name: "Article",
        methods: {
          // 很多地方都会用到这个功能,所以直接在组件内计算不合适,最好在 store 添加一个 getter
          getCommentById() {
            console.log(this.$store.getters.getCommentById(2));
          },
        },
      };
    </script>
  • ArticleContent.vue

    <template>
      <div class="article-content" v-if="article">
        <h2>{{ article.title }}</h2>
        <p class="comment-num">评论数:{{ commentNum }}</p>
        <div class="content" v-html="article.content"></div>
      </div>
      <div v-else>请稍等</div>
    </template>
    
    <script>
      export default {
        name: "ArticleContent",
        computed: {
          article() {
            return this.$store.state.article;
          },
          commentNum() {
            return this.$store.getters.commentNum;
          },
        },
      };
    </script>
  • ArticleComment.vue

    <template>
      <div class="article-comment">
        <h3>评论 <span>评论总数:{{ commentsNum }}</span></h3>
        <textarea v-model.lazy.trim="text"></textarea>
        <button @click="submit">提交</button>
        <ul v-if="comments.length">
          <li v-for="comment in comments" :key="comment.id">
            <span>{{ comment.text }}</span>
            <button @click="del(comment.id)">删除</button>
          </li>
        </ul>
      </div>
    </template>
    
    <script>
      export default {
        name: "ArticleComment",
        computed: {
          comments() {
            return this.$store.state.comments;
          },
          commentsNum() {
            return this.$store.getters.commentNum;
          },
        },
        data() {
          return {
            text: "",
          };
        },
        methods: {
          submit() {
            const { text } = this;
            if (text) {
              this.$store.commit({
                type: "addComment",
                comment: { id: new Date().getTime(), text },
              });
              this.text = "";
            }
          },
          del(id) {
            this.$store.commit("delComment", id);
          },
        },
      };
    </script>
  • store 文件夹下的 index.js 文件

    import Vue from "vue";
    import Vuex from "vuex";
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      state: {
        article: {
          title: "vuex 入门",
          content: "<p>贼简单</p>",
        },
        comments: [
          { id: 1, text: "666" },
          { id: 2, text: "哈哈哈" },
          { id: 3, text: "gogo" },
        ],
      },
      // state 的计算属性 getter
      getters: {
        // getter 默认接收 state 作为参数,函数的返回值就是计算属性的值
        // 接收 getters 作为第二个参数,该参数指的是所有的getter
        commentNum(state, getters) {
          console.log(getters.a); //10 getters是一个对象
          return state.comments.length;
        },
        a() {
          return 10;
        },
        // 组件中使用 this.$store.getters.commentNum
    
        // 传参的 getter
        // 使用的时候需要当成函数调用
        // getter 函数要返回一个函数,返回的函数必须设置返回值,这个返回值是计算属性
        // 返回的函数接收组件内使用getter的传参
        getCommentById: (state) => (id) =>
          state.comments.find((comment) => comment.id === id),
        // 第一个是 getCommentById 函数本身
        // 第二个是返回的函数
      },
      mutations: {
        addComment(state, { comment }) {
          state.comments.push(comment);
        },
        delComment(state, id) {
          state.comments = state.comments.filter((comment) => comment.id !== id);
        },
      },
    });
    export default store;

# 对象更新小问题

  • ObjectState.vue

    <template>
      <!-- 更新时会出现的问题 -->
      <div>
        <button @click="update">添加一个年龄</button>
        <div v-for="(value, key) in object" :key="key">
          <!-- 循环对象 -->
          <!-- key 属性名 value属性值 -->
          <p><span>{{ key }}</span> : <span>{{ value }}</span></p>
          <!-- name: 毛茸茸 -->
          <!-- sex: 男 -->
        </div>
      </div>
    </template>
    
    <script>
      export default {
        name: "ObjectState",
        computed: {
          object() {
            return this.$store.state.testObject;
          },
        },
        methods: {
          update() {
            this.$store.commit("updateObject", 20);
          },
        },
      };
    </script>
  • index.js

    import Vue from "vue";
    import Vuex from "vuex";
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      state: {
        testObject: {
          name: "毛茸茸",
          sex: "男",
        },
      },
      mutations: {
        updateObject: (state, age) => {
          // 对象的新增属性操作不能引起视图的变化,需要使用 Vue 的 set 方法添加
          // Vue.set(state.testObject, "age", age); // 变量想要当成属性名的话需要加字符串
          state.testObject = { ...state.testObject.age };
          // state.testObject.age = age; //这个更新不了
        },
      },
    });
    export default store;

# 服务器请求数据

  • 1. 请求评论

    • 自建 src 文件夹下 data 文件夹中的 data.json

      {
        "comments": [
          { "id": 1, "text": "66666", "articleId": 1 },
          { "id": 2, "text": "哈哈哈", "articleId": 1 },
          { "id": 3, "text": "gogo", "articleId": 2 }
        ],
        "articles": [
          {
            "id": 1,
            "title": "vuex 入门",
            "content": "<p>贼简单</p>"
          },
          {
            "id": 2,
            "title": "vuex 进阶",
            "content": "<p>不是简单</p>"
          }
        ]
      }
    • index.js

      import Vue from "vue";
      import Vuex from "vuex";
      Vue.use(Vuex);
      
      const store = new Vuex.Store({
        state: {
          comments: [], // 这里是空的
        },
        // state 的计算属性 getter
        getters: {
          // 传参的 getter
          getCommentById: (state) => (id) =>
            state.comments.find((comment) => comment.id === id),
          // 第一个是 getCommentById 函数本身
          // 第二个是返回的函数
        },
      
        // mutations 函数是同步函数,所以不能有异步操作
        mutations: {
          // 这里是 请求评论函数
          getComments: (state, comments) => {
            state.comments = comments;
          },
        },
      });
      export default store;
    • 自己封装的 plugins 文件夹下的 axios.js

      // 封装 axios
      // http://localhost:8080/ 向 http://localhost:3008/ 取数据,跨域
      // 协议 域名 端口号
      // axios 默认不支持跨域,浏览器支持 本地跨域(localhost)
      import axios from "axios";
      import Vue from "vue";
      // 基地址
      axios.defaults.baseURL = "http://localhost:3008/";
      
      // 超时时间
      axios.defaults.timeout = 2500;
      
      // 响应拦截
      axios.interceptors.response.use((res) => res.data);
      // 这里是响应的成功拦截,res.data 变成了 res
      
      // 请求拦截器
      
      // 设置成 Vue 全局
      Vue.prototype.$axios = axios;
      
      // 导出
      export default axios;
    • Article.vue

      <template>
        <div class="article">
          <button @click="getCommentById">
            根据id获取对应的评论数据 log出来
          </button>
        </div>
      </template>
      
      <script>
        export default {
          // 这里是请求
          created() {
            // 因为设置了基地址,所以直接写后面的地址就可以
            this.$axios.get("/comments?articleId=1").then((res) => {
              // 因为设置了响应拦截器,res就是 res.data
              this.$store.commit("getComments", res);
            });
          },
          name: "Article",
          methods: {
            getCommentById() {
              console.log(this.$store.getters.getCommentById(2));
            },
          },
        };
      </script>
  • 2. 请求评论和文章并发

    • Article.vue

      <script>
        export default {
          created() {
            // 请求文章评论和详情
            this.$axios
              .all([
                this.$axios.get("/comments?articleId=1"),
                this.$axios.get("/articles/1"),
              ])
              .then((res) => {
                this.$store.commit("getComments", res[0]);
                this.$store.commit("getArticle", res[1]);
              });
          },
        };
      </script>
    • index.js

      import Vue from "vue";
      import Vuex from "vuex";
      Vue.use(Vuex);
      
      const store = new Vuex.Store({
        state: {
          article: null,
          comments: [],
        },
      
        // mutations 函数是同步函数,所以不能有异步操作
        mutations: {
          // 这里是请求文章函数
          getArticle: (state, article) => {
            state.article = article;
          },
          // 这里是请求评论函数
          getComments: (state, comments) => {
            state.comments = comments;
          },
        },
      });
      export default store;

# Actions

Action 可以包含异步操作
异步操作想要修改 store 中的数据时,放到 Action 中

  • 请求小案例

    • Article.vue

      <script>
        export default {
          created() {
            // 请求文章评论和详情
            this.$store.dispatch("getComments");
            this.$store.dispatch("getArticle");
          },
        };
      </script>
    • index.js

      import axios from "../plugins/axios";
      // 这里要引入自己创建的axios,否则会引入默认的axios
      
      const store = new Vuex.Store({
        state: {
          // 文章内容
          article: null,
          comments: [],
        },
        // mutations 函数是同步函数,所以不能有异步操作
        mutations: {
          // 这里是请求文章函数
          getArticle: (state, article) => {
            state.article = article;
          },
          // 这里是请求评论函数
          getComments: (state, comments) => {
            state.comments = comments;
          },
        },
      
        // action函数,存放更新 store 数据的异步操作
        // 异步执行之后使用 commit 提交 mutation
        // 在组件中使用 dispatch 方法触发 action
        // this.$store.dispatch(action函数名)
        actions: {
          // action 函数的函数名一般设置成和对应的 mutation 函数名相同
          // action 函数默认第一个参数是 context(上下文),它与 store 实例具有相同的属性和方法
          // 我们可以从该对象中获取 commit 方法
          // context 等价于 this.$store
          getComments: async ({ commit }) => {
            // { commit } 解构赋值 取 commit
            // 发请求获取评论数据,然后使用 commit 提交 mutation
            const res = await axios.get("/comments?articleId=1");
            commit("getComments", res);
          },
          getArticle: async ({ commit }) => {
            // 发请求获取文章数据,然后使用 commit 提交 mutation
            const res = await axios.get("/articles/1");
            // 用 / 拿到的是对象(对应的那一个条)  ?拿到的是数组
            commit("getArticle", res);
          },
        },
      });
      export default store;
  • 请求添加删除案例

    • index.js

      import Vue from "vue";
      import Vuex from "vuex";
      import axios from "../plugins/axios";
      
      Vue.use(Vuex);
      
      const store = new Vuex.Store({
        state: {
          article: null,
          comments: [],
        },
        // mutations 函数是同步函数,所以不能有异步操作
        mutations: {
          // 这里是请求文章函数
          getArticle: (state, article) => {
            state.article = article;
          },
          // 这里是请求评论函数
          getComments: (state, comments) => {
            state.comments = comments;
          },
          addComment(state, { comment }) {
            state.comments.push(comment);
          },
          delComment(state, id) {
            state.comments = state.comments.filter(
              (comment) => comment.id !== id
            );
          },
        },
      
        // action函数,存放更新 store 数据的异步操作
        actions: {
          getComments: async ({ commit }) => {
            // 发请求获取评论数据,然后使用 commit 提交 mutation
            const res = await axios.get("/comments?articleId=1");
            commit("getComments", res);
          },
          getArticle: async ({ commit }) => {
            // 发请求获取文章数据,然后使用 commit 提交 mutation
            const res = await axios.get("/articles/1");
            commit("getArticle", res);
          },
      
          /* // 传参的 action
          addComment: async ({ commit }, { comment }) => {
            // comment 必须和后台格式一样,id可以没有 {text,articleId}
            const res = await axios.post("/comments", comment);
            commit({ type: "addComment", comment: res });
          }, */
      
          // promise 写法,async简单一些
          addComment: ({ commit }, { comment }) => {
            return new Promise((resolve, reject) => {
              axios
                .post("/comments", comment)
                .then((res) => {
                  commit({ type: "addComment", comment: res });
                  resolve(); // 这里是 ArticleComment中then传的参数
                })
                .catch((err) => {
                  console.log(err);
                  reject(); // 这里是 ArticleComment中catch传的参数
                });
            });
            // action函数内也可以执行其他的action函数,只需要从context 中拿 dispatch 即可
          },
          delComment: async ({ commit }, { id }) => {
            await axios.delete(`/comments/${id}`);
            commit("delComment", id);
          },
        },
      });
      export default store;
    • ArticleComment.vue

      <template>
        <div class="article-comment">
          <textarea v-model.lazy.trim="text"></textarea>
          <button @click="submit">提交</button>
          <ul v-if="comments.length">
            <li v-for="comment in comments" :key="comment.id">
              <span>{{ comment.text }}</span>
              <button @click="del(comment.id)">删除</button>
            </li>
          </ul>
        </div>
      </template>
      
      <script>
        export default {
          name: "ArticleComment",
          computed: {
            comments() {
              return this.$store.state.comments;
            },
          },
          data() {
            return {
              text: "",
            };
          },
          methods: {
            // async submit() {
            submit() {
              const { text } = this;
              if (text) {
                // await 只能等 promise 和 async 函数
                /* await this.$store.dispatch({
                  type: "addComment",
                  comment: { text, articleId: 1 },
                });
                this.text = ""; */
      
                // promise写法
                this.$store
                  .dispatch({
                    type: "addComment",
                    comment: { text, articleId: 1 },
                  })
                  .then(() => {
                    this.text = "";
                  })
                  .catch(() => {
                    console.log("出错了");
                  });
              }
            },
            del(id) {
              // this.$store.commit("delComment", id);
              this.$store.dispatch({ type: "delComment", id });
            },
          },
        };
      </script>
    • Article.vue

      <script>
        export default {
          created() {
            // 请求文章评论和详情
            this.$store.dispatch("getArticle");
            this.$store.dispatch("getComments");
          },
        };
      </script>

# Modules

模块,把 store 分割成模块

  • 案例 1 这里没有使用命名空间

    • Article.vue

      <template>
        <div class="article">
          <ArticleContent />
        </div>
      </template>
      
      <script>
        import ArticleContent from "./ArticleContent";
        export default {
          created() {
            // 请求文章评论和详情
            this.$store.dispatch("getArticle");
          },
          components: { ArticleContent },
        };
      </script>
    • ArticleContent.vue

      <template>
        <div class="article-content" v-if="article">
          <h2>{{ article.title }}</h2>
          <div class="content" v-html="article.content"></div>
        </div>
        <div v-else>请稍等</div>
      </template>
      
      <script>
        export default {
          name: "ArticleContent",
          computed: {
            article() {
              return this.$store.state.article.data;
              // 这里换成了data,这里的article是modules中的属性名
              // this.$store.state.article 拿到的是对应的模块,模块下的state有data属性
            },
          },
        };
      </script>
    • index.js

      import Vue from "vue";
      import Vuex from "vuex";
      import axios from "../plugins/axios";
      import article from "./modules/article"; // 这里一定要导入进来
      
      Vue.use(Vuex);
      
      const store = new Vuex.Store({
        state: {
          // 文章内容
          // article: null,
        },
        // mutations 函数是同步函数,所以不能有异步操作
        mutations: {
          // 这里是请求文章函数
          /* getArticle: (state, article) => {
            state.article = article;
          }, */
          // 这里是请求评论函数
        },
      
        // 用来合并模块
        modules: {
          // key: value
          // key 是合并之后的名称 value 是对应模块的值
          article: article, // 需要把上面跟 article 相关的注释掉
        },
      });
      export default store;
    • src 文件夹下自建的 store 文件夹下自建的 modules 文件夹下的 article.js

      import axios from "../../plugins/axios";
      // 文章模块
      // 一个 store 模块就是一个对象,这个对象下可以写 state mutations getters actions
      const article = {
        // 模块内的 state 需要写成一个函数返回一个对象
        state: () => ({
          data: null,
        }),
        // 尽管分了模块,如果模块没有命名的话,mutation 和 action 都是全局的,
        // 也就是说在组件中用 commit 和 dispatch 触发对应函数的时候和原来没区别
        mutations: {
          getArticle: (state, article) => {
            state.data = article;
          },
        },
        actions: {
          getArticle: async ({ commit }) => {
            // 发请求获取文章数据,然后使用 commit 提交 mutation
            const res = await axios.get("/articles/1");
            commit("getArticle", res);
          },
        },
      };
      export default article;
  • 案例 2 这里使用了命名空间

    • Article.vue

      <template>
        <div class="article">
          <ArticleComment />
        </div>
      </template>
      
      <script>
        import ArticleComment from "./ArticleComment";
        export default {
          created() {
            this.$store.dispatch("commentsModule/getComments");
          },
          components: { ArticleComment },
        };
      </script>
    • ArticleComment.vue

      <template>
        <div class="article-comment">
          <h3>评论 <span>评论总数:{{ commentsNum }}</span></h3>
          <textarea v-model.lazy.trim="text"></textarea>
          <button @click="submit">提交</button>
          <ul v-if="comments.length">
            <li v-for="comment in comments" :key="comment.id">
              <span>{{ comment.text }}</span>
              <button @click="del(comment.id)">删除</button>
            </li>
          </ul>
        </div>
      </template>
      
      <script>
        export default {
          name: "ArticleComment",
          computed: {
            comments() {
              return this.$store.state.commentsModule.comments;
            },
            commentsNum() {
              // 默认情况下 action,mutation,getter 是全局的
              // return this.$store.getters.commentNum;
              // 加了命名空间之后要这样写
              return this.$store.getters["commentsModule/commentNum"];
            },
          },
          data() {
            return {
              text: "",
            };
          },
          methods: {
            submit() {
              const { text } = this;
              if (text) {
                this.$store
                  .dispatch({
                    type: "commentsModule/addComment", // 这里也需要改,因为使用了命名空间
                    comment: { text, articleId: 1 },
                  })
                  .then(() => {
                    this.text = "";
                  })
                  .catch(() => {
                    console.log("出错了");
                  });
              }
            },
            del(id) {
              // this.$store.commit("delComment", id);
              this.$store.dispatch({ type: "commentsModule/delComment", id }); // 这里也需要改,因为使用了命名空间
            },
          },
        };
      </script>
    • src 文件夹下自建的 store 文件夹下自建的 modules 文件夹下的 comments.js

      import axios from "../../plugins/axios";
      // 文章评论模块
      const commentsModule = {
        namespaced: true,
        // 当模块添加了命名空间,空间的名称是在 store 中合并时候的属性名
        // 组件内访问该空间下的所有内容时需要指定空间名
        // 例如执行 action 时,this.$store.dispatch('空间名/action函数名')
        // 例如执行 mutation 时,this.$store.commit('空间名/action函数名')
        // 例如获取 getter 时,this.$store.getters['空间名/getter名']
        state: () => ({
          comments: [],
        }),
        getters: {
          // 模块内的 getter 函数默认接收四个参数
          // state(局部) getters(局部)
          // rootState(全局的state store中的数据) rootGetters(全局)
          commentNum(state) {
            return state.comments.length;
          },
        },
        mutations: {
          getComments: (state, comments) => {
            state.comments = comments;
          },
          addComment(state, { comment }) {
            state.comments.push(comment);
          },
          delComment(state, id) {
            state.comments = state.comments.filter(
              (comment) => comment.id !== id
            );
          },
        },
        actions: {
          // 一个模块内的 action 函数内的第一个参数中可以获取到
          // commit dispatch rootState(store数据) rootGetters
          getComments: async ({ commit }) => {
            // 发请求获取评论数据,然后使用 commit 提交 mutation
            const res = await axios.get("/comments?articleId=1");
            commit("getComments", res);
          },
          addComment: ({ commit }, { comment }) => {
            return new Promise((resolve, reject) => {
              axios
                .post("/comments", comment)
                .then((res) => {
                  commit({ type: "addComment", comment: res });
                  resolve();
                })
                .catch((err) => {
                  console.log(err);
                  reject();
                });
            });
          },
          delComment: async ({ commit }, { id }) => {
            await axios.delete(`/comments/${id}`);
            commit("delComment", id);
          },
        },
      };
      export default commentsModule;
    • index.js

      import Vue from "vue";
      import Vuex from "vuex";
      // import axios from "../plugins/axios";
      import article from "./modules/article";
      import commentsModule from "./modules/comments";
      
      Vue.use(Vuex);
      
      const store = new Vuex.Store({
        state: {
          // article: null,
          // comments: [],
          testObject: {
            name: "毛茸茸",
            sex: "男",
          },
        },
        getters: {},
        mutations: {},
        actions: {},
      
        // 用来合并模块
        modules: {
          // key: value
          // key 是合并之后的名称 value 是对应模块的值
          article: article,
          commentsModule,
        },
      });
      export default store;

文章作者: Mrr-cxh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Mrr-cxh !
赏
 上一篇
uni-app uni-app
uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。
2021-07-14
下一篇 
Vue-Router Vue-Router
Vue Router 是 Vue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:嵌套的路由/视图表,模块化的、基于组件的路由配置,路由参数、查询、通配符,基于 Vue.js 过渡系统的视图过渡效果……
2021-05-24
  目录