手把手教你玩转render函数「组件封装-dynamic-select」(下)

1,818 阅读3分钟

接上篇文章👉手把手教你玩转render函数「组件封装-dynamic-select」(上)

自定义slots

Select Slots提供了prefix/empty这两个内置插槽,所以我们基于el-select封闭的组件需要支持用户自定义插槽内容

配置项

先根据目前提供配置项实现组件基础功能

参数说明类型默认值
slots插槽Object{}
scopedSlots作用域插槽Object{}

render函数

DynamicSelect/select.js

import SlotContent from './slotContent'
export default {
  // ...忽略上篇文章定义过的配置项
  
  // 注册一个函数式组件用来渲染提供给组件外层插槽内容 
  components: { SlotContent },
  render(h) {
   // 配置插槽 => 用来渲染组件内置的作用域插槽
    const slots = Object.keys(self.slots).map(slotName => {
      return [h('slot-content', {
        props: {
          render: self.slots[slotName],
          data: self
        },
        slot: slotName,
        key: slotName
      })]
    })

  }
}

下面需要修改渲染el-select那块代码中createElement的第三个参数

createElement方法的第三个参数是当前节点的内容, 就类似于div -> 可以放多个span, 所以第三个参数包括:

  • 当前渲染元素(组件)的子节点
  • 当前渲染元素(组件)提供的内置作用域插槽(可以通过$scopeSlots识别到)
  • 当前渲染元素(组件)的插槽内容slot
return h('el-select', {
  class: self.className,
  staticClass: 'jl-full-line',
  ....
}, [Object.keys(this.$slots).map(s => this.$slots[s]), ...optionsVnode, ...slots])

通过上面我们就能将这三种类型的内容都渲染到el-select组件里面

DynamicSelect/select.js

export default {
  name: 'SlotContent',
  // 声明这是函数式组件
  functional: true,
  props: {
    render: {
      type: Function,
      require: true
    },
    data: Object
  },
  render: (h, ctx) => {
    return ctx.props.render(h, ctx.props.data)
  }
}

灵活使用render来高度自定义组件

<template>
  <DynamicSelect
    ref="DynamicSelect"
    :value.sync="projectId"
    v-bind="projectSelectOption"
    :parse-data="parseData"
    :formatter="formatterValue"
    :multiple="true"
    collapse-tags
    // 添加自定义slots配置
    :slots="customSlots"
  >
  </DynamicSelect>
</template>

<script>
export default {
  data() {
    return {
      customSlots: {
        prefix: this.prefixRender
      },
    }
  },
   methods: {
    prefixRender(h, vue) {
      // h => createElement
      // vue => DynamicSelect实例

      // 我们可以通h来渲染任意组件到这个自定义插槽里面
      return h('div', {
        staticClass: 'select-prefix',
      },
      [h('svg-icon', {
        props: {
          'icon-class': 'bug'
        },
      })])
    },
   }
}
</script>

自定义options

效果图💗

1.png

继续完善render函数

+ const { value, label, labelRender } = self.optionsProps
+ // 选项格式化显示
+ let labelRenderNode = null
+ if (labelRender && typeof labelRender === 'function') {
+   labelRenderNode = function(labelValue, op) {
+     return labelRender(h, labelValue, op)
+   }
+ }

// 渲染options
 const optionsVnode = self.optionsData.map((op, index) => {
    return [h('el-option', {
      attrs: {
        value: op[value],
        label: op[label],
        disabled: op.disabled
      },
      key: op.id || op.value // 绑定唯一key
+    } labelRenderNode ? labelRenderNode(op[label], op) : null)]
  })

formatter自定义options内容

<template>
  <DynamicSelect
    ...
    // 添加自定义slots配置
    :slots="customSlots"
    :props="props"
  >
  </DynamicSelect>
</template>


<script>
// 定义渲染options的props
const props = {
  label: 'proName',  
  value: 'id',
  children: 'children',
  labelRender(h, label, item) {
    // h => createElement
    // label => options的label
    // item => options的每一项, 也就是每一个项目对象
    return [h('span', {
      staticClass: 'left',
      domProps: {
        innerText: label
      }
    }),
    h('span', {
      staticClass: 'right',
      domProps: {
        // 右侧我们展示了项目id的前两位
        innerText: item?.id.substr(0, 2) || ''
      }
    })]
  }
}


export default {
  data() {
    return {
      customSlots: {
        prefix: this.prefixRender
      },
      // 通过Object.freeze冻结对象, 避免vue给对象每个成员加上getter/setter (也是个性能优化技巧)
      props: Object.freeze(props),
    }
  },
}
</script>

通过$slots给组件里面加个全选

<template>
  <DynamicSelect
    ...
  >
    <el-tag
      slot="selectAll"
      type="primary"
      class="select-tag-class"
      @click="handleSelectAll"
    >
      {{ selectAllLabel[selectAll] }}
    </el-tag>
  </DynamicSelect>
</template>


<script>
export default {
  data() {
    return {
      selectAllLabel: {
        false: '全选',
        true: '取消',
      },
      // 默认非全选
      selectAll: false,
    }
  },
  methods: {
    // 全选Or取消
    handleSelectAll() {
      this.selectAll = !this.selectAll
      const list = []

     
      if (this.selectAll) { // 全选

      } else { // 取消
        
      }
      // 通过$refs将内容
      this.$refs.DynamicSelect.newValue = list
    }
  }
}
</script>

这里就有个问题了, 我们是动态获取的options ,我$refs虽然可以引用DynamicSelect的optionsData属性, 但是我接口拉取需要时间, 假设我走详情页面进来我之前选中的全部,我要让选项全部选中怎么搞 , tip -> (我们项目选择全部的时候传给后端的是一个[]

为什么不把所有项目的id全传进去???

/*
  这里设计到一个动态响应的问题, 假设我当时新增单据的时候总共有4个项目ids: [A, B, C, D]

  后面我新增了两个项目 [E, F]
  但是我之前存给后端的还是4个项目的id,所以当时有个项目中就是这么搞的
*/

回到之前的问题,动态拉取的options我怎么及时响应呢? 👉发布订阅

发布订阅

很多种实现方式,这里用类来写

./DynamicSelect/Publish.js

// 发布订阅模式(解决options异步拉取数据的问题)
export default class Publish {
  constructor() {
    this.pond = []
  }

  on(callBack) {
    this.pond.push(callBack)
  }

  emit(name) { 
    // 传递回调名称,有匹配的就将回调执行
    const fn = this.pond.find(c => c.name === name)
    fn && fn()
  }
}

给之前的方法加个自定义事件

// 动态拉取选项数据
async getOptionsData() {
	this.$emit('getOptionsSuccess', this.optionsData)
}

监听这个自定事件

getOptionsSuccess(data) {
  // 外层存储一份options的引用地址
	this.selectOptions = data
}

完善handleSelectAll

export default {
import Publish from './DynamicSelect/Publish.js'
const subscribe = new Publish()
  methods: {
    handleSelectAll() {
      // 可以方法内部往事件池里push回调,也可以在调用方法push(看执行时机)	
      this.$nextTick(() => {
        this.selectAll = !this.selectAll
        const list = []
        if (this.selectAll) { // 全选
					list = this.selectOptions.map(i => i.id)
        } else { // 取消
					list = []
        }
        // 主动将组件全选 -> 
        // $refs引用到组件的newValue直接赋值
        // 直接变动value 推荐这个
        this.porjectId = list
      })
    }
  }
}

至此组件封装就完事了,还有远程搜索那块没有写上来,发现写这种文章不太好写,代码太多又怕读者看起来发懵,不上代码干讲也感觉很难讲明白。比较难权衡,这也是我之前文章【以模块化的思想开发中后台项目】迟迟没有后续更新的原因,不过我也一直在学习这方面,这个系列后续还是会更的。

tip: 本组件用render函数封闭并不是因为有多高级,render函数允许我们通过写js的方式来构建组件,相较于模板语法确实灵活不少,但是也需要付出相应的代价

很多Vue提供的语法糖是用不了的,需要自己去实现,所以需要根据组件功能各自取舍

组件地址👉github.com/it-beige/bl…

写在最后

如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

往期文章

【建议追更】以模块化的思想来搭建中后台项目

【以模块化的思想开发中后台项目】第一章

【前端体系】从一道面试题谈谈对EventLoop的理解(更新了四道进阶题的解析)

【前端体系】从地基开始打造一座万丈高楼

【前端体系】正则在开发中的应用场景可不只是规则校验

「函数式编程的实用场景 | 掘金技术征文-双节特别篇」

【建议收藏】css晦涩难懂的点都在这啦