手把手教你用jsx封裝Vue中的複雜組件(網易雲音樂實戰項目需求)

背景介紹

最近在作vue高仿網易雲音樂的項目,在作的過程當中發現音樂表格這個組件會被很是多的地方複用,並且需求比較複雜的和靈活。vue

預覽地址

源碼地址

圖片預覽

  • 歌單詳情
    歌單詳情
  • 播放列表
    播放列表
  • 搜索高亮
    搜索高亮

需求分析

它須要支持:node

  • hideColumns參數, 自定義須要隱藏哪些列。git

  • highLightText,傳入字符串,數據中命中的字符串高亮。github

首先 看一下咱們日常的table寫法。element-ui

<el-table
      :data="tableData"
      style="width: 100%">
      <el-table-column
        prop="index"
        label=" "
        width="180">
      </el-table-column>
      <el-table-column
        prop="name"
        label="音樂標題"
        width="180">
      </el-table-column>
      <el-table-column
        prop="artistsText"
        label="歌手">
      </el-table-column>
    </el-table>
複製代碼

這是官網的寫法,假設咱們傳入了 hideColumns: ['index', 'name'],咱們須要在模板裏隱藏的話```bash

<el-table
    :data="tableData"
    style="width: 100%">
    <el-table-column
  +++ v-if="!hideColumns.includes('index')"
      prop="index"
      label=" "
      width="180">
    </el-table-column>
    <el-table-column
  +++ v-if="!hideColumns.includes('name')"
      prop="name"
      label="音樂標題"
      width="180">
    </el-table-column>
    <el-table-column
  +++ v-if="!hideColumns.includes('address')"
      prop="artistsText"
      label="歌手">
    </el-table-column>
  </el-table>
複製代碼

這種代碼很是笨,因此咱們確定是接受不了的,咱們很天然的聯想到日常用v-for循環,能不能套用在這個需求上呢。 首先在data裏定義columnsbabel

data() {
    return {
      columns: [{
        prop: 'index',
        label: '',
        width: '50'
      }, {
        prop: 'artistsText',
        label: '歌手'
      }, {
        prop: 'albumName',
        label: '專輯'
      }, {
        prop: 'durationSecond',
        label: '時長',
        width: '100',
      }]
    }
}
複製代碼

而後咱們在computed中計算hideColumns作一次合併ide

computed: {
    showColumns() {
      const { hideColumns } = this
      return this.columns.filter(column => {
        return !this.hideColumns.find((prop) => prop === column.prop)
      })
    },
  },
複製代碼

那麼模板裏咱們就能夠簡寫成post

<el-table
    :data="songs"
  >
    <template v-for="(column, index) in showColumns">
      <el-table-column
        :key="index"
        // 混入屬性
        v-bind="column"
      >
      </el-table-column>
    </template>
  </el-table>
複製代碼

注意v-bind="column"這行, 至關於把column中的全部屬性混入到table-column中去,是一個很是簡便的方法。ui

script配合template的解決方案

這樣需求看似解決了,很美好。
可是咱們忘了很是重要的一點,slotScopes這個東西!

好比音樂時長咱們須要format一下,

<el-table-column>
     <template>
        <span>{{ $utils.formatTime(scope.row.durationSecond) }}</span>
     </template>
 </el-table-column>
複製代碼

可是咱們如今把columns都寫到script裏了,和template分離開來了,我暫時還不知道有什麼方法能把sciprt裏寫的模板放到template裏用,因此先想到一個能夠解決問題的方法。就是在template里加一些判斷。

<el-table
    v-bind="$attrs"
    v-if="songs.length"
    :data="songs"
    @row-click="onRowClick"
    :cell-class-name="tableCellClassName"
    style="width: 99.9%"
  >
    <template v-for="(column, index) in showColumns">
      <!-- 須要自定義渲染的列 -->
      <el-table-column
        v-if="['durationSecond'].includes(column.prop)"
        :key="index"
        v-bind="column"
      >
          <!-- 時長 -->
          <template v-else-if="column.prop === 'durationSecond'">
            <span>{{ $utils.formatTime(scope.row.durationSecond) }}</span>
          </template>
      </el-table-column>

      <!-- 普通列 -->
      <el-table-column
        v-else
        :key="index"
        v-bind="column"
      >
      </el-table-column>
    </template>
  </el-table>
複製代碼

又一次的需求看似解決了,很美好。

高亮文字匹配需求分析


可是新需求又來了!!根據傳入的 highLightText 去高亮某些文字,咱們分析一下需求

雞你太美這個歌名,咱們在搜索框輸入雞你 咱們須要把

<span>雞你太美</span>
複製代碼

轉化爲

<span>
    <span class="high-light">雞你</span>
    太美
 </span>
複製代碼

咱們在template裏找到音樂標題這行,寫下這端代碼:

<template v-else-if="column.prop === 'name'">
  <span>{{this.genHighlight(scope.row.name)}}</span>
</template>
複製代碼
methods: {
    genHighlight(text) {
       return <span>xxx</span>
    }
}
複製代碼

我發現無從下手了, 由於jsx最終編譯成的是return vnode的方法,genHighlight執行之後返回的是vnode,可是你不能直接把vnode放到template裏去。

jsx終極解決方案

因此咱們要統一環境,直接使用jsx渲染咱們的組件,文檔能夠參照
babel-plugin-transform-vue-jsx
vuejs/jsx

import ElTable from 'element-ui/lib/table'

data() {
    const commonHighLightSlotScopes = {
      scopedSlots: {
        default: (scope) => {
          return (
            <span>{this.genHighlight(scope.row[scope.column.property])}</span>
          )
        }
      }
    }
    return {
      columns: [{
        prop: 'name',
        label: '音樂標題',
        ...commonHighLightSlotScopes
      }, {
        prop: 'artistsText',
        label: '歌手',
         ...commonHighLightSlotScopes
      }, {
        prop: 'albumName',
        label: '專輯',
        ...commonHighLightSlotScopes
      }, {
        prop: 'durationSecond',
        label: '時長',
        width: '100',
        scopedSlots: {
          default: (scope) => {
            return (
              <span>{this.$utils.formatTime(scope.row.durationSecond)}</span>
            )
          }
        }
      }]
    }
  },
  methods: {
    genHighlight(title = '') {
      ...省去一些細節
      const titleSpan = matchIndex > -1 ? (
        <span>
          {beforeStr}
          <span class="high-light-text">{hitStr}</span>
          {afterStr}
        </span>
      ) : title;
      return titleSpan;
    },
  },
 render() {
    const elTableProps = ElTable.props
    // 從$attrs裏提取做爲prop的值
    // 這裏要注意的點是駝峯命名法(camelCase)和短橫線命名法(kebab-case)
    // 都是能夠被組件接受的,雖然elTable裏的prop定義的屬性叫cellClassName
    // 可是咱們要把cell-class-name也解析進prop裏
    const { props, attrs } = genPropsAndAttrs(this.$attrs, elTableProps)
    
    const tableAttrs = {
      attrs,
      on: {
        ...this.$listeners,
        ['row-click']: this.onRowClick,
      },
      props: {
        ...props,
        cellClassName: this.tableCellClassName,
        data: this.songs,
      },
      style: { width: '99.9%' }
    }
    return this.songs.length ? (
      <el-table
        {...tableAttrs}
      >
        {this.showColumns.map((column, index) => {
          const { scopedSlots, ...columnProps } = column
          return (
            <el-table-column key={index} props={columnProps} scopedSlots={scopedSlots} >
            </el-table-column>
          )
        })}
      </el-table>
    ) : null
  }
複製代碼

attrs: this.$attrs 注意這句話,咱們在template裏能夠經過 v-bind="$attrs"去透傳外部傳進來的全部屬性,
可是在jsx中咱們必須分類清楚傳給el-table的attrsprops
好比el-table接受data這個prop,若是你放在attrs裏傳進去,那麼就失效了。

這個我暫時也沒找到特別好的解決方法,只能先引用去拿elTable上的props去進行比對$attrs,取交集。

import ElTable from 'element-ui/lib/table'
// 從$attrs裏提取做爲prop的值
// 這裏要注意的點是駝峯命名法(camelCase)和短橫線命名法(kebab-case)
// 都是能夠被組件接受的,雖然elTable裏的prop定義的屬性叫cellClassName
// 可是咱們要把cell-class-name也解析進prop裏
const { props, attrs } = genPropsAndAttrs(this.$attrs, elTableProps)
複製代碼

最終代碼中模板的部分少了不少重複的判斷,維護性和擴展性都更強了,jsx能夠說是複雜組件的終極解決方案。

相關文章
相關標籤/搜索