Skip to content
On this page

advanced-vue-component-design renderLess 组件分离公共行为

总结实用(项目里能马上用到的)的 vue 组件设计方法,让封装组件更加容易,代码更加好维护。

demos 预览

vue2 如何共享公共行为的?

  1. mixins -- 缺点太对,不建议使用

  2. renderLess 组件提供公共的业务逻辑

比如下面一个点击某个 div 外部的组件

html
<script>
  export default {
    name: 'onClickOutside',
    props: ['clickOutside'],
    mounted() {
      const listener = e => {
        if (e.target === this.$el || this.$el.contains(e.target)) {
          return
        }
        this.clickOutside()
      }

      document.addEventListener('click', listener)
      this.$once('hook:beforeDestroy', () => document.removeEventListener('click', listener))
    },
    render() {
      return this.$slots.default[0]
    },
  }
</script>

vue3 的写法

html
<script>
  import { getCurrentInstance, onMounted, onBeforeUnmount, ref, defineComponent } from 'vue'
  export default defineComponent({
    name: 'OnClickOutside',
    props: ['clickOutside'],
    setup(props, { emit, attrs, slots }) {
      const vm = getCurrentInstance()
      const listener = event => {
        const isClickInside = vm.subTree.children.some(element => {
          const el = element.el
          return event.target === el || el.contains(event.target)
        })
        if (isClickInside) {
          console.log('clickInside')
          return
        }
        props.clickOutside && props.clickOutside()
      }
      onMounted(() => {
        document.addEventListener('click', listener)
      })
      onBeforeUnmount(() => {
        document.removeEventListener('click', listener)
      })
      return () => slots.default()
    },
  })
</script>

vue3 中获取$el比较麻烦, 并且 vue3 的组合 api 提供了更加优雅的解决方案,后续把这个组件提取成组合函数。

再看一个使用 renderLess 组件封装业务逻辑的组件:

html
<script>
  // scoped-slot + render == 业务组件:实现共享业务逻辑
  export default {
    name: 'FetchData',
    props: ['url'],
    data() {
      return {
        data: null,
        loading: true,
      }
    },
    created() {
      fetch(this.url)
        .then(response => response.json())
        .then(json => {
          setTimeout(() => {
            this.json = json
            this.loading = false
          }, 2000)
        })
    },
    render() {
      return this.$scopedSlots.default({
        json: this.data,
        loading: this.loading,
      })
    },
  }
</script>

vue3 的写法:

js
export const FetchData = (props, { slots }) => {
  const data = { name: 'JackChou' }
  return [slots.left(data), slots.default(), slots.right()]
}

使用:

html
<FetchData>
  <template #left="{ name }">left,{{ name }}</template>
  <p>world</p>
  <template #right>right</template>
</FetchData>

使用函数组件封装公共逻辑的缺点是无法使用生命钩子,希望在 FetchData 中请求后台数据,然后在父组件布局展示。

html
<FetchData url="https://api.github.com/users/jackchoumine">
  <!--这里展示数据 -->
</FetchData>

优化后的组件:

js
import { defineComponent, onMounted, ref } from 'vue'

export const FetchData = defineComponent({
  props: ['url'],
  setup(props, { slots }) {
    const userInfo = ref({})
    const loading = ref(true)
    onMounted(() => {
      fetch(props.url)
        .then(res => res.json())
        .then(data => {
          console.log(data)
          userInfo.value = data
          loading.value = false
        })
    })
    return () => slots.default({ userInfo: userInfo.value, loading: loading.value })
  },
})

使用:

html
<FetchData url="https://api.github.com/users/jackchoumine">
  <template #default="{ userInfo, loading }">
    <p v-if="loading">正在加载...</p>
    <p v-else-if="userInfo.avatar_url">
      <img :src="userInfo.avatar_url" />
    </p>
    <div v-else>
      <h3>error</h3>
      <p>{{ userInfo.message }}</p>
    </div>
  </template>
</FetchData>

Released under the MIT License.