状态管理

在很多应用场景下,我们需要在组件之间共享状态,比如我们的左侧导航栏需要收缩和展开的功能,收缩状态时宽度很小,只显示菜单图标,因为导航菜单栏收缩之后宽度变了,所以右侧的主内容区域要占用导航栏收缩的空间,主内容区域宽度也要根据导航栏的收缩状态做变更,而导航栏和主内容区域是两个不同的组件,而非父子组件之间不支持状态传递,所以组件之间的状态共享问题发生了。

之前我写flutter的时候,响应式设计的状态管理给我留下了深刻的印象,当时我用了provider这个库来管理全局状态。然后vue生态里有个vuex,据说借鉴了 FluxReduxThe Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

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

Vuex官网地址:https://vuex.vuejs.org/zh/

安装vuex

不管那么多,先安装

yarn add vuex@3.6.2

注意要使用3.x版本的vuex,4.x是vue3用的,目前我们的项目还是基于vue2。

添加store

在src目录下新建一个store目录,专门管理应用状态。

目录结构如下

store
├── modules
│   └── app.js
└── index.js

index.js

在index.js中引入vuex并统一组织导入和管理子模块。

代码如下

import Vue from 'vue'
import vuex from 'vuex'

Vue.use(vuex);

// 引入子模块
import app from './modules/app'

const store = new vuex.Store({
  modules: {
    app: app
  }
})

export default store

modules/app.js

app.js是属于应用内的全局性的配置,比如主题色、导航栏收缩状态等,详见注释。

代码如下

export default {
  state: {
    appName: "StarBlog",  // 应用名称
    themeColor: "#14889A",  // 主题颜色
    oldThemeColor: "#14889A",   // 上一次主题颜色
    collapse: false,  // 导航栏收缩状态
    menuRouteLoaded: false    // 菜单和路由是否已经加载
  },
  getters: {
    collapse(state) {// 对应着上面state
      return state.collapse
    }
  },
  mutations: {
    onCollapse(state) {  // 改变收缩状态
      state.collapse = !state.collapse
    },
    setThemeColor(state, themeColor) {  // 改变主题颜色
      state.oldThemeColor = state.themeColor
      state.themeColor = themeColor
    },
    menuRouteLoaded(state, menuRouteLoaded) {  // 改变菜单和路由的加载状态
      state.menuRouteLoaded = menuRouteLoaded;
    }
  },
  actions: {}
}

引入与使用store

src/main.js中引入store。

代码如下

import store from './store'

new Vue({
  el: '#app',
  router,
  store,	// 传入store
  components: {App},
  template: '<App/>'
})

这里以收缩侧边栏功能为例。

首先给顶栏添加一个按钮,用来控制侧边栏收缩。

添加收缩组件

在src下新建components目录,并在其下创建导航栏收缩展开组件Hamburger。

目录结构如下

src
└── components
    └── Hamburger
        └── index.vue

组件是使用SVG绘制,绘制根据isActive状态决定是否旋转、显示收缩和展开状态不同的图形。

代码如下

<template>
  <svg t="1492500959545" @click="onClick == null ? emptyClick : onClick" class="hamburger" fill="#fff"
       fill-opacity="0.8" :class="{'is-active':isActive}" viewBox="0 0 1024 1024"
       version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1691" xmlns:xlink="http://www.w3.org/1999/xlink"
       width="64" height="64">
    <path
      d="M966.8023 568.849776 57.196677 568.849776c-31.397081 0-56.850799-25.452695-56.850799-56.850799l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 543.397081 998.200404 568.849776 966.8023 568.849776z"
      p-id="1692"></path>
    <path
      d="M966.8023 881.527125 57.196677 881.527125c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 856.07443 998.200404 881.527125 966.8023 881.527125z"
      p-id="1693"></path>
    <path
      d="M966.8023 256.17345 57.196677 256.17345c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.850799 56.850799-56.850799l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.850799l0 0C1023.653099 230.720755 998.200404 256.17345 966.8023 256.17345z"
      p-id="1694"></path>
  </svg>
</template>

<script>
export default {
  name: 'hamburger',
  props: {
    isActive: {
      type: Boolean,
      default: false
    },
    onClick: {
      type: Function,
      default: null
    }
  },
  methods: {
    emptyClick() {

    }
  }
}
</script>

<style scoped>
.hamburger {
  display: inline-block;
  cursor: pointer;
  width: 20px;
  height: 20px;
  transform: rotate(90deg);
  transition: .38s;
  transform-origin: 50% 50%;
}

.hamburger.is-active {
  transform: rotate(0deg);
}
</style>

HeaderBar页面引用收缩组件

编辑HeaderBar.vue,在头部区域HeadBar中引入hamburger,并将自身isActive状态跟收缩状态collapse绑定

布局代码如下

<template>
  <div class="header" style="background:#14889A"
       :class="collapse?'position-collapse-left':'position-left'">
    <!-- 导航收缩 -->
    <span class="hamburg">
      <el-menu class="el-menu-demo" background-color="#14889A" text-color="#fff" active-text-color="#14889A"
               mode="horizontal">
        <el-menu-item index="1" @click="onCollapse"><hamburger :isActive="collapse"></hamburger></el-menu-item>
      </el-menu>
    </span>
    <!-- 工具栏 -->
    <!-- 旧代码省略 -->
</template>

通过computed计算属性引入store属性,这样就可以直接在页面中通过collapse引用状态值了。(当然如果不嫌长,也可以不使用计算属性,直接在页面中通过$store.state.app.collapse引用)

添加以下js代码

// 首先引入
import {mapState} from 'vuex'
import Hamburger from "@/components/Hamburger"

export default {
  name: "HeaderBar",
  components: {
    Hamburger
  },
  methods: {
    // 折叠导航栏
    onCollapse: function () {
      this.$store.commit('onCollapse')
    },
  },
  computed: {
    ...mapState({
      collapse: state => state.app.collapse
    })
  }
}

添加以下scss代码

.hamburg, .navbar {
  float: left;
}
.position-collapse-left {
  left: 65px;
}

修改Navbar.vue

布局代码修改

<template>
  <div class="menu-bar-container">
    <!-- logo -->
    <div class="logo" style="background:#14889A" :class="collapse?'menu-bar-collapse-width':'menu-bar-width'"
         @click="$router.push('/')">
      <img v-if="collapse" src="@/assets/codelab.jpg"/>
      <div>{{ collapse ? '' : appName }}</div>
    </div>
  </div>
</template>

js代码修改

import {mapState} from 'vuex'
export default {
  computed: {
    ...mapState({
      appName: state => state.app.appName,
      collapse: state => state.app.collapse,
    })
  },
}

scss代码添加

.menu-bar-collapse-width {
    width: 65px;
}

MainContent页面修改

修改布局代码

<template>
  <div id="main-container" class="main-container"
       :class="$store.state.app.collapse?'position-collapse-left':'position-left'">
    <!-- 标签页 -->
    <div class="tab-container"></div>
    <!-- 主内容区域 -->
    <div class="main-content">
      <keep-alive>
        <transition name="fade" mode="out-in">
          <router-view></router-view>
        </transition>
      </keep-alive>
    </div>
  </div>
</template>

添加scss代码

.position-collapse-left {
  left: 65px;
}

测试效果

未收缩侧边栏的情况

image-20220220173448183

点击按钮收缩侧边栏

image-20220220173506251

完成,收工~