vue高级特性:render函数封装动态select(上)|小册免费学__Vue.js
发布于 3 年前 作者 banyungong 1845 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

theme: juejin highlight: atelier-estuary-light

先来看下效果👇

1.gif

上面展示了select组件的两种很常见的使用常见

  • 1.表格的过滤(根据项目过滤数据)
  • 2.表单的使用(基于项目来创建单据)

组件用例

先给份用例了解下组件大致提供那些功能

  • 支持格式化选项数据
  • 自定义事件回调listeners
  • 插入组件提供的作用域插槽
<template>
  <DynamicSelect type="select" v-model="value" v-bind="config">
  </DynamicSelect>
   <div>选中的值:{{value}}</div>
</template>

<script>
  import '@/components/FormSelect'
  export default {
    data() {
      return {
        value: '',
        config: {
          clearable: true,
          filterable: true,
          options: [
            {
                name: '选项一',
                id: '1',
                number: 'FFF1'
            },
            {
                name: '选项二',
                id: '2',
                number: 'SSS2'
            },
            {
                name: '选项三',
                id: '3',
                number: 'TTT3',
                disabled: true
            },
            {
                name: '选项四',
                id: 4,
                number: 'LLL4'
            } 
          ],
          props: {
            label: 'name',
            value: 'id',
            formatter: (val, op) => {
              return `${val} ${op.number}`
            }
          },
          multiple: true,
          listeners: {
            change: (val) => {console.log('change ===> ', val)},
            dataChange: (val) => {console.log('dataChange ===> ', val)},
          },
          slots: {
            prefix: (h) => (<i class='el-icon-edit el-input__icon' />)
          }           
        }
      }
    },
  }
</script>

列表过滤使用

对于列表根据项目过滤很多地方都用到,我们可以选择定义一个mixin文件来简化每个页面的配置操作

DynamicSelectOptions.js

import { requestUrl } from '@/api/constant'
const DEFAULT_PAGE = 1
const DataTableMixin = {
  data() {
    return {
      _url: '',
      _params: {},
      _query: {},
      page: DEFAULT_PAGE,
      offset: 20,
      loading: false, // 是否在加载数据
      list: [], // 数据列表
      total: 0,
      _dataField: null,
      projectSelectOption: {
        url: requestUrl.projectSettingsList,
        props: {label: 'proName',value: 'id'},
        params: {status: 1, pageSize: 1000, pageIndex: 1},
        searchKey: "projectName",
        clearable: true,
        filterable: true
      }
    }
  },
  methods: {
     /**
     * 拉取数据
     * @returns {Promise<any>}
     */
      async dataTableInit() {
        // 根据url拉取数据
      },
  }
}

export default DataTableMixin

A页面

<template class="A app-container">
	<FormSelect
    ref="FormSelect"
    value.sync="projectId"
    v-bind="projectSelectOption"
    :clearable="false" 
		// 如果不允许清除选项,可以通过下面的覆盖上面v-bind绑定的配置项 
    // 类似 Object.assing({clearable: true}, {clearable: false})
	>
	</FormSelect>
</template>


<script type="text/ecmascript-6">
import '@/components/FormSelect'
import DynamicSelectOptions form './DynamicSelectOptions'

export default {
	data() {
		reutrn {
			projectId: '',
		}
	}
}
</script>

配置项

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

参数 说明 类型 默认值
value 数据 [String, Boolean, Number, Array] -
formatter 格式化数据 Function -
disabled 是否禁用 Boolean false
readonly 是否只读 Boolean false
options 备选项 Array -
props 备选项映射 Object {label: 'label',value: 'value',children: 'children'}
className 类名 [String, Array] -
customStyle 自定义样式 Object -
url 动态获取数据url String -
method 动态获取数据请求方式 String GET
params 动态获取数据请求参数 Object -
parseData 解析接口返回的数据 Function -
searchable 是否可远程搜索 Boolean -
searchKey 搜索关键字字段名 String label
remoteMethod 远程搜索方法 Function -
listeners 事件 Object {}

基本实现

DynamicSelect/select.js

export default {
  name: 'DynamicSelect',
  props: {/* 暂时就上面那些 */},
  inheritAttrs: false,
  inject: {
    jlFormSubject: { default: null } // 接受注入的表单实例
  },
  data() {
    return {
      // 默认事件
      defaultOn: {
        input: this.handleChange
      },
      optionsData: this.options,
      loading: false,
      oldOptionsData: null,
    }
  },
  computed: {
     // 因为Vue不能修改props传递过来的值, 所以借助新的值进行传递
     newValue: {
      get({ value }) {
        return value
      },
      set(val) {
        // 双向绑定
        this.$emit('update:value', val)
        return val
      }
    },
    // 支持listener对象传入回调
    onEvents() {
      return Object.assign({}, this.defaultOn, this.listeners)
    },
    // props支持扩展,比如传入formatter函数进行格式化
    optionsProps() {
      return Object.assign({}, defaultProps, this.props)
    },
    bindAttrs() {
      const obj = {
        // 禁用的情况下不显示placeholder
       	placeholder: this.disabled ? '' : this.$attrs.placeholder || '请选择'
      }
      // 远程搜索功能开启再添加必要props
      if (this.searchable) {
        obj.filterable = true
        obj.remote = true
      }
      return Object.assign(
        {},
        this.$attrs,
        obj
      )
    },
    // 组合远程搜索params: 尽可能增强容错性
    requestOption() { 
      let paramsKey = this.method.toUpperCase() === 'GET' ? 'params' : 'data'
      return {
        baseUrl: this.$attrs.baseUrl,
        url: this.url,
        method: this.method
        [paramsKey]: this.params || this.$attrs.data 
      }
    }
  },
  watch: {
    options: {
      handler(value) {
        this.optionsData = value
      },
      deep: true,
      immediate: true
    },
    // 允许动态修改url、params、method
    requestOption: {
      handler() {
        if (this.url) {
          this.getOptionsData()
        }
      },
      deep: true
    }
  },
}
  

以上哪些就是我们的动态数据源了,从上面那些大致可以get到封装组件技巧:

  • 通过computed计算属性的getter/setterOr $emit来实现v-model
  • 通过computed将一个功能的几个属性整合起来通过watch侦听依赖数据的变动来作出行动(我们一般不会把关键性的props定义成对象让用户传递, 比如定义requestOption的props让用户传递对象的配置项)
  • 对于太过边缘的属性不会一个个都用props去接,通过computed将$attrs和props根据逻辑进行整合成一个属性对象

实现动态请求数据及响应数据变动

// 都是接着上面来的,还是同一个文件
created() {
 // 非级联select
 if (!Reflect.has(this.bindAttrs, 'cascade') && this.url) {
   	// getOptionsData:获取数据后返回promise, 可以在获取数据后做其他操作
    this.getOptionsData().then(data => {
     // 比如自动选中第一个等等。。。
   })
 }
},
methods: {
  // 动态拉取选项数据
  async getOptionsData() {
    return new Promise((resolve, reject) => {
      let {
        requestOption,
        parseData
      } = this
			this.loading = true
       // 再做一次兜底校验
       if (!requestOption.url) return
			request(...requestOption).then((res = {}) => {
        this.loading = false
        // 对于响应数据可以进行处理
        /*
        	1. 比如:接口返回了这样的数据结构 {code: 200, pageData: {data: []}, pageIndex: 1, pageSize: 100,}   => 可以通过parseData方法 return {data: res.pageData?.data || []}
          2. 再比如:我们需要加个全部选项:return {data: res.pageData?.data ? [{label: '全部', id: 'all'}, ...res.pageData.data] : []}
        */
        if (parseData && typeof parseData === 'function') {
          res = parseData(res)
        }
        if (Array.isArray(res.data)) {
          this.optionsData = res.data
        } else {
          this.optionsData = []
        }
        resolve(this.optionsData)
      }).catch((e) => {
        this.optionsData = []
        this.loading = false
        reject(e)
      })
    }) 	
  }
	
}

从上面那些大致可以get到封装组件技巧:

  • 尽量多spread(…)运算符,避免写死key等
  • 不太好控制的区块要尽可能将主权交给使用者,我们一般通过回调实现

render函数

下面通过render函数来渲染select, 这里并没有用jsx的写法。这块如果不知道建议写去过下文档的render,深入数据对象那块。传送门👉深入数据对象

render(h) {
  const self = this   
  // 渲染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
    }]
  })

  return h('el-select', {
    // 支持自定义className
    class: self.className, 
    // 静态的className,可以通过这个来写点自样式
    staticClass: 'jl-full-line',
    // 也支持写权限较高的行内样式
    style: self.customStyle,
    // 将props全部传入,原生attrs支持就有效果
    attrs: {
      ...self.bindAttrs
    },
    props: {
      value: self.newValue,
      loading: self.loading,
      ...self.bindAttrs
    },
    on: {
      ...self.onEvents, // 将所有listeners监听全部传入
      input: self.handleInputEvent,
      change: self.handleChangeEvent,
      'visible-change': self.handleVisibleChange
    },
  }, optionsVnode)
}

从上面那些大致可以get到封装组件技巧:

  • spread(…)运算符,让你组件属性大量可扩展

这里可能有人就会问了,为什么不用$listener,非要自己通过回调实现自定义事件,意义何在?

如果你写过很多基础组件可能会遇到过

当这个组件当做其他封装组件的子组件的时候,其他组件也会通过v-on=$listener将监听的自定义事件透传下来,如果组件嵌套很多,组件内部的事件很有可能会被外部组件影响,特别:input, change这类事件,当你组件自身监听过,外层通过透传也监听到了

对于上面监听的input、change事件这里就不解析了,这里讲一点handleValueChanged

/*
	对于value又是我们会需要进行foramtter,比如:
	我们拉取的数据是这种结构:
		data: [{id: '1', name: 'A'}, {id: '2', name: 'B'}, xxx]
	我们多选之后,拿到的数据是这样的[{id: '1', name: 'A'}, {id: '2', name: 'B'}],但是后端要的数据结构是这种
	[{projectName: 'A', projectEntrys: {id: '1', name: 'A'}, ... }]
*/

上面这种情况相信在项目中是常见的,我们肯定不能通过监听事件来变动value, formatter就是最好的方式,根据当前业务去写逻辑就可以了,这里就不贴代码了

后续内容下个章节更新

  • 通过自定义slots来渲染组件内容
  • 提供组件外层render函数灵活渲染组件提供的作用域插槽

效果图💗

3.gif

写在最后

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

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

往期文章

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

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

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

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

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

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

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

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 前端自学驿站 原文链接:https://juejin.im/post/6950974720015597604

回到顶部