NoraH1to

V1

2022/05/04阅读:97主题:山吹

vue2搬砖提效 - (1)

受控表单

B 端开发中打交道最多的组件 - 表单

原文: 点我跳转

示例代码: 点我跳转

发生甚么事了

最近在掘金摸鱼时,刷到了好几篇关于在 vue2 中封装表单的文章,什么提效 200% 叭叭叭的说的天花乱坠

点进去一看,其实就是把表单封装成一个可配置的巨型组件,模板语法里一大堆 v-if v-else-if

我个人认为,这种封装方式不太可取,原因有以下几点:

  1. 它完全是把写模板的代码量转移到了配置上,实质上只是换了一种写法,工作量并没有减少多少,简直是为了封装而复用,本末倒置

  2. 不仅要理解封装后组件的使用方法,还需要熟悉原始表单组件的使用方法

  3. 并且如果稍微增加、变动几个需求或者换个表单组件,一座没法维护的 💩 山就形成了


这边总结一下较高层抽象的表单通用封装需求:

  1. 可以替换不同的组件库

  2. 可以灵活应对业务逻辑的变化

  3. 使用成本低

这么一看,可以抽离封装的东西其实不多,只有表单数据的逻辑

只对数据进行抽象封装,模板中的组件消费这些数据

撸码

首先整一个简单的表单出来

<!-- components/FormControl/index.vue -->

<template>
  <el-form ref="form" :model="formData" label-width="80px">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="formData.name"></el-input>
    </el-form-item>
    <el-form-item label="性别" prop="sex">
      <el-radio v-model="formData.sex" label="1"></el-radio>
      <el-radio v-model="formData.sex" label="2"></el-radio>
    </el-form-item>
    <el-form-item label="年龄" prop="age">
      <el-input-number v-model="formData.age"></el-input-number>
    </el-form-item>
  </el-form>
</template>

<script>
  export default {
    data() {
      return {
        formData: {
          nameundefined,
          sex'1',
          ageundefined,
        },
      };
    },
  };
</script>

我们简单的思考下,表单一般都是增、改两种操作,那么表单应该能接受一个初始值

那么外部需要能够控制组件内部的值,也就是一个受控组件

这时候不妨贯彻下 vue2 的一些组件思维,直接把数据做成双向绑定 (v-model),减少组件的使用理解成本

这边的双向绑定实现与官方示例不同,我们内部也维护了一个状态而官方示例中并没有,因此组件可以是非受控的

双向绑定语法糖需要实现这两个点:

  1. 外部值变化时,组件内部同步更新

  2. 组件内部值变化时,外部值同步更新

双向绑定配置

在实例中配置 model,配置接收属性为 data,更新外部数据的事件为 update:data

在外部既可以使用 v-mode 也可以使用 .sync 修饰符

// components/FormControl/index.vue - script

export default {
  model: {
    prop'data',
    event'update:data',
  },
  ...
};

外部值变化,更新内部值

首先需要接收一个外部的值 data

// components/FormControl/index.vue - script

export default {
  ...
  props: {
    data: {
      typeObject,
      requiredfalse,
      default() => ({}),
    },
  },
  ...
};

在实例内部监听 data 的变化

大部分情况下表单编辑的初始值都是异步获取的,但是有些需求是外部组件写死的默认值,这种情况下如果没有立刻同步会导致初始值无效

所以需要立即把 data 的值更新到 formData 中,即 immediate: true

// components/FormControl/index.vue - script

export default {
  ...
  data() {
    return {
      formData: { ... },
    };
  },
  watch: {
    data: {
      handler(newData) {
        Object.keys(newData).forEach((k) =>
          this.$set(this.formData, k, newData[k]),
        );
      },
      deep: true,
      immediate: true,
    },
  },
};

内部值变化,更新外部值

直接在实例中监听 formData

因为初始值是外部决定的,所以无需设置 immediate

// components/FormControl/index.vue - script

export default {
  ...
  data() {
    return {
      formData: { ... },
    };
  },
  watch: {
    ...
    formData: {
      handler(n) {
        /**
         * 这里用解构是为了让外部数据和内部数据不是同一个引用
         * 防止出现内存泄漏或其它奇奇怪怪的引用引起的bug
         * 一般进行一层的浅拷贝就足够了
         */

        this.$emit('update:data', { ...n });
      },
      deeptrue,
    },
  },
};

完整组件代码

<!-- components/FormControl/index.vue -->

<template>
  <el-form ref="form" :model="formData" label-width="80px">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="formData.name"></el-input>
    </el-form-item>
    <el-form-item label="性别" prop="sex">
      <el-radio v-model="formData.sex" label="1"></el-radio>
      <el-radio v-model="formData.sex" label="2"></el-radio>
    </el-form-item>
    <el-form-item label="年龄" prop="age">
      <el-input-number v-model="formData.age"></el-input-number>
    </el-form-item>
  </el-form>
</template>

<script>
  export default {
    model: {
      prop'data',
      event'update:data',
    },
    props: {
      data: {
        typeObject,
        requiredfalse,
        default() => ({}),
      },
    },
    data() {
      return {
        formData: {
          nameundefined,
          sex'1',
          ageundefined,
        },
      };
    },
    watch: {
      data: {
        handler(newData) {
          Object.keys(newData).forEach((k) =>
            this.$set(this.formData, k, newData[k])
          );
        },
        deep: true,
        immediate: true,
      },
      formData: {
        handler(n) {
          this.$emit('update:data', { ...n });
        },
        deeptrue,
      },
    },
  };
</script>

试试效果

直接在页面中引用

<!-- index.vue -->

<template>
  <form-control v-model="formData" />
</template>

<script>
  import FormControl from './components/FormControl/index.vue';
  export default {
    components: {
      FormControl,
    },
    data() {
      return {
        formData: {
          name'norah1to',
          sex'1',
          age23,
        },
      };
    },
  };
</script>

双向绑定成功

效果
效果

抽取公共逻辑

简单分析下 script 标签中的代码,可以很简单的发现,除了 formData 里的数据结构,其它都是能复用的部分,于是可以这样封装一个 mixin

// components/FormControl/mixins/control.js

/**
 * control
 * @param {Record} model
 */

const control = (model = {}, propName = 'data') => ({
  model: {
    prop`${propName}`,
    event`update:${propName}`,
  },
  props: {
    [`${propName}`]: {
      typeObject,
      requiredfalse,
      default() => ({}),
    },
    immediate: {
      typeBoolean,
      requiredfalse,
      defaulttrue,
    },
  },
  data() {
    return {
      formData: {
        ...model,
      },
    };
  },
  watch: {
    formData: {
      handler(n) {
        this.$emit(`update:${propName}`, { ...n });
      },
      deeptrue,
    },
  },
  created() {
    this.$watch(
      `${propName}`,
      (newData) => {
        Object.keys(newData).forEach((k) =>
          this.$set(this.formData, k, newData[k])
        );
      },
      { deep: trueimmediatethis.immediate }
    );
  },
});

export default control;

这里我还做了一点小优化:

  1. 有时需要自定义双向绑定的变量名,所以混入的第二个的参数用于自定义变量名,默认值为 data

  2. 前面说过异步场景下监听外部 data 时可以不需要 immediate,于是我把这个监听器改为可控的,通过 immediate 传参控制,在最早可以拿到传参的 created 生命周期手动监听

改造表单组件

使用成本非常低,只需要记住内部数据是存储在 formData 中即可

加上混入后script 标签中的内容简化至 4 行

<!-- components/FormControl/index.vue -->

...
<script>
  import control from './mixins/control';
  export default {
    mixins: [control({ nameundefinedsex'1'ageundefined })],
  };
</script>

最终结果,全部代码

<!-- index.vue -->

<template>
  <form-control v-model="formData" />
</template>

<script>
  import FormControl from './components/FormControl/index.vue';
  export default {
    components: {
      FormControl,
    },
    data() {
      return {
        formData: {
          name'NoraH1to',
          sex'1',
          age23,
        },
      };
    },
  };
</script>
<!-- components/FormControl/index.vue -->

<template>
  <el-form ref="form" :model="formData" label-width="80px">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="formData.name"></el-input>
    </el-form-item>
    <el-form-item label="性别" prop="sex">
      <el-radio v-model="formData.sex" label="1"></el-radio>
      <el-radio v-model="formData.sex" label="2"></el-radio>
    </el-form-item>
    <el-form-item label="年龄" prop="age">
      <el-input-number v-model="formData.age"></el-input-number>
    </el-form-item>
  </el-form>
</template>

<script>
  import control from './control';
  export default {
    mixins: [control({ nameundefinedsex'1'ageundefined })],
  };
</script>
// components/FormControl/mixins/control.js 混入

/**
 * control
 * @param {Record} model
 */

const control = (model = {}, propName = 'data') => ({
  model: {
    prop`${propName}`,
    event`update:${propName}`,
  },
  props: {
    [`${propName}`]: {
      typeObject,
      requiredfalse,
      default() => ({}),
    },
    immediate: {
      typeBoolean,
      requiredfalse,
      defaulttrue,
    },
  },
  data() {
    return {
      formData: {
        ...model,
      },
    };
  },
  watch: {
    formData: {
      handler(n) {
        this.$emit(`update:${propName}`, { ...n });
      },
      deeptrue,
    },
  },
  created() {
    this.$watch(
      `${propName}`,
      (newData) => {
        Object.keys(newData).forEach((k) =>
          this.$set(this.formData, k, newData[k])
        );
      },
      { deep: trueimmediatethis.immediate }
    );
  },
});

export default control;

总结

看到这你会发现,这只是一个很简单的双向绑定封装,是的,但是大家别小看它,这是我后面很多实践的基石,这个混入之后也会进行更高抽象层次的拆解、封装

它贵在通用性、易用性和可扩展性强,并且 vue2 的复用方式真的不多,选择混入属于是无奈之举(下个季度公司项目会逐渐迁移到 vue3,好耶!)

混入非常容易让使用者混乱,大家千万不要做侵入性过强、属性过多的混入封装,一切从简

后续我会写一些组合使用混入达到提效目的的文章,做 B 端开发的同学可以留意下

分类:

前端

标签:

Vue.js

作者介绍

NoraH1to
V1