Vue中的scope slot和render函数

July 05, 2020

在Vue中使用slot-scope和render函数都可以做到自定义渲染,其实二者有很多相通之处,本文将通过几个例子来分析slot-scope和render 的灵活应用。

代码背景

本文一个需要实现自定义表格,每列的内容都希望可以自定义渲染,在业务组件business.vue中通过column传给给Grid组件一些配置,就可以实现自定义渲染。

为了文章篇幅,本文代码均为精简代码,只是为了演示最核心的内容。

实现方案

render 函数

// Render.js
export default {
  functional: true,
  props: {
    row: Object,
    column: Object,
    rowIndx: Number,
    columnIndx: Number,
    render: Function
  },
  render(h, ctx) {
    const params = {
      row: ctx.props.row,
      column: ctx.props.column,
      rowIndex: ctx.props.rowIndex,
      columnIndex: ctx.props.columnIndx
    }
    return ctx.props.render(h, params)
  }
}

Grid组件columns prop中可以接收render函数,将rende函数r传递给render组件。

<!-- Grid.vue -->
<template>
<div>
  <tbody>
    <tr v-for="(row, rowIndex) in data">
      <td v-for="(col, colIndex) in columns">
        <template v-if="'render' in col">
          <Render :render="col.render" :row="row" :column="col" :rowIndex="rowIndex" :columnIndex="colIndex" />
        </template>
      </td>
    </tr>
  </tbody>
</div>
</template>

在使用的时候配置render项

<template>
	<div>
    <Grid :columns="columns" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          title: 'name',
          render: (h, { row, column, rowIndex, columnIndex }) =>
            h('span', `${rowIndex}: ${row.name}`)
        }
      ]
    }
  }
}
</script>

slot-scope

在组件内支持配置slot,如果slot有值的话,则使用slot渲染

<!-- Grid.vue -->
<template>
<div>
  <tbody>
    <tr v-for="(row, rowIndex) in data">
      <td v-for="(col, colIndex) in columns">
        <template v-if="'slot' in col">
          <slot :name="col.slot" :row="row" :column="col" :rowIndex="rowIndex" :columnIndex="colIndex" />
        </template>
      </td>
    </tr>
  </tbody>
</div>
</template>

在使用Grid组件的时候,通过slot选项实现自定义渲染

<!-- business.vue -->
<template>
	<div>
    <Grid :colomn="columns">
    	<template v-slot:name="{ row }">
        <input type="text" v-model="editName" />
        <span v-else>{{ row.name }}</span>
      </template>

      <template v-slot:age="{ row }">
        <input type="text" v-model="editAge" />
        <span v-else>{{ row.age }}</span>
      </template>
    </Grid>
  </div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          text: 'name',
          slot: 'name'
        },
        {
          text: age,
          slot: 'age'
        }
      ]
    }
  }
}
</script>

slot -> render 使用上转化

有得时候Grid组件并不支持slot选项,仅仅支持render函数,但是我们又想通过scope-slot的方式来使用。

<!-- Grid.vue -->
<template>
  <div>
    <tbody>
      <tr v-for="(row, rowIndex) in data">
        <td v-for="(col, colIndex) in columns">
          <template v-if="'render' in col">
            <Render :render="col.render" :row="row" :column="col" :rowIndex="rowIndex" :columnIndex="colIndex" />
          </template>
        </td>
      </tr>
    </tbody>
  </div>
</template>

可以在使用Grid组件的时候将插槽转换为render函数。

<!-- business.vue -->
<template>
<div>
  <Grid :colomn="columns" ref="grid">
    <template v-slot:name="{ row }">
      <input type="text" v-model="editName" />
      <span v-else>{{ row.name }}</span>
    </template>

    <template v-slot:age="{ row, index }">
      <input type="text" v-model="editAge" />
      <span v-else>{{ row.age }}</span>
    </template>
  </Grid>
</div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          text: 'name',
          render: (h, { row, column, rowIndex, columnIndex} ) => {
            return this.$refs.grid.$scopeSlots.name({
              row,
              column,
              rowIndex.
              columnIndex
            })
          }
        },
        {
          text: 'age',
          render: (h, { row, column, rowIndex, columnIndex} ) => {
            return this.$refs.grid.$scopeSlots.name({
              row,
              column,
              rowIndex.
              columnIndex
            })
          }
        }
      ]
    }
  }
}
</script>

这里将Grid组件的scope-slots通过组件实例的$scopeSlots选项取到(本质上是一个函数),然后在business中的render函数中返回调用结果,尽管Grid组件不支持插槽,但是我们在使用Grid的时候,可以在模板中写插槽(仅仅写是不会生效的),然后将插槽转化为render函数。

slot-> render组件中转化

其实,组件内部支持scope-slot不仅仅可以通过slot标签来实现,还可以用下面这种方式来实现

// SlotRender.js
export default {
  functional: true,
  inject: ['Grid'],
  props: {
    row: Object,
    column: Object,
    rowIndex: Number,
    columnIndex: Number
  },
  render: (h, ctx) =>
  	return h('div', ctx.injection.Grid.$scopeSlots[ctx.props.column.slot]({
          row: ctx.props.row,
          column: ctx.props.column,
      		rowIndex: ctx.props.rowIndex,
      		columnIndex: ctx.props.columnIndex
        }))
}
<!-- Grid.vue -->
<template>
  <div>
    <tbody>
      <tr v-for="(row, rowIndex) in data">
        <td v-for="(col, colIndex) in columns">
          <template v-if="'slot' in col">
            <SlotRender :row="row" :column="col" :rowIndex="rowIndex" :columnIndex="colIndex" />
          </template>
        </td>
      </tr>
    </tbody>
  </div>
</template>

<script>
export default {
  provide() {
    return {
      Grid: this
    }
  }
}
</script>

在使用上,仍然可以通过插槽

<!-- business.vue -->
<<template>
<div>
  <Grid :colomn="columns" ref="grid">
    <template v-slot:name="{ row }">
      <input type="text" v-model="editName" />
      <span v-else>{{ row.name }}</span>
    </template>

    <template v-slot:age="{ row }">
      <input type="text" v-model="editAge" />
      <span v-else>{{ row.age }}</span>
    </template>
  </Grid>
</div>
</template>

<script>
export default {
  data() {
    return {
      columns: [
        {
          text: 'name',
          slot: 'name'
        },
        {
          text: 'age',
          slot: 'age'
        }
      ]
    }
  }
}
</script>

这里要注意的是,$scopeSlots只能在组件实例上能够取到,所以上面的两个例子中,在上层组件获取Grid实例的方式是通过ref的方式,在Grid子组件中获取Grid实例的方式是通过provide/inject。

以上就是scopeSlots 和 render函数,其实最主要的是render函数和slot方式,另外两种方式能够更好地帮助你理解作用域插槽和render函数。

补充

强烈看看vue-promised源码来学习scoped-slots和render函数的妙用。


Profile picture

Written by Colgin who lives and works in China, focus on web development. You can comment on github