优雅的使用 Vue

作者: KrisLeeSH | 来源:发表于2018-05-18 11:47 被阅读212次

    我们通常会说一个人的英文够不够地道,够不够 native,同样在使用 vue 的时候希望大家也能够更地道的书写 vue 代码。
    这可能需要我们抛弃一些思想,比如我认为 jQuery 中页面/组件的状态变更是事件驱动的,而 Vue/React 中则更多的是数据驱动,即 data/prop 的变化引起页面展示的变化。
    所以,请不要在 Vue 中带入其他框架的思想,一个典型的特例就是 滥用 watch 监听数据变化来生成新的数据。

    computed 衍生数据

    vue 官网的介绍中 computed 数据直译为“计算属性”,但从功能应用上个人觉得叫做“衍生数据”更为贴切。

    computed 中定义的数据大体有两种用法:

    1. 衍生,即通过 propsdata 与其他数据(如 import 导入的外部数据)的组合、运算生成新的数据对象
    2. 代理,即通过定义的 setget 方法实现通过衍生取数据,修改该数据时,映射/更新到他的衍生

    衍生示例

    {
      data () {
        return {
          menuList: [],  // 账户下的菜单列表,通过请求接口获得
        }
      },
      computed: {
        hasPagePermission () {  // 是否具备当前页面的权限
          const currentPath = this.$route.path
          return this.menuList.includes(currentPath)
        }
      }
    }
    

    例子中 hasPagePermission 数据即为衍生数据,路由变化时 vue 自动根据当前路由和用户的菜单列表 menuList 计算出用户是否有该路由的权限。

    代理示例

    {
      data () {
        return {
          firstName: '',
          familyName: ''
        }
      },
      computed: {
        fullName: {
          get () {
            return `${this.firstName} ${this.familyName}`
          },
          set (val) {
            const [ firstName, familyName ] = val.split(' ')
            this.firstName = firstName
            this.familyName = familyName
          }
        }
      }
    }
    

    例子来源于官网,这个不需要过多解释了。

    watch 数据监听

    注册数据变化的 callback,参数依次时新数据、旧数据。

    在对对象型数据监听时注意设置 deep 属性为 true 达到对象深层属性/值变化时能够触发回调。

    大多数你觉得需要用到 watch 的场景其实更适合用 computed,比如 代理示例 中,多数人会选择监听 $route 来更新 data 定义的 hasPagePermission,当然功能是可以实现的,但是不是那个味儿。

    建议在 数据驱动组件状态变化 时使用 watch。如 checkbox-group,当勾选了 Thanos 时,需要禁用掉 Doctor Strange、Scarlet Witch 等选项时,可以使用 watch 监听 checkbox-group 绑定的 v-model,在其 handler 中更新选项属性。这个思想可以理解为数据驱动型,当然如果你在 checkbox-groupchange 事件回调中修改选项属性也是可以的(事件驱动型),但个人更偏向于在 vue 中尽量使用数据驱动型的处理模式。

    v-model 实现自定义 input 组件

    v-model 其实是块语法糖:

    1. 通过 props.value 接收父组件的数据
    2. 通过 this.$emit('input', DATA) 抛出数据给父组件

    基于以上,我们可以实现自定义的 input 组件

    自定义 checkbox-group 组件

    {
      template: `
        <div class="checkbox-group">
          <label v-for="ele in choices" @click="handleCheck"><input type="checkbox" :checked="value.includes(ele)" :value="ele">{{ele}}</label>
        </div>
      `,
      
      props: {
        value: Array,
        choices: Array
      },
      
      methods: {
        handleCheck (e) {
          const currentOptionVal = e.target.value
          let result = []
          if (e.target.checked) {
            result = [ ...this.value, currentOptionVal ]
          } else {
            result = this.value.filter(ele => ele !== currentOptionVal)
          }
          this.$emit('input', result)
        }
      }
    }
    

    demo 及 源码 点击 这里

    mixin 混入

    混入,通过它可以抽离/封装公共逻辑,在需要时混入组件中就可以直接用其中的方法、数据了。
    可以理解为它是一个没有 template 的抽象组件。
    混入 mixin 后,组件中定义的数据/方法会覆盖掉 mixin 中定义的同名数据/方法。
    其逻辑有点像面向对象语言中的继承。
    合理使用 mixin 可以充分发扬程序员懒的美德。

    使用场景:多组件逻辑重复。
    比如,报表业务开发中多个页面都是筛选表单 + 展示表格 + 分页,附加上loading、导出等 feature,那么这部分功能就可以通过 mixin 进行封装抽离。

    示例

    const TableDataMixin = {
      /*
       * 表格数据展示/导出逻辑 mixin
       *
       * 使用时需要在 data/computed 中配置以下数据
       * 1. 数据请求 api/参数:loadDataApi / loadDataParam
       * 2. 数据导出 api/参数:exportDataApi / exportDataParam
       */
      data () {
        return {
          tableData: [],  // 表格数据
          tableLoading: false,  // loading 状态
          pagination: {  // 分页
            currentPage: 1,
            pageSize: 10,
            total: 0
          },
          tableExporting: false  // exporting 状态
        }
      },
      
      computed: {
        shouldDisableExport () {  // 是否需要禁用 export
          return this.tableLoading || this.tableExporting
        }
      },
      
      methods: {
        loadTableData () {
          this.tableLoading = true
          api.get(this.loadDataApi, this.loadDataParam)
            .then(({ data: { status, message, data: { data, total } } }) => {  // 解析 api 返回数据,项目中应该是统一格式的,如 { status, message, data }
                this.tableData = data
              this.tablePagination.total = total
              this.tableLoading = false
            })
            .catch(err => {
                console.error('load table data failed:', err)
              this.tableLoading = false
            })
        },
        
        exportTableData () {
          this.tableExporting = true
          api.get(this.exportDataApi, this.exportDataParam)
            .then(({ data: { status, message, data: fileURI } }) => {  // 解析 api 返回数据,项目中应该是统一格式的,如 { status, message, data }
                window.location.href = fileURI
              this.tableExporting = false
            })
            .catch(err => {
                console.error('export table data failed:', err)
              this.tableExporting = false
            })
        },
        
        handlePaginationChange ({ currentPage, pageSize }) {
          this.tbalePagination.currentPage = currentPage
          this.tbalePagination.pageSize = pageSize
        }
      }
    }
    

    有了以上 mixin 后就不需要在每个页面中写一遍获取/导出数据的逻辑了,只需混入后配置相关数据即可:

    const TablePage = Vue.component('my-page', {
      mixins: [ TableDataMixin ],
    
      data () {
        return {
          formData: {},
          loadDataApi: 'YOUR LOAD DATA API',
          exportDataApi: 'YOUR EXPORT DATA API'
        }
      },
      
      computed: {
        loadDataParam () {
          const { currentPage, pageSize } = this.tablePagination
          return Object.assign({ currentPage, pageSize }, this.formData, { /* some other params */ })
        },
        
        exportDataParam () {
          return Object.assign({}, this.formData, { /* some other params */ })
        }
      }
    })
    

    而对于页面中导出按钮点击回调、分页变化回调、筛选表单提交后进行的数据检索等都可以直接使用 TableDataMixin 中的 exportTableData | handlePaginationChange | loadTableData 函数,而表格数据读取、分页数据读写等也可以直接绑定 tableData | tablePagination 等。

    directive 自定义指令

    指令也是代码封装/复用的大杀器。
    详细介绍可以自行参考 vue 官方文档 自定义指令

    使用场景 不限于一些需要底层 dom 操作的情况,如任何 ui 框架中的 v-loading 等,或者官方文档中提到的 v-focus。
    这里以一个页标题为例,在 SPA 开发中,每个页面一般都具备一个单独的 document.title,在每个页面的挂载/更新钩子中设置 document.title 显然太繁琐,那么我们可以通过自定义指令解决:

    自定义页标题指令

    const Title = {
      inserted: function (el, binding, vnode, oldVnode) {
        const { value: title = 'DEFALT TITLE' } = binding
        document.title = title
      },
    
      update: function (el, binding, vnode, oldVnode) {
        const { value: title = 'DEFALT TITLE' } = binding
        document.title = title
      }
    }
    
    const Page = Vue.component('my-page', {
      directives: [ Title ],
    
      template: `
      <div class="my-page" v-title="My Page">
        <!-- page content -->
      </div>
      `
    })
    

    以上来源于个人开发中的一些反思总结,如有不同意见可以在评论中回复。
    后续开发中如果发现其他的 little tricks 会进一步更新。

    相关文章

      网友评论

      本文标题:优雅的使用 Vue

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