使用vue3.x + typescript + tsx基于element-plus开发一个搜索组件(上)

5,307 阅读4分钟

简介

由于业务需要,本人需要开发一个基于element-plus的表单组件集成的搜索组件(效果如下图),效果为通过对组件以JSON Schema的方式传入配置项的json达到动态渲染的效果。本文主要记录开发组件的过程,记得先点赞在看噢!

QQ图片20210818214212.png

何为JSON Schema

json是目前应用非常多的数据交换格式。既然是用于数据交换的格式,那么就存在数据交换的双方。如何约定或校验对方的数据格式是符合要求的,就成了交互需要解决的一个问题。所以Json Schema就是用来定义json数据约束的一个标准。根据这个约定模式,交换数据的双方可以理解json数据的要求和约束,也可以据此对数据进行验证,保证数据交换的正确性。

如下为JSON Schema的一个实例

111.png 可以看到,它是通过一份JSON的配置文件来限制数据具体的格式,包括变量的类型、长度等等,根据这个思想,我们也可以通过一份配置的json动态的生成我们所需要的搜索组件。

在了解了如何动态的生成我们的搜索组件后,话不多说,直接开干。

项目初始化

通过vue-cli搭建项目

  1. 在终端中输入vue create demo,配置如下

image.png vue的话选择3.x版本,选择sass,其他的话看各位老板的喜好自己搭配就行。

  1. 安装ui库,tsx插件 本项目采用element-plus作为ui库

运行npm i element-plus

@vue/babel-plugin-jsx开发tsx

运行npm i @vue/babel-plugin-jsx -D

附上文档:www.npmjs.com/package/@vu…

文件修改

  1. 全局引用element-plus,以及配置babel.config.js

image.png

image.png

  1. 删除多余文件,整理项目结构,最终长这样

image.png

  1. 最后在Home.vue文件中引用searchArea组件,demo搭建完成!

image.png

组件配置Json初定义

通过观察效果图得知,搜索项有多个(下文称之为小组件),很自然我们能想到通过传入一个config数组来遍历出各个小组件,当然还要传入一个v-model来接收各个小组件的value,还要传入一个showLength来控制默认显示的小组件的个数,其余的通过点击展开/搜索按钮进行显隐。

image.png

那么问题来了,config中的每一项又需要传入哪些变量来生成一个小组件呢?

先看看最简单的input组件和select组件,需要配置的是左边的搜索标题name,还有对应渲染哪种组件,还有对应的value值;select组件还需要配置下拉列表options,也可以通过传入options的labelName和valueName来渲染options

image.png

最重要的一点是,还可以传入element-plus一些组件原来就有的属性,于是我们声明一下config数组每一项的类型

image.png

组件内也引用一下刚定义好的类型

image.png

传入配置

接下来,我们想生成一个输入框和搜索框,传入配置

image.png

组件传入

image.png

input、select组件的开发

首先,开发大体布局

import { defineComponent, PropType, ref } from "vue";
import { IConfigItem } from './types';

export default defineComponent({
  props: {
    // 传入的配置项
    config: {
      type: Array as PropType<IConfigItem[]>,
      required: true,
    },
    // 传入的v-model值
    modelValue: {
      type: Object,
      required: true,
    },
    // 默认显示的数量
    showLength: {
      type: Number,
      default: 2,
    },
  },
  setup() {
    // 控制展开收起
    const isOpen = ref(false);
    // 点击展开/收起
    const handleOpenRetract = () => {
      isOpen.value = !isOpen.value;
    };
    return {
      isOpen,
      handleOpenRetract,
    }
  },
  render() {
    return (
      <el-form
        label-width="96px"
        size="small"
      >
        <el-row>
          {
            this.config.map((item: IConfigItem, index: number) => {
              return (
                <el-col
                  xs={24}
                  sm={12}
                  md={8}
                  lg={8}
                  xl={6}
                >
                  {/* 存放每一个config项生成的小组件 */}
                  <el-form-item></el-form-item>
                </el-col>
              )
            })
          }
          <el-col
            xs={24}
            sm={12}
            md={8}
            lg={8}
            xl={6}
            class="margin-bottom-15 padding-left-20">
            <el-button
              size="small"
              class="margin-left-20"
              type="primary"
            >查询</el-button>
            <el-button
              size="small"
            >重置</el-button>
            {
              this.config.length > this.showLength && (
                <el-button
                  size="small"
                  class="margin-left-20"
                  onClick={this.handleOpenRetract}
                >
                  {this.isOpen ? '收起' : '展开'}
                  <i class={`el-icon-arrow-${this.isOpen ? 'up' : 'down'}`}></i>
                </el-button>
              )
            }
          </el-col>
        </el-row>
      </el-form>
    )
  }
})

接着,开发input组件

const renderInput = (item: IConfigItem) => {
  // component为input时渲染el-input组件
  return item.component === 'input' ? (
    // 传入一些常用的属性,原生属性通过展开item传入,定义v-model
    // 触发input事件时,更新外部params
    <el-input
      placeholder="请输入"
      clearable={true}
      {...item}
      v-model={this.modelValue[item.valueName!]}
      onInput={(value: string) => {
        const _value = value.trim();
        this.updateValue(_value, item);
      }}
    />
  ) : null;
};

更新外部传入params

// 更新值
const updateValue = (value: string, item: IConfigItem) => {
  if (['input', 'select'].includes(item.component!)) {
    emit('update:modelValue', {
      ...props.modelValue,
      [item.valueName as string]: value,
    });
  }
};

渲染el-input

{
    this.config.map((item: IConfigItem, index: number) => {
      return (
        <el-col
          xs={24}
          sm={12}
          md={8}
          lg={8}
          xl={6}
        >
          {/* 存放每一个config项生成的小组件 */}
          <el-form-item label={item.name} label-width="96px">
            {renderInput(item)}
          </el-form-item>
        </el-col>
      )
    })
}

同理可开发select组件

const renderSelect = (item: IConfigItem) => {
  // component为select时渲染el-select组件
  return item.component === 'select' ? (
    // 传入一些常用的属性,原生属性通过展开item传入,定义v-model
    // 触发change事件时,更新外部params
    <el-select
      placeholder="请选择"
      clearable={true}
      {...item}
      v-model={this.modelValue[item.valueName!]}
      onChange={(value: string) => {
        this.updateValue(value, item);
      }}
    >
      {
        item.options?.map((it) => {
          return (
            <el-option
              // 通过传入label和value字段名的配置
              label={it[item.optionLabel ?? 'label']}
              value={it[item.optionValue ?? 'value']}
            />
          );
        })
      }
    </el-select>
  ) : null;
};

最后给查询、重置按钮绑定事件

<el-button
  size="small"
  class="margin-left-20"
  type="primary"
  onClick={() => {
    this.$emit('handleSearch');
  }}
>查询</el-button>
<el-button
  size="small"
  onClick={() => {
    this.$emit('handleReset');
  }}
>重置</el-button>

父组件监听事件:

image.png

大功告成,我们看看实际效果(忽略样式,有点丑)

收起时

image.png

展开时

image.png

点击查询

image.png

总结

至此,我们简单的完成了搜索组件(包含input和select)的封装,从这里可以看出,他们的生成都是基于config配置;当然,这正是我们这个搜索组件的冰山一角(语文不太好,不知道有没有用错);当然了,我们强调掌握的是封装组件的思想,从一开始的传参,到组件的开发,都是对这个需求总体的考虑。

附上源码:github.com/qaz61912862…

ps:后续还会封装很多小组件,详见:

《使用vue3.x + typescript + tsx基于element-plus开发一个搜索组件(下篇)》

有空就更新~

点个赞吧,求求了~~!