Vuejs2.x 的逻辑复用与状态管理

1,176 阅读2分钟

Vuejs2.x 的逻辑复用与状态管理

状态管理

在平常的业务开发中,常把应用状态划分为以下三种:

  • 全局状态:用于管理SPA应用全局状态,一直保留在应用生命周期内
  • 组件状态:用于管理组件内部状态,随组件销毁而销毁
  • 页面级状态:用于管理当前页面下所有模块的状态,随页面销毁而销毁

全局状态的管理

  • 使用 vuex 来管理全局状态
├── components
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块
  • 模块定义与 store申明

产品模块: modules/products

// initial state
const state = () => ({
  all: []
})

// getters
const getters = {}

// actions
const actions = {
  getAllProducts ({ commit }) {
    shop.getProducts(products => {
      commit('setProducts', products)
    })
  }
}

// mutations
const mutations = {
  setProducts (state, products) {
    state.all = products
  },

  decrementProductInventory (state, { id }) {
    const product = state.all.find(product => product.id === id)
    product.inventory--
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

Store声明, 应用全局共享一个 Vuex.Store 实例

import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    cart,
    products
  },
}

基本使用: components/icon ProductList.vue

import { mapState, mapActions } from 'vuex'

export default {
  computed: mapState({
    products: state => state.products.all
  }),
  methods: mapActions('cart', [
    'addProductToCart'
  ]),
  created () {
    this.$store.dispatch('products/getAllProducts')
  }
}

组件内部状态管理

使用组件的 data 来管理

export default {
  data(){
      return {
       isShow: false,
       count: 10,
       groceryList: [
          { id: 0, text: '蔬菜' },
          { id: 1, text: '奶酪' },
          { id: 2, text: '随便其它什么人吃的东西' }
        ]
      }
}

以上两种是最基本也是最常用的状态管理方式,下面主要介绍两种页面级的状态管理方式

页面级状态管理

动态注册状态模块来管理

使用 store.registerModule 来注册页面级状态模块 使用 store.unregisterModule 来卸载状态模块

全局注册的状态,都挂载于 this.$store 下面,

打印 this.$store 如下

可以看到 store 实例提供了动态注册和卸载状态模块的能力

应用到具体的业务

页面模块的目录结构

└── stores
└── views
    └── login
        ├── index.vue  # 页面模块
        └── store.js   # 产品模块

页面级状态 sotre.js

export default {
  // 加上 namespaced,避免与其它模块重名
  namespaced: true,
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
}

页面模块 index.vue

import { mapState, mapMutations } from 'vuex';
import counterStore from './store';

export default {
  //...
  computed: {
      ...mapState({
      count: (state) => state.counter.count,
    }),
  },
  methods: { 
    ...mapMutations({
      add: 'counter/increment'
    })
  }
  beforeCreate(){
    this.$store.registerModule('counter', counterStore)
  },
  beforeDestroy(){
    this.$store.unregisterModule('counter', counterStore)
  },
  //...
}

这样动态注册的好处是,随着页面的销毁,状态对象也会从全局状态管理器中卸载掉 页面下面所有的组件都能从 store 中获取相关状态数据,不用一层一层传递 缺点是,注册的模块仍然是全局状态,如果不小心使用,可能注册同名模块,有时也可能忘记卸载模块

使用 Mixin 实现逻辑复用

另外一种常用的方式就是使用类似于继承的方式,使用混入来复用逻辑 这种方式的好处是,每个vue组件的使用是独立的 混入的方式使得组件继承了所有的属性,多个组件使用同一 mixin 时互不影响, 也能跟随页面或组件的生命周期销毁而销毁

目录结构

└── mixins
    └── counter.mixin.js
└── views
    └── login
        ├── index.vue  # 页面模块
// counter.mixin.js
export default {
  data(){
    return {
      count: 0,
    };
  },
  methods: {
    increment(state) {
      state.count++
    }
  }
}
// login/index.vue
import counter from '@/mixins/counter.mixin'

export default {
    mixins: [counter],
    mounted(){
      console.log(this.count)
      console.log(this.increment)
    }
}

例:混入响应式断点

在管理平台的业务开发中一般组件库都提供了响应式的组件,但有时候我们不得不根据当前屏幕的宽度来调整一些业务逻辑

breakpoint.mixin.js 在需要的页面的混入到组件中,可实现通过 watch 状态属性 breakpointName 来处理相应的业务逻辑

export default {
  data: function(){
    return {
      breakpointName: 'md',
    }
  },
  mounted () {
    this.getResponseName();
    window.addEventListener('resize', this.getResponseName);
  },
  beforeDestroy () {
    window.removeEventListener('resize', this.getResponseName);
  },
  methods: {
    getResponseName() {
      const options = [
        { rule: '414px', size: 'xs' },
        { rule: '768px', size: 'sm' },
        { rule: '992px', size: 'md' },
        { rule: '1200px', size: 'lg' },
      ];
      const toMedia = rule => `(min-width: ${rule})`;
      options.forEach(v => {
        v.rule = toMedia(v.rule);
      });
      const res = options.reverse().find(v => window.matchMedia(v.rule).matches);
      const responseName = res ? res.size : 'lg';
      this.breakpointName = responseName;
    },
  }
}

使用

import breakpoint from '@/mixins/breakpoint.mixin'

export default {
    mixins: [breakpoint],
    mounted(){
      console.log(this.breakpointName)
    },
    watch: {
      breakpointName(n, o){
        if(n !== o){
          // todo
        }
      },
    }
  }
}

可以看出来,mixin 是逻辑复用利器,但唯一的缺点就是,当记不清当前 mixin 中所有的方法属性时会不小心覆盖,所以 mixin 一般跟随项目,如果做为第三方公共包,一定要提供详尽的文档,不然就只能翻源码了。

混入仍然不解决不了状态传递的问题,业务中还是需要一层层传递。

更复杂的更通用的业务,比如:列表展示,在管理平台中基本的CRUD逻辑都是一样的,可以封装如 crud.mxin.js 应用到各个页面,减少重复业务逻辑的编写,同时也便于集中管理维护升级CRUD通用逻辑,大大减少代码开发工作量。

vant组件库中使用的mixin

本文使用 mdnice 排版