Vue技巧:无渲染组件

2,433 阅读1分钟
原文链接: github.com

Vue 技巧:无渲染组件

最近,使用 Vue 开发组件时遇到了这样一个问题:开发的组件所能够自定义的 props 比较多,导致使用该组件时需要传入太多属性,数据、样式控制什么的属性都在一起了,看起来很不美观,像下面这样:

<some-component title="我是标题" :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" :data="testData" />

虽然看起来可能还好,但是实际使用时可以自定义的属性是不止这些的,这样使用起来就很不美观了。于是就有了这样一个想法:定义一个类似于 React 中常写的 theme 组件,将一些非数据相关的 props 定义到 theme 组件上,theme 组件再自动将 props 透传给其他组件使用即可。theme 组件使用起来像这样:

<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
  <some-component title="我是标题":data="testData" />
</thmem>

开发 theme 组件

React 中,开发这样一个高阶组件 theme 是很简单的。但是在 vue 中如何开发 theme 组件以达到上面设想的使用效果?通过翻阅 Vue 的文档,发现借助 $slotsrender 函数可以做到。

export default {
  name: 'theme',

  render(h) {
    const theme = this.$attrs // 通过 $attrs 可以拿到使用该组件时定义的 props,而无需声明有哪些 props

    const merge = vNode => {
      if (!vNode.tag) {
        return
      }

      if (vNode.componentOptions) {
        let props = vNode.componentOptions.propsData
        props = Object.assign({}, theme, props)
        vNode.componentOptions.propsData = props
      } else {
        if (!vNode.data) {
          return
        }

        let attrs = vNode.data.attrs || {}
        attrs = Object.assign({}, theme, attrs)
        vNode.data.attrs = attrs
      }
    }

    this.$slots.default.map(vNode => merge(vNode))

    Object.keys(this.$attrs).forEach(key => {
      this.$attrs[key] = null
    })

    return this.$slots.default[0] // 直接返回,无需额外渲染
  }
}

如此便达到了这样的使用效果:

<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
  <some-component title="我是标题":data="testData" />
</thmem>

很显然 theme 组件是没有渲染的,它所做的也只不过是透传 props 给其它组件而已,称之为 无渲染组件

slot-scope

Vue文档 中提到了 slot-scope 可以使用作用域插槽变得更干净。那么结合 theme 组件的经验,可以写出这样一款 axios 组件。

Vue.component('s-axios', {
  props: ['url'],

  data() {
    return {
      loading: true,
      response: null
    }
  },

  created() {
    axios.get(this.url)
      .then(response => {
        this.loading = false
        this.response = response
      })
  },

  render() {
    return this.$scopedSlots.default({
      loading: this.loading,
      response: this.response
    })
  }
})

使用起来也很方便:

<div id="app">
  <s-axios url="https://api.github.com/orgs/reactjs/repos">
    <div slot-scope="{ loading, response }">
      <div v-if="loading">loading</div>
      <div v-else>响应数据为:${{ response.data }}</div>
    </div>
  </s-axios>
</div>

可以点击查看在线Demo

总结

通过 $slots$scopedSlots结合 render可以创造很多好玩的组件,比如本篇文章中说到的 无渲染组件 ,关键就在于使用者怎么想。