响应表单开发。实践中的KISS,YAGNI,DRY原则

您好,在本教程中,我们将研究如何在React中开发一个非常简单但可控制的表单,重点是代码的质量。

在开发表格时,我们将遵循“ KISS”,“ YAGNI”,“ DRY”的原则。要成功完成本教程,您不需要了解这些原理,我将在此过程中进行解释。但是,我相信您对现代javascript有很好的掌握,并且可以在React中考虑



教程结构:







为了事业!使用KISS和YAGNI编写简单的表格


因此,让我们假设我们有一个任务来实现授权表单:
复制代码
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



我们通过分析KISS和YAGNI的原理开始我们的开发,暂时忘记了其余的原理。

吻-“让代码简单明了。”我认为您熟悉简单代码的概念。但是“哑”代码是什么意思?以我的理解,这是使用最少数目的抽象来解决问题的代码,而这些抽象之间的嵌套也很少。

YAGNI-“您将不需要它。”该代码应该只能执行其编写的内容。我们不会创建以后可能需要的任何功能,也不会创建使我们认为更好的应用程序。我们仅执行任务执行所需的特定工作。

让我们严格遵循以下原则,但还要考虑:

  • initialDataonSubmit用于LogInForm来自顶部(这是一种有用的技术,尤其是当形式应该能够处理createupdate在同一时间)
  • 每个领域都必须有 label

另外,让我们错过样式化,因为它对我们而言并不有趣且有效,因为这是单独教程的主题。

请按照上述原则自行实施表格。

我的表格执行
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your nickname
        <input
          value={logInData.nickname}
          onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
        />
      </label>
      <label>
        Enter your email
        <input
          type="email"
          value={logInData.email}
          onChange={e => setLogInData({ ...logInData, email: e.target.value })}
        />
      </label>
      <label>
        Enter your password
        <input
          type="password"
          value={logInData.password}
          onChange={e => setLogInData({ ...logInData, password: e.target.value })}
        />
      </label>
      <button>Submit</button>
    </form>
  );
};
    






您的决定很可能会有所不同,因为每个开发人员的想法都不相同。您可以为每个字段创建自己的状态,或将处理程序函数放在单独的变量中,无论如何这都不重要。

但是,如果立即为所有字段创建了一个处理函数,那么这将不再是解决该问题的“最笨拙”的方法。我们现阶段的目标是创建最简单,最“笨”的代码,以便以后我们可以查看整个图片并选择最佳的抽象。

如果您使用的代码完全相同,那么这很酷,这意味着我们的想法会融合!

此外,我们将使用此代码。这很简单,但仍远非理想。




重构和干


现在是应对DRY原则的时候了。

DRY,简化的措词-“请勿重复您的代码。” 该原理看起来很简单,但是有一个陷阱:要摆脱代码重复,您需要创建抽象。如果这些抽象不够好,我们将违反KISS原则。

同样重要的是要了解不需要DRY即可更快地编写代码。它的任务是简化对我们解决方案的阅读和支持。因此,不要急于立即创建抽象。最好对代码的某些部分进行简单的实现,然后分析您需要创建哪些抽象,以简化代码的读取并减少需要更改的地方的数量。

正确抽象的清单:
  • 抽象的名称与其目的完全一致
  • 抽象执行特定的,可理解的任务
  • 读取从中提取了抽象的代码得到了改进

因此,让我们开始重构。
而且我们已经声明了重复的代码:
复制代码
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



此代码复制了2个元素的组成:label、、input让我们将它们合并为一个新的抽象InputField
复制代码
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



现在我们的LogInForm看起来像这样:
复制代码
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={e => setLogInData({ ...logInData, email: e.target.value })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={e => setLogInData({ ...logInData, password: e.target.value })}
      />
      <button>Submit</button>
    </form>
  );
};



阅读变得更加容易。抽象的名称与它要解决的任务相对应。该组件的目的很明显。代码变得更少了。因此,我们正朝着正确的方向前进!

现在很明显,InputField.onChange逻辑是重复的。
那里发生的事情可以分为两个阶段:

复制代码
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



第一个功能描述了从事件获取值的详细信息input。我们可以选择2种抽象来存储此逻辑:InputFieldLogInForm

为了正确地确定我们需要引用我们的代码的抽象,我们必须转向DRY原理的完整表述:“知识的每个部分在系统中都应具有唯一,一致和权威的表示形式。”

具体示例中的部分知识是知道如何从中的事件获取价值input。如果我们将这些知识存储在我们的知识中LogInForm,那么很明显,在使用我们的知识InputField在另一种形式中,我们将不得不复制我们的知识,或者将其转化为单独的抽象并从那里使用它。并且基于KISS原则,我们应该具有尽可能少的抽象数。的确,如果我们只需将这种逻辑放入我们的逻辑中InputField,而外部代码将不了解输入内部的工作方式,那么为什么需要创建另一个抽象InputField它只是具有现成的含义,就像它向内传递一样。

如果您对将来可能需要举办的活动感到困惑,请记住YAGNI原则。您可以随时onChangeEvent向我们的组件添加一个额外的道具InputField

在此之前,InputField它将看起来像这样:
复制代码
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



因此,在向组件输入和输出期间观察到类型的一致性,并且隐藏了外部代码所发生事件的真实本质。如果将来我们需要另一个ui组件,例如checkbox或select,那么在其中我们还将保持输入输出的类型统一性。

这种方法为我们提供了更大的灵活性,因为我们的表单可以与任何输入输出源一起使用,而无需为每个输入源生成其他唯一的处理程序。

默认情况下,许多框架中都内置了这种启发式技巧。例如,这是许多人都喜欢的主要思想v-modelVue因为它易于使用表单。

让我们回到业务上,LogInForm根据中的更改更新我们的组件InputField
复制代码
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={nickname => setLogInData({ ...logInData, nickname })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={email => setLogInData({ ...logInData, email })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={password => setLogInData({ ...logInData, password })}
      />
      <button>Submit</button>
    </form>
  );
};



它看起来已经不错,但我们可以做得更好!

Callback传入的onChange总是做相同的事情。其中只有密钥被更改:密码,电子邮件,昵称。因此,我们可以将其替换为对函数的调用:handleChange('password')

让我们实现这个功能:
复制代码
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



如您所见,该函数接收参数并将其存储在处理程序的闭包中,处理程序立即返回到外部代码。似乎一次调用一个参数的函数也称为curried。

让我们看一下结果代码:
复制代码
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    const handleChange = fieldName => fieldValue => {
      setLogInData({
        ...logInData,
        [fieldName]: fieldValue,
      });
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };
  
  // InputField.js
  const InputField = ({ type, label, value, onChange }) => (
    <label>
      {label}
      <input type={type} value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );



该代码是任务的简短,简洁和适度的声明式实现。我认为,在任务中进行进一步的重构是没有意义的。




还有什么可以做的?



如果项目中有很多表单,则可以将handleChange计算放在单独的钩子中useFieldChange
复制代码
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



由于这是一个纯函数(意味着第一次调用它,因为它总是返回相同的函数),因此它不必是一个钩子。但是从概念上讲,该挂钩看起来是React的更正确,更自然的解决方案。

您还可以添加callback就地支持fieldValue完全复制通常行为setStateReact
复制代码
  const isFunc = val => typeof val === "function";

  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: isFunc(fieldValue) ? fieldValue(state[fieldName]) : fieldValue,
    }));
  };



与我们的表单一起使用的示例:
复制代码
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
    const handleChange = useFieldChange(setLogInData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };



但是,如果您的应用程序只有一种形式,则无需执行所有这些操作!因为它与YAGNI原则相矛盾,因此我们不应该执行解决特定问题所需的操作。如果您只有一种形式,那么这种运动的真正好处是不够的。毕竟,我们仅将组件的代码减少了三行,但是引入了一个额外的抽象,隐藏了某种形式的逻辑,最好保留在表面上。




不好了!请不要这样做!



异型配置表单


锁定的配置类似于webpack的配置,仅用于表单。

最好举个例子,看下面的代码:
复制代码
  const Form = () => (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={state.nickname}
        onChange={handleChange('nickname')}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={state.email}
        onChange={handleChange('email')}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={state.password}
        onChange={handleChange('password')}
      />
      <button>Submit</button>
    </form>
  );



似乎有些代码在这里重复了,因为我们调用了相同的InputField组件,并在其中传递了相同的标签,值和onChange参数。并且他们开始将自己的代码干燥-yav,以避免虚构的重复。
他们经常这样做:
复制代码
const fields = [
  {
    name: 'nickname',
    label: 'Enter your nickname',
  },
  {
    type: 'email',
    name: 'email',
    label: 'Enter your email',
  },
  {
    type: 'password',
    name: 'password',
    label: 'Enter your password',
  },
];

const Form = () => (
  <form onSubmit={handleSubmit}>
    {fields.map(({ type, name, label }) => (
      <InputField
        type={type}
        label={label}
        value={state[name]}
        onChange={handleChange(name)}
      />
    ))}
    <button>Submit</button>
  </form>
);



结果,使用17行jsx代码,我们获得16行配置。太棒了!这就是我对DRY的了解。如果这里有100个输入,则分别得到605和506行。

但是,结果,我们得到了更复杂的代码,违反了KISS原则。确实,现在它包含2个抽象:字段和算法(是的,算法也是抽象),将其转变为反应元素树。阅读此代码时,我们将不得不在这些抽象之间不断跳转。

但是此代码的最大问题是它的支持和扩展。
想象一下,如果发生以下情况,他会怎么做:
  • 将添加更多具有不同可能属性的字段类型
  • 字段是否根据其他字段的输入进行渲染
  • 更改时某些字段需要一些其他处理
  • 选择中的值取决于先前的选择

此列表可以持续很长时间...

要实现此目的,您将始终必须在配置中存储一些功能,并且呈现配置的代码将逐渐变成科学怪人,需要使用许多不同的道具并将它们扔到不同的组件上。

根据组件的组成,之前编写的代码将根据您的需要悄悄地扩展,更改,而不会由于缺乏中间抽象而变得非常复杂-一种从我们的配置中构建React元素树的算法。此代码不是重复的。它只是遵循某种设计模式,因此看起来是“有图案的”。

无用的优化


在React中出现钩子之后,有一种将所有处理程序和组件随意包装在useCallback和中的趋势memo请不要那样做!这些钩子不是由React开发人员提供的,因为React速度很慢,所有东西都需要优化。它们为您在遇到性能问题时优化应用程序提供了空间。即使遇到此类问题,也无需将整个项目包装在memo和中useCallback使用Profiler识别问题,然后在正确的位置进行记录。

记忆总是会使您的代码更复杂,但并不总是能提高效率。

让我们来看一个真实项目中的代码。比较带有useCallback和不带有该函数的外观
复制代码
  const applyFilters = useCallback(() => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  }, [selectedMetricsStatus, onApplyFilterClick]);

  const applyFilters = () => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  };



删除包装后,代码的可读性明显提高,因为理想的代码是缺少代码。

该代码的性能并未提高,因为该函数的用法如下:
复制代码
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



位置RightFooterButton-仅styled.button来自styled-components,它将很快更新。但是内存消耗增加了我们的应用程序,因为React将始终保留在内存中selectedMetricsStatusonApplyFilterClick并且函数的版本与applyFilters这些依赖项相关。

如果这些参数对您来说还不够,请阅读讨论该主题文章




发现


  • 反应形式很容易。由于开发人员本身和React文档的缘故,它们出现了问题,在该主题中没有足够详细地公开此主题。
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


最初,我计划撰写有关复杂嵌套表单的声明式验证的文章。但是,最后,我决定让观众为此做好准备,以便使图片尽可能完整。
以下教程将关于验证本教程中实现的简单表单。
非常感谢阅读到底的每个人!

不论您如何编写代码或做什么都无关紧要,主要是享受它。

All Articles