Vuex03 Vuex其他

插件

Vuex的插件就是一个函数,接收store作为唯一参数,通过subscribestore每次的mutation进行监听:

const myPlugin = store => {
  // 当store初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}

插件需要使用plugins选项引入:

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

插件内同样不允许直接修改state,只能通过只能通过提交mutation来触发变化

严格模式

开启严格模式,当不通过mutation而直接修改state时,Vuex都会抛出错误。

不要再发布环境下启用严格模式,严格模式会深度检测状态树来检测不合规的状态变更,造成性能上的损失:

const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

表单处理

在开启了严格模式后,把Vuex的state使用到v-model会报错:

<input v-model="subTitle.message" type="text" />

上面的代码中的subTitle是属于Vuex的store的对象,用户输入时相当于没有通过mutation直接修改了state,Vuex会抛出错误:

Error: [vuex] do not mutate vuex store state outside mutation handlers.

解决方法有两个,一种是利用了v-model的语法糖的本质,将obj.message作为value,再input方法中手动触发commit方法,然后在mutation中修改state

<input :value="subTitle.message" type="text" @input="inputHandler"/>
export default {
  methods: {
    inputHandler(e) {
      this.$store.commit('changeSubTitle', { message: e.target.value })
    },
  },
  computed: {
    ...mapState(['subTitle']),
  }
}

在Store中:

export default new Vuex.Store({
  mutations: {
    changeSubTitle(state, { message }) {
      state.subTitle.message = message;
    }
  },
})

另一种方法就是使用带有setter的双向绑定计算属性:

<input v-model="title" type="text" />
computed: {
  // 带有 setter 的双向绑定的计算书行
  title: {
    get() {
      return this.$store.state.title
    },
    set(value) {
      this.$store.commit('changeTitle', {
        message: value
      })
    }
  },
}

测试

Mutation和Getter测试时思路相同,将Mutation或者Getter单独导出来,在测试文件中模拟一个state,来进行断言:

const state = {
  count: 0,
}

// mutations 作为命名输出对象
export const mutations = {
  increment: state => state.count++
}

export default new Vuex.Store({
  state,
  mutations
})
// mutations.spec.js
import { expect } from 'chai'
import { mutations } from './store'

const { increment } = mutations

describe('mutations', () => {
  it('INCREMENT', () => {
    // 模拟状态
    const state = { count: 0 }
    // 应用 mutation
    increment(state)
    // 断言结果
    expect(state.count).to.equal(1)
  })
})

测试Action比较麻烦,因为它们可能会调用外部的API,需要将外部的API调用进行Mock,可以使用webpack和inject-loader打包测试文件:

// actions.js
import shop from '../api/shop'

export const getAllProducts = ({ commit }) => {
  commit('REQUEST_PRODUCTS')
  shop.getProducts(products => {
    commit('RECEIVE_PRODUCTS', products)
  })
}
// actions.spec.js

// 使用 require 语法处理内联 loaders。
// inject-loader 返回一个允许我们注入 mock 依赖的模块工厂
import { expect } from 'chai'
const actionsInjector = require('inject-loader!./actions')

// 使用 mocks 创建模块
const actions = actionsInjector({
  '../api/shop': {
    getProducts (cb) {
      setTimeout(() => {
        cb([ /* mocked response */ ])
      }, 100)
    }
  }
})

// 用指定的 mutations 测试 action 的辅助函数
const testAction = (action, args, state, expectedMutations, done) => {
  let count = 0

  // 模拟提交
  const commit = (type, payload) => {
    const mutation = expectedMutations[count]

    try {
      expect(mutation.type).to.equal(type)
      if (payload) {
        expect(mutation.payload).to.deep.equal(payload)
      }
    } catch (error) {
      done(error)
    }

    count++
    if (count >= expectedMutations.length) {
      done()
    }
  }

  // 用模拟的 store 和参数调用 action
  action({ commit, state }, ...args)

  // 检查是否没有 mutation 被 dispatch
  if (expectedMutations.length === 0) {
    expect(count).to.equal(0)
    done()
  }
}

describe('actions', () => {
  it('getAllProducts', done => {
    testAction(actions.getAllProducts, [], {}, [
      { type: 'REQUEST_PRODUCTS' },
      { type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
    ], done)
  })
})

还是挺复杂的,实际上actionsInjector模块mock的仅仅是API部分(../api/shop),而actions.getAllProducts执行的还是原来的action,但是commitstate已经都被我们替换了。

如果可以使用Sinon.JS,那么可以使用它来替换上面的辅助函数testAction

describe('actions', () => {
  it('getAllProducts', () => {
   
    // 一步直接模拟 commit
    const commit = sinon.spy()
    const state = {}
    
    actions.getAllProducts({ commit, state })
    
    expect(commit.args).to.deep.equal([
      ['REQUEST_PRODUCTS'],
      ['RECEIVE_PRODUCTS', { /* mocked response */ }]
    ])
  })
})

如果需要给Vuex写单元测试的时候,还是需要到这里对照着例子来实现一下。

执行测试可以在Node环境下,也可以在浏览器环境下,在Node环境下执行的时候需要创建以下webpack配置(需要配置好.babelrc):

// webpack.config.js
module.exports = {
  entry: './test.js',
  output: {
    path: __dirname,
    filename: 'test-bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
}

执行的时候:

$ webpack 
$ mocha test-bundle.js

在浏览器中测试可以参考文档

热重载

Vue-cli脚手架针对Vuex提供了热刷新的功能,当更改Store的数据,页面会自动刷新,但是相比于Vue组件的热重载功能,体验还是略逊一筹。

Vuex想要实现热重载,也是借助了webpack的Hot Module Replacement API,以前曾经学习过它的实现原理(注意,面试的时候的高频题目)

实现热重载的前提就是,必须将代码模块化,所以Store中的Mutation/Module/Action/Getter必须导出为单独的JS文件,才可以实现热重载

if (module.hot) {
  module.hot.accept(['./modules/todo-list'], () => {
    // 获取更新后的模块
    // 因为 babel 6 的模块编译格式问题,下面需要加上 .default
    const newTodoList = require('./modules/todo-list').default;

    console.log(newTodoList);

    // 加载新模块
    store.hotUpdate({
      modules: {
        store_todoList: newTodoList,
      }
    })
  })
}

注意:热重载的目标只能是Mutation/Module/Action/Getter,手动对state的修改不能触发HMR,可以参考这个issue。所以这就导致了一个问题:

如果配置了热重载,那么如果改动state时就必须手动刷新,热刷新也没有了;如果不配置热重载,修改任何文件都是热刷新。这样的话热重载我感觉意义不大

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章