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時就必須手動刷新,熱刷新也沒有了;如果不配置熱重載,修改任何文件都是熱刷新。這樣的話熱重載我感覺意義不大

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