優(yōu)雅的使用 Vue

我們通常會(huì)說一個(gè)人的英文夠不夠地道,夠不夠 native,同樣在使用 vue 的時(shí)候希望大家也能夠更地道的書寫 vue 代碼。
這可能需要我們拋棄一些思想,比如我認(rèn)為 jQuery 中頁面/組件的狀態(tài)變更是事件驅(qū)動(dòng)的,而 Vue/React 中則更多的是數(shù)據(jù)驅(qū)動(dòng),即 data/prop 的變化引起頁面展示的變化。
所以,請(qǐng)不要在 Vue 中帶入其他框架的思想,一個(gè)典型的特例就是 濫用 watch 監(jiān)聽數(shù)據(jù)變化來生成新的數(shù)據(jù)。

computed 衍生數(shù)據(jù)

vue 官網(wǎng)的介紹中 computed 數(shù)據(jù)直譯為“計(jì)算屬性”,但從功能應(yīng)用上個(gè)人覺得叫做“衍生數(shù)據(jù)”更為貼切。

computed 中定義的數(shù)據(jù)大體有兩種用法:

  1. 衍生,即通過 props、data 與其他數(shù)據(jù)(如 import 導(dǎo)入的外部數(shù)據(jù))的組合、運(yùn)算生成新的數(shù)據(jù)對(duì)象
  2. 代理,即通過定義的 set、get 方法實(shí)現(xiàn)通過衍生取數(shù)據(jù),修改該數(shù)據(jù)時(shí),映射/更新到他的衍生

衍生示例

{
  data () {
    return {
      menuList: [],  // 賬戶下的菜單列表,通過請(qǐng)求接口獲得
    }
  },
  computed: {
    hasPagePermission () {  // 是否具備當(dāng)前頁面的權(quán)限
      const currentPath = this.$route.path
      return this.menuList.includes(currentPath)
    }
  }
}

例子中 hasPagePermission 數(shù)據(jù)即為衍生數(shù)據(jù),路由變化時(shí) vue 自動(dòng)根據(jù)當(dāng)前路由和用戶的菜單列表 menuList 計(jì)算出用戶是否有該路由的權(quán)限。

代理示例

{
  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
      }
    }
  }
}

例子來源于官網(wǎng),這個(gè)不需要過多解釋了。

watch 數(shù)據(jù)監(jiān)聽

注冊(cè)數(shù)據(jù)變化的 callback,參數(shù)依次時(shí)新數(shù)據(jù)、舊數(shù)據(jù)。

在對(duì)對(duì)象型數(shù)據(jù)監(jiān)聽時(shí)注意設(shè)置 deep 屬性為 true 達(dá)到對(duì)象深層屬性/值變化時(shí)能夠觸發(fā)回調(diào)。

大多數(shù)你覺得需要用到 watch 的場景其實(shí)更適合用 computed,比如 代理示例 中,多數(shù)人會(huì)選擇監(jiān)聽 $route 來更新 data 定義的 hasPagePermission,當(dāng)然功能是可以實(shí)現(xiàn)的,但是不是那個(gè)味兒。

建議在 數(shù)據(jù)驅(qū)動(dòng)組件狀態(tài)變化 時(shí)使用 watch。如 checkbox-group,當(dāng)勾選了 Thanos 時(shí),需要禁用掉 Doctor Strange、Scarlet Witch 等選項(xiàng)時(shí),可以使用 watch 監(jiān)聽 checkbox-group 綁定的 v-model,在其 handler 中更新選項(xiàng)屬性。這個(gè)思想可以理解為數(shù)據(jù)驅(qū)動(dòng)型,當(dāng)然如果你在 checkbox-groupchange 事件回調(diào)中修改選項(xiàng)屬性也是可以的(事件驅(qū)動(dòng)型),但個(gè)人更偏向于在 vue 中盡量使用數(shù)據(jù)驅(qū)動(dòng)型的處理模式。

v-model 實(shí)現(xiàn)自定義 input 組件

v-model 其實(shí)是塊語法糖:

  1. 通過 props.value 接收父組件的數(shù)據(jù)
  2. 通過 this.$emit('input', DATA) 拋出數(shù)據(jù)給父組件

基于以上,我們可以實(shí)現(xiàn)自定義的 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 及 源碼 點(diǎn)擊 這里

mixin 混入

混入,通過它可以抽離/封裝公共邏輯,在需要時(shí)混入組件中就可以直接用其中的方法、數(shù)據(jù)了。
可以理解為它是一個(gè)沒有 template 的抽象組件。
混入 mixin 后,組件中定義的數(shù)據(jù)/方法會(huì)覆蓋掉 mixin 中定義的同名數(shù)據(jù)/方法。
其邏輯有點(diǎn)像面向?qū)ο笳Z言中的繼承。
合理使用 mixin 可以充分發(fā)揚(yáng)程序員懶的美德。

使用場景:多組件邏輯重復(fù)。
比如,報(bào)表業(yè)務(wù)開發(fā)中多個(gè)頁面都是篩選表單 + 展示表格 + 分頁,附加上loading、導(dǎo)出等 feature,那么這部分功能就可以通過 mixin 進(jìn)行封裝抽離。

示例

const TableDataMixin = {
  /*
   * 表格數(shù)據(jù)展示/導(dǎo)出邏輯 mixin
   *
   * 使用時(shí)需要在 data/computed 中配置以下數(shù)據(jù)
   * 1. 數(shù)據(jù)請(qǐng)求 api/參數(shù):loadDataApi / loadDataParam
   * 2. 數(shù)據(jù)導(dǎo)出 api/參數(shù):exportDataApi / exportDataParam
   */
  data () {
    return {
      tableData: [],  // 表格數(shù)據(jù)
      tableLoading: false,  // loading 狀態(tài)
      pagination: {  // 分頁
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      tableExporting: false  // exporting 狀態(tài)
    }
  },
  
  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 返回?cái)?shù)據(jù),項(xiàng)目中應(yīng)該是統(tǒng)一格式的,如 { 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 返回?cái)?shù)據(jù),項(xiàng)目中應(yīng)該是統(tǒng)一格式的,如 { 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 后就不需要在每個(gè)頁面中寫一遍獲取/導(dǎo)出數(shù)據(jù)的邏輯了,只需混入后配置相關(guān)數(shù)據(jù)即可:

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 */ })
    }
  }
})

而對(duì)于頁面中導(dǎo)出按鈕點(diǎn)擊回調(diào)、分頁變化回調(diào)、篩選表單提交后進(jìn)行的數(shù)據(jù)檢索等都可以直接使用 TableDataMixin 中的 exportTableData | handlePaginationChange | loadTableData 函數(shù),而表格數(shù)據(jù)讀取、分頁數(shù)據(jù)讀寫等也可以直接綁定 tableData | tablePagination 等。

directive 自定義指令

指令也是代碼封裝/復(fù)用的大殺器。
詳細(xì)介紹可以自行參考 vue 官方文檔 自定義指令。

使用場景 不限于一些需要底層 dom 操作的情況,如任何 ui 框架中的 v-loading 等,或者官方文檔中提到的 v-focus。
這里以一個(gè)頁標(biāo)題為例,在 SPA 開發(fā)中,每個(gè)頁面一般都具備一個(gè)單獨(dú)的 document.title,在每個(gè)頁面的掛載/更新鉤子中設(shè)置 document.title 顯然太繁瑣,那么我們可以通過自定義指令解決:

自定義頁標(biāo)題指令

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>
  `
})

以上來源于個(gè)人開發(fā)中的一些反思總結(jié),如有不同意見可以在評(píng)論中回復(fù)。
后續(xù)開發(fā)中如果發(fā)現(xiàn)其他的 little tricks 會(huì)進(jìn)一步更新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容