优雅的使用 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 会进一步更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容