# 第13章 实战:表单配置化实现

本章节相关代码存放在Github (opens new window)中。

配置化功能有意思的地方在于,当你实现了配置化的支持,你的整个应用都可以通过配置化来实现,甚至包括进行配置化的页面,从而实现自举。

前面《第10章 抽象的正确姿势》介绍了如何将页面抽象成数据,以及简单介绍了如何将抽象出来的数据进行配置化。这一章我们来实战一下,表单是大多数管理端都需要的部分,而这些通用的部分,我们要怎样去将他们做成可配置的组件,同时通过配置和数据绑定来实现可复用的表单组件呢?这一章内容,同样会基于 Element 来实现,节约一些细节的时间,带大家快速做出想要的效果。

# 13.1 表单配置化设计

在管理端页面中(也可以参考第12章三天开发一个管理端的内容),表单几乎是不可缺少的一个组成部分。但我们在日常开发中,每次新来一个配置需求,都需要新增一个页面,新增列表、表单、以及相似的增删查改逻辑。在这里,我们可以将表单进行抽象,做成配置化的组件,后续只需要引入这个组件和传入一些配置,就可以快速地得到想要的效果了。

# 13.1.1 将表单抽象化

我们在做抽象的时候,有一个前提是我们已有重复性的开发过程,再针对性地结合应用场景来进行抽象。所以说世界上没有完全通用的工具,但在给出具体的使用场景之后,我们还是可以做出很多在同类型场景下通用的工具。所以在这里,我们先来找一个常用的表单内容,例如 Element 官网上的这个 Form 例子:

<!-- 用到了很多的表单组件 -->
<el-form ref="form" :model="form" label-width="80px">
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
  <el-form-item label="活动区域">
    <el-select v-model="form.region" placeholder="请选择活动区域">
      <el-option label="区域一" value="shanghai"></el-option>
      <el-option label="区域二" value="beijing"></el-option>
    </el-select>
  </el-form-item>
  <el-form-item label="活动时间">
    <el-col :span="11">
      <el-date-picker
        type="date"
        placeholder="选择日期"
        v-model="form.date1"
        style="width: 100%;"
      ></el-date-picker>
    </el-col>
    <el-col class="line" :span="2">-</el-col>
    <el-col :span="11">
      <el-time-picker
        placeholder="选择时间"
        v-model="form.date2"
        style="width: 100%;"
      ></el-time-picker>
    </el-col>
  </el-form-item>
  <el-form-item label="即时配送">
    <el-switch v-model="form.delivery"></el-switch>
  </el-form-item>
  <el-form-item label="活动性质">
    <el-checkbox-group v-model="form.type">
      <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
      <el-checkbox label="地推活动" name="type"></el-checkbox>
      <el-checkbox label="线下主题活动" name="type"></el-checkbox>
      <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
    </el-checkbox-group>
  </el-form-item>
  <el-form-item label="特殊资源">
    <el-radio-group v-model="form.resource">
      <el-radio label="线上品牌商赞助"></el-radio>
      <el-radio label="线下场地免费"></el-radio>
    </el-radio-group>
  </el-form-item>
  <el-form-item label="活动形式">
    <el-input type="textarea" v-model="form.desc"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="onSubmit">立即创建</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>

我们能看到,每个选项都包括左侧的标签、右侧的表单组件,用<el-form-item>描述。这里我们看每个表单组件,都有相似的配置信息,主要包括:

  • v-model: 用来绑定值
  • placeholder: 默认填充信息,主要用于填写类组件
  • 选项: 用户选择的选项,用于下拉选项、多选、单选等组件

这里每个组件都会有的是v-model,同时不同的表单组件会有稍微不同的一些配置内容,所以我们可以用简单的条件判断和循环来生成这样的组件,同时底部的<el-button>比较特殊,我们可以通过留出<slot>的方式来使用,所以我们可以得到这样一个表单配置组件:

<template>
  <el-form ref="form" label-width="80px">
    <!-- 遍历 formComponentList,生成表单组件列表 -->
    <!-- 通过 formLabel 配置左侧 label 标签名称 -->
    <el-form-item
      :label="item.formLabel"
      v-for="(item, index) in formComponentList"
      :key="index"
    >
      <!-- 通过 v-if 判断,插入对应的表单组件 -->
      <!-- 每个表单组件都有 v-model 来绑定 value 值 -->
      <el-input
        v-if="item.componentName === 'el-input'"
        v-model="item.value"
        :type="item.type"
        :placeholder="item.placeholder"
      ></el-input>
      <el-select
        v-if="item.componentName === 'el-select'"
        v-model="item.value"
        :placeholder="item.placeholder"
      >
        <!-- select、checkbox-group、radio-group 等选项组件可通过 options 来配置相应的选项 -->
        <el-option
          v-for="option in item.options"
          :label="option.label"
          :value="option.value"
          :key="option.value"
        ></el-option>
      </el-select>
      <!-- 日期和时间组件,可通过 valueFormat 配置值的格式 -->
      <el-date-picker
        v-if="item.componentName === 'el-date-picker'"
        :type="item.type || 'date'"
        :value-format="item.valueFormat"
        :placeholder="item.placeholder"
        v-model="item.value"
      ></el-date-picker>
      <el-time-picker
        v-if="item.componentName === 'el-time-picker'"
        :value-format="item.valueFormat"
        :placeholder="item.placeholder"
        v-model="item.value"
      ></el-time-picker>
      <el-switch
        v-if="item.componentName === 'el-switch'"
        v-model="item.value"
      ></el-switch>
      <el-checkbox-group
        v-if="item.componentName === 'el-checkbox-group'"
        v-model="item.value"
      >
        <el-checkbox
          v-for="option in item.options"
          :label="option.label"
          :key="option.label"
          >{{option.text || option.label}}</el-checkbox
        >
      </el-checkbox-group>
      <el-radio-group
        v-if="item.componentName === 'el-radio-group'"
        v-model="item.value"
      >
        <el-radio
          v-for="option in item.options"
          :label="option.label"
          :key="option.label"
          >{{option.text || option.label}}</el-radio
        >
      </el-radio-group>
    </el-form-item>
    <!-- slot 留个性化的内容 -->
    <slot></slot>
  </el-form>
</template>

<script>
  // 下面是 Vue 组件
  export default {
    // 传入选项
    props: {
      // 表单组件信息
      formComponentList: {
        type: Array,
        default: () => []
      }
    }
  };
</script>

这里我们支持的配置项主要包括以下表格的内容:

表 13-1 表单配置说明

配置项 说明
formLabel 左侧标签说明
componentName 组件名称,可参考 Element 组件
value 表单组件值
placeholder 表单组件默认占位内容
options 选择组件的列表选项,同样需要参考 Element 组件
type 部分表单组件支持配置类型,例如日期、时间组件
其他选项 参考 Element 组件

而如果我们要实现完整的表单功能,还需要很多其他的功能完善,例如表单校验也是很基础的能力。这里我们只介绍实现的方法和思维,大家可以自行进行更多的尝试,丰富表单配置的能力。

# 13.1.2 通过配置生成表单

Element 中的这些表单组件,可配置的选项远不止上面列出的,大家可以根据实际的使用情况来新增或者移除一些选项。我们能看到,这里通过传入formComponentList配置,就可以生成对应的表单了。我们需要生成和上方一致的表单,可以通过这样进行配置:

<template>
  <el-container>
    <el-header>配置化表单</el-header>
    <el-main>
      <!-- 传入 formComponentList 配置数据 -->
      <DynamicForm :formComponentList="formComponentList">
        <!-- 中间的内容会替换掉 slot -->
        <!-- 这里传入了创建和取消的按钮 -->
        <el-form-item>
          <el-button type="primary" @click="onSubmit">立即创建</el-button>
          <el-button>取消</el-button>
        </el-form-item>
      </DynamicForm>
    </el-main>
  </el-container>
</template>

<script>
  // 引入动态表单组件
  import DynamicForm from "./components/DynamicForm.vue";

  export default {
    name: "app",
    components: {
      // 使用动态表单组件
      DynamicForm
    },
    data() {
      return {
        // 配置表单相关的属性和数据
        formComponentList: [
          {
            componentName: "el-input",
            formLabel: "活动名称"
          },
          {
            componentName: "el-select",
            formLabel: "活动区域",
            options: [
              { label: "区域一", value: "shanghai" },
              { label: "区域二", value: "beijing" }
            ],
            placeholder: "请选择活动区域"
          },
          {
            componentName: "el-date-picker",
            formLabel: "活动时间",
            type: "date",
            valueFormat: "yyyy-MM-dd",
            placeholder: "选择日期"
          },
          {
            componentName: "el-time-picker",
            valueFormat: "HH:mm:ss",
            placeholder: "选择时间"
          },
          {
            componentName: "el-switch",
            formLabel: "即时配送"
          },
          {
            componentName: "el-checkbox-group",
            formLabel: "活动性质",
            options: [
              { label: "美食/餐厅线上活动" },
              { label: "地推活动" },
              { label: "线下主题活动" },
              { label: "单纯品牌曝光" }
            ],
            value: []
          },
          {
            componentName: "el-radio-group",
            formLabel: "特殊资源",
            options: [{ label: "线上品牌商赞助" }, { label: "线下场地免费" }],
            type: "textarea"
          }
        ]
      };
    },
    methods: {
      onSubmit() {
        // 点击提交的时候,将值筛选出来
        const formValueList = this.formComponentList.map(x => x.value);
        console.log({ formValueList });
      }
    }
  };
</script>

我们可以看到最终页面效果,以及点击提交的时候输出的内容如图:

image
图 13-1 配置化表单效果

点击此处查看页面效果 (opens new window) 点击此处查看源码 (opens new window)

# 13.2 表单配置所见即所得

现在我们已经实现了怎样通过传入配置来直接生成我们想要的表单组件效果。但我们在真正使用的时候,如果有所见即所得的支持方式,就可以直接在线调试想要的配置效果,然后将配置保存下来,而不用每调整一下就得手动保存、再切到浏览器查看效果。我们来看一下怎样做成所见即所得的效果。

# 13.2.1 jsoneditor 的 v-model 绑定实现

所见即所得,一般依赖于将配置进行在线化,也就是说我们可以在线改动配置,然后在线查看改动效果。这里由于我们的formComponentList配置是数组的格式,所以我们也可以用 JSON 编辑器来管理。这里用到了 jsoneditor(https://github.com/josdejong/jsoneditor),大家也可以去 Github 上查看这个工具。我们通过 Npm 进行安装:

npm install jsoneditor

然后我们可以使用自定义v-model的方式(第5章也有介绍),通过设置组件的valueProp 和input事件来实现:

<template>
  <!-- 设置外层高度,jsoneditor 会根据外层高度来初始化 -->
  <div style="height: 600px">
    <!-- 提供一个绑定 id 的元素进行初始化 -->
    <div id="jsoneditor"></div>
  </div>
</template>

<script>
  // 引入 jsoneditor 方法
  import JSONEditor from "jsoneditor/dist/jsoneditor.min.js";

  export default {
    data() {
      return {
        editor: null, // 保存 jsoneditor 实例
        innerValue: null // 保存内部值,用于与外部值比较和更新
      };
    },
    // 配置 v-model 的事件和绑定值,分别为 change 和 value
    model: {
      event: "change",
      prop: "value"
    },
    // 配置 Prop 属性
    props: {
      // 传入配置项,可用于更新配置
      options: {
        type: Object,
        default: () => {}
      },
      // 是否只读状态,默认可编辑
      isReadonly: {
        type: Boolean,
        default: false
      },
      // 绑定 v-model 的值
      value: null
    },
    watch: {
      // 监听 options 选项,用于更新
      options(val) {
        this.setOption(val);
      },
      // 监听 value 值,用于更新 jsoneditor 的值
      value(newVal, oldVal) {
        // 这里由于是对象或数组类型,所以使用 JSON.stringify 进行比较
        if (JSON.stringify(newVal) !== JSON.stringify(this.innerValue)) {
          // 如果更新的值与内部的值不相等,则更新
          this.editor.set(newVal);
        }
      }
    },
    mounted() {
      // 初始化选项,传入是否可编辑相关的一些配置
      let options = this.isReadonly
        ? {
            mode: "view"
          }
        : {
            mode: "code",
            modes: ["code", "form", "tree", "view"],
            onChange: () => {
              // 编辑模式下,onChange 即编辑内容有变更
              try {
                // 此时需要获取编辑内容
                const value = this.editor.get();
                // 然后更新
                this.setValue(value);
              } catch (e) {}
            }
          };
      // 将初始化选项与传入选项合并
      options = Object.assign(options, this.options);
      // 初始化 jsoneditor,传入选项信息
      this.editor = new JSONEditor(this.$el, options);
      // 如果有默认传入值,则设置进去
      if (this.value) {
        this.editor.set(this.value);
      }
    },
    methods: {
      setValue(val) {
        // 更新内部值
        this.innerValue = val;
        // 通过 $emit 触发 change 事件,将 val 值更新到 v-model
        this.$emit("change", val);
      }
    }
  };
</script>
<style scoped>
  /* 局部引入 jsoneditor 样式 */
  @import "~jsoneditor/dist/jsoneditor.min.css";
</style>

这样,我们就可以通过<JSONEditor v-model="jsonValue"></JSONEditor>这样的方式,来直接绑定使用 jsoneditor 了。

# 13.2.2 绑定表单配置到 jsoneditor

上面我们已经实现了 jsoneditor 绑定数据到v-model,现在我们来调整下页面结构,方便所见即所得的实现。我们将表单的效果展示放置在左侧,右侧是 jsoneditor 的配置编辑内容:

<template>
  <div>
    <el-container>
      <el-header>配置化表单</el-header>
      <el-main>
        <el-row :gutter="50">
          <!-- 左侧为表单展示 -->
          <el-col :span="12">
            <!-- 传入 formComponentList 配置数据 -->
            <DynamicForm :formComponentList="formComponentList">
              <!-- 中间的内容会替换掉 slot -->
              <!-- 这里传入了创建和取消的按钮 -->
              <el-form-item>
                <el-button type="primary" @click="onSubmit">立即创建</el-button>
                <el-button>取消</el-button>
              </el-form-item>
            </DynamicForm>
          </el-col>
          <!-- 右侧为 jsoneditor 编辑器 -->
          <el-col :span="12">
            <!-- v-model 绑定 formComponentList -->
            <JSONEditor v-model="formComponentList"></JSONEditor>
          </el-col>
        </el-row>
      </el-main>
    </el-container>
  </div>
</template>

<script>
  // 引入动态表单组件
  import DynamicForm from "./components/DynamicForm.vue";
  // 引入 jsoneditor 组件
  import JSONEditor from "./components/JSONEditor.vue";

  export default {
    name: "app",
    components: {
      // 使用组件
      DynamicForm,
      JSONEditor
    },
    data() {
      return {
        // 配置表单相关的属性和数据
        formComponentList: [
          // 这里参考上面一节内容,没有做任何变更,篇幅原因省略
        ]
      };
    },
    mounted() {
      // 设置三秒后变更 formComponentList,检查左侧表单和右侧 jsoneditor 效果
      setTimeout(() => {
        this.formComponentList = [
          {
            componentName: "el-input",
            formLabel: "活动名称"
          }
        ];
      }, 3000);
    },
    methods: {
      onSubmit() {
        // 点击提交的时候,将值筛选出来
        const formValueList = this.formComponentList.map(x => x.value);
        console.log({ formValueList });
      }
    }
  };
</script>

这里我们调整了布局,以及将formComponentList绑定到 jsoneditor 中,我们可以看到效果:

image
图 13-2 配置化在线表单效果

我们修改下右侧的配置,也可以看到左侧内容变更(图 13-4 中方框所示),包括表单值的内容(图 13-4 中箭头所示):

image
图 13-3 配置化在线表单修改效果 1

同时,我们这里设置了 3 秒以后,更新formComponentList(模拟从后台拉取数据的效果),也能看到左侧表单和右侧配置都改变了:

image
图 13-4 配置化在线表单修改效果 2

点击此处查看页面效果 (opens new window) 点击此处查看源码 (opens new window)

# 13.3 表单值的抽离与配置

现在我们已经实现了,通过在线编辑可以以所见即所得的方式来看到表单的页面效果。但在真正使用过程中,我们常常需要的是将表单里用户填入或者选择的值单独提交上来,或者说我们需要从后台拉取到之前的编辑信息,再导入到我们的表单中进行修改。在这样的情况下,我们可以通过单独抽离出表单的值来进行管理。

# 13.3.1 表单的 v-model 绑定实现

我们要单独将表单的真实值提取出来,也可以通过v-model的方式来进行绑定,因此我们需要进行以下调整:
(1) 每个表单组件的绑定值,都绑定到整个表单的formModel上,通过新增name属性,确定每个组件绑定值的关键字。
(2) 表单通过配置v-model的事件和绑定值,来保持内部值和外部的一致性。

通过这样的方式,我们可以使用v-model直接绑定表单的值:

<!-- DynamicForm.vue -->
<!-- 篇幅关系,原有的注释移除,这里只标记新增或调整的部分 -->
<template>
  <!-- 添加 model 属性,绑定 formModel -->
  <el-form ref="form" :model="formModel" label-width="80px">
    <el-form-item
      :label="item.formLabel"
      v-for="(item, index) in formComponentList"
      :key="index"
    >
      <!-- 每个表单组件都有 v-model 来绑定 value 值,这里调整成 formModel,通过 name 来绑定 formModel 的属性值 -->
      <!-- 同时通过绑定 change 事件,来通知 formModel 更新 -->
      <el-input
        v-if="item.componentName === 'el-input'"
        v-model="formModel[item.name]"
        @change="updateValue"
        :type="item.type"
        :placeholder="item.placeholder"
      ></el-input>
      <el-select
        v-if="item.componentName === 'el-select'"
        v-model="formModel[item.name]"
        :placeholder="item.placeholder"
        @change="updateValue"
      >
        <el-option
          v-for="option in item.options"
          :label="option.label"
          :value="option.value"
          :key="option.value"
        ></el-option>
      </el-select>
      <!-- 其他组件同等处理,此处省略 -->
    </el-form-item>
    <slot></slot>
  </el-form>
</template>

<script>
  // 下面是 Vue 组件
  export default {
    props: {
      formComponentList: {
        type: Array,
        default: () => []
      },
      // 绑定 v-model 的值
      value: null
    },
    // 配置 v-model 的事件和绑定值,分别为 change 和 value
    model: {
      event: "change",
      prop: "value"
    },
    data() {
      return {
        // 保存内部值,用于与外部值比较和更新
        formModel: {}
      };
    },
    watch: {
      value(val) {
        // 更新内部值
        this.updateFormModel(val);
      }
    },
    methods: {
      updateValue() {
        // 更新外部值,需要变更引用才会生效
        this.$emit("change", { ...this.formModel });
      },
      // 更新内部值
      updateFormModel(value) {
        let formModelInit = {};
        // 检查是否有 el-checkbox-group 类型组件
        this.formComponentList.forEach(x => {
          if (x.componentName === "el-checkbox-group") {
            // 该组件需要有初始值,不然会报错
            formModelInit[x.name] = [];
          }
        });
        // 合并初始值和传入值
        this.formModel = value
          ? { ...formModelInit, ...value }
          : { ...formModelInit, ...this.formModel };
      }
    },
    created() {
      // 创建的时候,更新内部值
      this.updateFormModel();
    }
  };
</script>

# 13.3.2 通过 jsoneditor 编辑表单值

前面我们已经封装好 jsoneditor 组件,现在我们可以直接用来编辑表单的值,这里我们也调整了下页面的布局,稍微好看一些。左侧放表单效果展示以及表单绑定值的编辑器,右侧依然放置我们的表单配置编辑器:

<template>
  <div>
    <el-container>
      <el-header>配置化表单</el-header>
      <el-main>
        <el-row :gutter="50">
          <!-- 左侧为表单展示相关 -->
          <el-col :span="12">
            <!-- 上方为表单展示 -->
            <el-card class="box-card">
              <div slot="header" class="clearfix">
                <span>表单展示</span>
              </div>
              <!-- 传入 formComponentList 配置数据 -->
              <!-- v-model 绑定表单值 -->
              <DynamicForm
                :formComponentList="formComponentList"
                v-model="formModel"
              >
                <!-- 中间的内容会替换掉 slot -->
                <!-- 这里传入了创建和取消的按钮 -->
                <el-form-item>
                  <el-button type="primary" @click="onSubmit"
                    >立即创建</el-button
                  >
                  <el-button>取消</el-button>
                </el-form-item>
              </DynamicForm>
            </el-card>
            <!-- 下方为表单值 -->
            <el-card class="box-card">
              <div slot="header" class="clearfix">
                <span>表单数据</span>
              </div>
              <JSONEditor v-model="formModel"></JSONEditor>
            </el-card>
          </el-col>
          <!-- 右侧为 jsoneditor 编辑器 -->
          <el-col :span="12">
            <el-card class="box-card">
              <div slot="header" class="clearfix">
                <span>表单配置</span>
              </div>
              <!-- v-model 绑定 formComponentList -->
              <JSONEditor
                v-model="formComponentList"
                height="600px"
              ></JSONEditor>
            </el-card>
          </el-col>
        </el-row>
      </el-main>
    </el-container>
  </div>
</template>

<script>
  // 其他内容省略
  export default {
    // 其他选项省略
    data() {
      return {
        // 表单值
        formModel: {},
        // 配置表单相关的属性和数据
        formComponentList: [
          // 这里参考前面内容,没有做任何变更,篇幅原因省略
        ]
      };
    }
  };
</script>

可以看到,我们通过填写或者选择表单组件的内容,表单的值不再出现在配置中,而出现在我们的表单数据里:
image
图 13-5 配置化表单数据拆分

同时,我们也可以通过变更表单数据的值,来同步更新组件的值:

image
图 13-6 配置化表单数据拆分修改效果

点击此处查看页面效果 (opens new window) 点击此处查看源码 (opens new window)

通过这种方式,我们将表单配置和表单绑定的数据解耦开了,这样我们在使用的时候,就可以传入固定的配置,然后单独提取值或者设置值来使用。当然目前来说的实现都是偏 Demo 化,而真正在生产环境使用,还需要根据想要的效果来调整实现,常见的例如提供所见即所得的表单配置,自动生成所需要的页面(将配置保存到后台,在页面加载的时候自动获取加载),而表单值也可以结合与后台接口的交互来实现更新和编辑等功能。

除了表单之外,我们常见的管理端功能,还会包括列表的增删查改、菜单的选择和跳转等,其实这些都同样地可以做成配置化的方式。如果从菜单、列表增删查改、表单等能力都进行配置,那我们就可以将日常枯燥的重复开发工作节省出来,做更核心或者更有挑战性的功能了。在不断地提升开发效能之后,我们就可以将宝贵的时间用于更多的能力提升或者感兴趣的事情上了。