Vue知識總結(2)

weixin_34253539發表於2018-11-16

Vuex狀態管理

Vuex是專門為Vue應用程式提供的狀態管理模式,每個Vuex應用的核心是store倉庫),即裝載應用程式state狀態)的容器,每個應用通常只擁有一個store例項。

5815514-612eaffab51906c8.png
vuex.png

Vuex的state是響應式的,即store中的state發生變化時,相應元件也會進行更新,修改store當中state的唯一途徑是提交mutations

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

store.commit("increment")       // 通過store.state來獲取狀態物件

console.log(store.state.count)  // 通過store.commit()改變狀態

State

store當中獲取state的最簡單辦法是在計算屬性中返回指定的state,每當state發生改變的時候都會重新執行計算屬性,並且更新關聯的DOM。

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

Vuex提供store選項,將state從根元件注入到每個子元件中,從而避免頻繁import store

// 父元件中註冊store屬性
const app = new Vue({
  el: "#app",
  store: store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>`
})

// 子元件,store會注入到子元件,子元件可通過this.$store進行訪問
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

Vuex提供mapState()輔助函式,避免使用多個state的場景下,多次去宣告計算屬性。

// 在單獨構建的版本中輔助函式為 Vuex.mapState
import { mapState } from "vuex"

export default {
  computed: mapState({
    count: state => state.count,
    // 傳遞字串引數"count"等同於`state => state.count`
    countAlias: "count",
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

// 當計算屬性名稱與state子節點名稱相同時,可以向mapState傳遞一個字串陣列
computed: mapState([
  "count" // 對映this.count到store.state.count
])

mapState()函式返回一個包含有state相關計算屬性的物件,這裡可以通過ES6的物件展開運算子...將該物件與Vue元件本身的computed屬性進行合併。

computed: {
  localComputed () {},
  ...mapState({})
}

Vuex允許在store中定義getters可視為store的計算屬性),getters的返回值會根據其依賴被快取,只有當依賴值發生了改變才會被重新計算。該方法接收state作為第1個引數,其它getters作為第2個引數。可以直接在store上呼叫getters來獲取指定的計算值。

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: "...", done: true },
      { id: 2, text: "...", done: false }
    ]
  },
  getters: {
    doneTodos: (state, getters) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

// 獲取doneTodos = [{ id: 1, text: "...", done: true }]
store.getters.doneTodos

這樣就可以方便的根據store中現有的state派生出新的state,從而避免在多個元件中複用時造成程式碼冗餘。

computed: {
  doneTodosCount () {
    // 現在可以方便的在Vue元件使用store中定義的doneTodos
    return this.$store.getters.doneTodos 
  }
}

Vuex提供的mapGetters()輔助函式將store中的getters對映到區域性計算屬性。

import { mapGetters } from "vuex"

export default {
  computed: {
    // 使用物件展開運算子將getters混入computed計算屬性
    ...mapGetters([
      "doneTodosCount",
       // 對映store.getters.doneTodosCount到別名this.doneCount
      doneCount: "doneTodosCount" 
    ])
  }
}

Mutations

修改store中的state的唯一方法是提交mutation([mjuː"teɪʃ(ə)n] n.變化),mutations類似於自定義事件,擁有一個字串事件型別和一個回撥函式(接收state作為引數,是對state進行修改的位置)。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // 觸發型別為increment的mutation時被呼叫
    increment (state) {
      state.count++ // 變更狀態
    }
  }
})

// 觸發mutation
store.commit("increment")

可以通過store的commit()方法觸發指定的mutations,也可以通過store.commit()向mutation傳遞引數。

// commit()
store.commit({
  type: "increment",
  amount: 10
})

// store
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

mutation事件型別建議使用常量,並且將這些常量放置在單獨檔案,便於管理和防止重複。

// mutation-types.js
export const SOME_MUTATION = "SOME_MUTATION"

// store.js
import Vuex from "vuex"
import { SOME_MUTATION } from "./mutation-types"

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 可以通過ES6的計算屬性命名特性去使用常量作為函式名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

mutation()必須是同步函式,因為devtool無法追蹤回撥函式中對state進行的非同步修改。

Vue元件可以使用this.$store.commit("xxx")提交mutation,或者使用mapMutations()將Vue元件中的methods對映為store.commit呼叫(需要在根節點注入store)。

import { mapMutations } from "vuex"

export default {
  methods: {
    ...mapMutations([
      "increment" // 對映this.increment()為this.$store.commit("increment")
    ]),
    ...mapMutations({
      add: "increment" // 對映this.add()為this.$store.commit("increment")
    })
  }
}

Actions

Action用來提交mutation,且Action中可以包含非同步操作。Action函式接受一個與store例項具有相同方法和屬性的context物件,因此可以通過呼叫context.commit提交一個mutation,或者通過context.statecontext.getters來獲取state、getters。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit("increment")
    }
  }
})

生產環境下,可以通過ES6的解構引數來簡化程式碼。

actions: {
  // 直接向action傳遞commit方法
  increment ({ commit }) {
    commit("increment")
  }
}

Action通過store.dispatch()方法進行分發,mutation當中只能進行同步操作,而action內部可以進行非同步的操作。下面是一個購物車的例子,程式碼中分發了多個mutations,並進行了非同步API操作。

actions: {
  checkout ({ commit, state }, products) {

    const savedCartItems = [...state.cart.added]  // 把當前購物車的物品備份起來
    commit(types.CHECKOUT_REQUEST)             // 發出結賬請求,然後清空購物車
    // 購物Promise分別接收成功和失敗的回撥
    shop.buyProducts(
      products,
      () => commit(types.CHECKOUT_SUCCESS),                  // 成功操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)   // 失敗操作
    )
  }
}

元件中可以使用this.$store.dispatch("xxx")分發action,或者使用mapActions()將元件的methods對映為store.dispatch需要在根節點注入store)。

import { mapActions } from "vuex"

export default {
  methods: {
    ...mapActions([
      "increment"  // 對映this.increment()為this.$store.dispatch("increment")
    ]),
    ...mapActions({
      add: "increment"  // 對映this.add()為this.$store.dispatch("increment")
    })
  }
}

store.dispatch可以處理action回撥函式當中返回的Promise,並且store.dispatch本身仍然返回一個Promise

actions: {
  // 定義一個返回Promise物件的actionA
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit("someMutation") // 觸發mutation
        resolve()
      }, 1000)
    })
  },
  // 也可以在actionB中分發actionA
  actionB ({ dispatch, commit }) {
    return dispatch("actionA").then(() => {
      commit("someOtherMutation") // 觸發另外一個mutation
    })
  }
}

// 現在可以分發actionA
store.dispatch("actionA").then(() => {
  ... ... ...
})

可以體驗通過ES7的非同步處理特性async/await來組合action。

actions: {
  async actionA ({ commit }) {
    commit("gotData", await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch("actionA") //等待actionA完成
    commit("gotOtherData", await getOtherData())
  }
}

Module

整個應用使用單一狀態樹的情況下,所有state都會集中到一個store物件,因此store可能變得非常臃腫。因此,Vuex允許將store切割成模組(module),每個模組擁有自己的statemutationactiongetter、甚至是巢狀的子模組。

const moduleA = {
  state: {},
  mutations: {},
  actions: {},
  getters: {}
}

const moduleB = {
  state: {},
  mutations: {},
  actions: {}
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // moduleA的狀態
store.state.b // moduleB的狀態

module內部的mutations()getters()接收的第1個引數是模組的區域性狀態物件。

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      state.count++ // 這裡的state是模組的區域性狀態
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

模組內部action當中,可以通過context.state獲取區域性狀態,以及context.rootState獲取全域性狀態。

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit("increment")
      }
    }
  }
}

模組內部的getters()方法,可以通過其第3個引數接收到全域性狀態。

const moduleA = {
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

嚴格模式

嚴格模式下,如果state變化不是由mutation()函式引起,將會丟擲錯誤。只需要在建立store的時候傳入strict: true即可開啟嚴格模式。

const store = new Vuex.Store({
  strict: true
})

不要在生產環境下啟用嚴格模式,因為它會深度檢測不合法的state變化,從而造成不必要的效能損失,我們可以通過在構建工具中增加如下判斷避免這種情況。

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

嚴格模式下,在屬於Vuex的state上使用v-model指令會丟擲錯誤,此時需要手動繫結value並監聽input、change事件,並在事件回撥中手動提交action。另外一種實現方式是直接重寫計算屬性的get和set方法。

總結

  1. 應用層級的狀態應該集中到單個store物件中。
  2. 提交mutation是更改狀態的唯一方法,並且這個過程是同步的。
  3. 非同步邏輯都應該封裝到action裡面。

Webpack Vue Loader

vue-loader是由Vue開源社群提供的Webpack載入器,用來將.vue字尾的單檔案元件轉換為JavaScript模組,每個.vue單檔案元件可以包括以下部分:

  1. 一個<template>
  2. 一個<script>
  3. 多個<style>
<template>只能有1個</template>

<script>只能有1個</script>

<style>可以有多個</style>
<style>可以有多個</style>
<style>可以有多個</style>

CSS作用域

.vue單檔案元件的<style>標籤上新增scoped屬性,可以讓該<style>標籤中的樣式只作用於當前元件。使用scoped時,樣式選擇器儘量使用class或者id,以提升頁面渲染效能。

<!-- 單檔案元件定義 -->
<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">Hank</div>
</template>

<!-- 轉換結果 -->
<style>
.example[data-v-f3f3eg9] {
  color: blue;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>Hank</div>
</template>

可以在一個元件中同時使用帶scoped屬性和不帶該屬性的<style/>,分別用來定義元件私有樣式和全域性樣式。

CSS模組化

在單檔案元件.vue<style>標籤上新增module屬性即可開啟CSS模組化特性。CSS Modules用於模組化組合CSS,vue-loader已經整合了CSS模組化特性。

<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>

CSS模組會向Vue元件中注入名為$style計算屬性,從而實現在元件的<template/>中使用動態的class屬性進行繫結。

<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>

動畫

Vue在插入、更新、移除DOM的時候,提供瞭如下幾種方式去展現進入(entering)和離開(leaving)的過渡效果。

  1. 在CSS過渡和動畫中應用class。
  2. 鉤子過渡函式中直接操作DOM。
  3. 使用CSS、JavaScript動畫庫,如Animate.cssVelocity.js

transition元件

Vue提供了內建元件<transition/>來為HTML元素、Vue元件新增過渡動畫效果,可以在條件展示使用v-ifv-show)、動態元件展示元件根節點的情況下進行渲染。<transition/>主要用來處理單個節點,或者同時渲染多個節點當中的一個。

自動切換的class類名

在元件或HTML進入(entering)和離開(leaving)的過渡效果當中,Vue將會自動切換並應用下圖中的六種class類名。

5815514-42f3a18523da9f5f.png
transition.png

可以使用<transition/>name屬性來自動生成過渡class類名,例如下面例子中的name: "fade"將自動擴充為.fade-enter.fade-enter-active等,name屬性預設的情況下預設類名為v

<div id="demo">
  <button v-on:click="show = !show"> Toggle </button>
  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>

<script>
new Vue({
  el: "#demo",
  data: {
    show: true
  }
})
</script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s
}
.fade-enter, .fade-leave-to {
  opacity: 0
}
</style>

自定義CSS類名

結合Animate.css使用時,可以在<transition/>當中通過以下屬性自定義class類名。

<transition
  enter-class = "animated"
  enter-active-class = "animated"
  enter-to-class = "animated"
  leave-class = "animated"
  leave-active-class = "animated"
  leave-to-class = "animated">
</transition>

自定義JavaScript鉤子

結合Velocity.js使用時,通過v-on在屬性中設定鉤子函式。

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"
  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled">
</transition>

<script>
// ...
methods: {
  beforeEnter: function (el) {},
  enter: function (el, done) { done() },
  afterEnter: function (el) {},
  enterCancelled: function (el) {},
  beforeLeave: function (el) {},
  leave: function (el, done) { done() },
  afterLeave: function (el) {},
  leaveCancelled: function (el) {} // 僅用於v-show
}
</script>

顯式設定過渡持續時間

可以使用<transition>上的duration屬性設定一個以毫秒為單位的顯式過渡持續時間。

<transition :duration="1000"> Hank </transition>

<!-- 可以分別定製進入、移出的持續時間 -->
<transition :duration="{ enter: 500, leave: 800 }"> Hank </transition>

元件首次渲染時的過渡

通過<transition>上的appear屬性設定元件節點首次被渲染時的過渡動畫。

<!-- 自定義CSS類名 -->
<transition
  appear
  appear-class="custom-appear-class"
  appear-to-class="custom-appear-to-class"
  appear-active-class="custom-appear-active-class">
</transition>

<!-- 自定義JavaScript鉤子 -->
<transition
  appear
  v-on:before-appear="customBeforeAppearHook"
  v-on:appear="customAppearHook"
  v-on:after-appear="customAfterAppearHook"
  v-on:appear-cancelled="customAppearCancelledHook">
</transition>

HTML元素的過渡效果

Vue元件的key屬性

key屬性主要用在Vue虛擬DOM演算法中去區分新舊VNodes,不顯式使用key的時候,Vue會使用效能最優的自動比較演算法。顯式的使用key,則會基於key的變化重新排列元素順序,並移除不存在key的元素。具有相同父元素的子元素必須有獨特的key,因為重複的key會造成渲染錯誤。

<ul>
  <!-- 最常見的用法是在使用v-for的時候 -->
  <li v-for="item in items" :key="item.id">...</li>
</ul>

元素的的交替過渡

可以通過Vue提供的v-ifv-else屬性來實現多元件的交替過渡,最常見的過渡效果是一個列表以及描述列表為空時的訊息。

<transition>
  <table v-if="items.length > 0">
    <!-- ... -->
  </table>
  <p v-else>Sorry, no items found.</p>
</transition>

Vue中具有相同名稱的元素切換時,需要通過關鍵字key作為標記進行區分,否則Vue出於效率的考慮只會替換相同標籤內的內容,因此為<transition>元件中的同名元素設定key是一個最佳實踐

<transition>
  <button v-if="isEditing" key="save"> Save </button>
  <button v-else key="edit"> Edit </button>
</transition>

一些場景中,可以通過給相同HTML元素的key屬性設定不同的狀態來代替冗長的v-ifv-else

<!-- 通過v-if和v-else來實現 -->
<transition>
  <button v-if="isEditing" key="save"> Save </button>
  <button v-else key="edit"> Edit </button>
</transition>

<!-- 設定動態的key屬性來實現 -->
<transition>
  <button v-bind:key="isEditing"> {{isEditing ? "Save" : "Edit"}} </button>
</transition>

而對於使用了多個v-if的多元素過渡,也可以通過動態的key屬性進行大幅度的簡化。

<!-- 多個v-if實現的多元素過渡 -->
<transition>
  <button v-if="docState === "saved"" key="saved"> Edit </button>
  <button v-if="docState === "edited"" key="edited"> Save </button>
  <button v-if="docState === "editing"" key="editing"> Cancel </button>
</transition>

<!-- 通過動態key屬性可以大幅簡化模板程式碼 -->
<transition>
  <button v-bind:key="docState"> {{ buttonMessage }} </button>
</transition>

<script>
...
computed: {
  buttonMessage: function () {
    switch (this.docState) {
      case "saved": return "Edit"
      case "edited": return "Save"
      case "editing": return "Cancel"
    }
  }
}
</script>

Vue元件的過渡效果

多個Vue元件之間的過渡不需要使用key屬性,只需要使用動態元件即可。

<transition name="component-fade" mode="out-in">
  <component v-bind:is="view"></component>
</transition>

<script>
new Vue({
  el: "#transition-components-demo",
  data: {
    view: "v-a"
  },
  components: {
    "v-a": {
      template: "<div>Component A</div>"
    },
    "v-b": {
      template: "<div>Component B</div>"
    }
  }
})
<script>

<style>
.component-fade-enter-active, .component-fade-leave-active {
  transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to {
  opacity: 0;
}
<style>

選擇HTML元素或Vue元件的過渡模式

<transition>的預設進入(enter)和離開(leave)行為同時發生,所以當多個需要切換顯示的HTML元素或Vue元件處於相同位置的時候,這種同時生效的進入和離開過渡不能滿足所有需求,Vue可以通過<transition-gruop>元件的mode屬性來選擇如下過渡模式。

  • in-out:新元素先進行過渡,完成之後當前顯示的元素再過渡離開。
  • out-in:當前顯示的元素先進行過渡,完成之後新元素再過渡進入。
<transition name="fade" mode="out-in">
  <button v-if="docState === "saved"" key="saved"> Edit </button>
  <button v-if="docState === "edited"" key="edited"> Save </button>
  <button v-if="docState === "editing"" key="editing"> Cancel </button>
</transition>

transition-group元件

<transition-group>用來設定多個HTML元素或Vue元件的過渡效果,不同於<transition>,該元件預設會被渲染為一個真實的<span>元素,但是開發人員也可以通過<transition-group>元件的tag屬性更換為其它合法的HTML元素。<transition-group>元件內部的元素必須要提供唯一的key屬性值。

<div id="list-demo" class="demo">
  <button v-on:click="add">Add</button>
  <button v-on:click="remove">Remove</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
</div>

<script>
new Vue({
  el: "#list-demo",
  data: {
    items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
    nextNum: 10
  },
  methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove: function () {
      this.items.splice(this.randomIndex(), 1)
    },
  }
})
</script>

<style>
.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter, .list-leave-to {
  opacity: 0;
  transform: translateY(30px);
}
</style>

<transition-group>實現的列表過渡效果在新增、移除某個HTML元素時,相臨的其它HTML元素會瞬間移動至新位置,這個過程並非平滑的過渡。為解決這個問題,<transition-group>提供v-move特性去覆蓋移動過渡期間所使用的CSS類名。開啟該特性,即可以通過name屬性手動設定(下面例子中的name="flip-list".flip-list-move),也可以直接使用move-class屬性。

<div id="flip-list-demo" class="demo">
  <button v-on:click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" v-bind:key="item">
      {{ item }}
    </li>
  </transition-group>
</div>

<script>
new Vue({
  el: "#flip-list-demo",
  data: {
    items: [1,2,3,4,5,6,7,8,9]
  },
  methods: {
    shuffle: function () {
      this.items = _.shuffle(this.items)
    }
  }
})
</script>

<style>
.flip-list-move {
  transition: transform 1s;
}
<style>

可以通過響應式的繫結<transition><transition-gruop>上的name屬性,從而能夠根據元件自身的狀態實現具有動態性的過渡效果。

<transition v-bind:name="transitionName"></transition>