手记

优雅的使用 Vue

我们通常会说一个人的英文够不够地道,够不够 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 会进一步更新

作者:KrisLeeSH
链接:https://www.jianshu.com/p/e9ad533d536d


0人推荐
随时随地看视频
慕课网APP