美文网首页
Vue2技术栈归纳与精粹(下篇)

Vue2技术栈归纳与精粹(下篇)

作者: uinika | 来源:发表于2017-11-20 01:20 被阅读0次

    Vuex状态管理

    Vuex是专门为Vue应用程序提供的状态管理模式,每个Vuex应用的核心是store仓库),即装载应用程序state状态)的容器,每个应用通常只拥有一个store实例。

    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 () {
        return this.$store.getters.doneTodos // 现在可以方便的在Vue组件使用store中定义的doneTodos
      }
    }
    

    Vuex提供的mapGetters()辅助函数将store中的getters映射到局部计算属性。

    import { mapGetters } from "vuex"
    
    export default {
      computed: {
        // 使用对象展开运算符将getters混入computed计算属性
        ...mapGetters([
          "doneTodosCount",
          doneCount: "doneTodosCount" // 映射store.getters.doneTodosCount到别名this.doneCount
        ])
      }
    }
    

    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类名。

    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>
    

    相关文章

      网友评论

          本文标题:Vue2技术栈归纳与精粹(下篇)

          本文链接:https://www.haomeiwen.com/subject/ylsmvxtx.html