vlambda博客
学习文章列表

如何设计 React 代码结构?

怎样设计一个项目的文件和组件的结构、甚至是某个组件的内部结构?这个问题永远没有正确答案。

如何设计 React 代码结构?

作者 | Mathilde Wærstad
译者 | 弯月,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)

以下为译文:

像许多话题一样,许多人都持有不同的观点,而且解决方案也有很多。

可能你自己的意见会受到个人经历的影响,比如你对于何谓易读、何谓易解析,甚至何谓漂亮都有自己的看法。当你加入一个团队时,通常你们都需要在这些结构问题上达成一致,这并不是一件易事。

这类的讨论就好像两个人争论哪种颜色最好看,是蓝色还是红色。他们都会主张自己的意见,阐明自己的立场,然后固执己见。他们永远也不会折衷,比如认为紫色最好看。

因此,我并不期望能够在这个问题上达成任何共识。在本文中,我只会介绍在设计React代码结构方面的个人喜好,以及其中的原因。希望你能从中借鉴一二,或者至少可以从不同的角度理解这个问题。


如何设计 React 代码结构?

拼图


首先,我想简单地介绍一下我是哪种程序员。在写代码时,我会需要构建的东西看成基本的组成部分,就像拼图一样。我编写的每个功能、每个函数、每个组件都是一副拼图中的一部分。

我很喜欢将我做的东西可视化。我喜欢用画图的方式解释问题时,我希望能够将用户真正使用的UI功能反映到代码中。

对于我而言,还有一件重要的事情,那就是能迅速看到整体样貌,并尽可能简单地理解组件的功能。这两个目的都可以通过选择正确的命名方式以及可视化结构来实现。


如何设计 React 代码结构?

圣诞老人数字化


在这篇文章中我假想了一个项目,名叫圣诞老人的数字愿望单。所有人都可以使用这个项目,创建用户,添加愿望,还可以查看个人信息。

下面是组件 EditMyInformationToSanta 的代码,显然它需要重构。我们可以通过这个组件添加你希望获得的圣诞礼物。

import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';

const EditMyInformationToSanta = () => {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  const [gender, setGender] = useState(null);
  const [address, setAddress] = useState('');
  const [hasFireplace, setHasFireplace] = useState(null);
  const [naughtyOrNice, setNaughtyOrNice] = useState(null);
  const [letterToSanta, setLetterToSanta] = useState('');
  const [wish, setWish] = useState('');
  const [wishList, setWishList] = useState([]);

  const submitMyInformationToSanta = async event => {
    event.preventDefault();

    await saveMyInformationToSanta({
      name,
      age,
      gender,
      address,
      hasFireplace,
      naughtyOrNice,
      letterToSanta,
      wishList,
    });
  };

  return (
    <div>
      <h1>Hi, Santa! This is me</h1>
      <form>
        <h2>About me:</h2>

        <label>
          <span>My name is:</span>
          <input
            type="text"
            value={name}
            placeholder="Write your name"
            onChange={event =>
 setName(event.target.value)}
          />
        </label>

        <label>
          <span>My age is:</span>
          <input
            type="text"
            value={age}
            placeholder="Tell Santa your age"
            onChange={event =>
 setAge(event.target.value)}
          />
        </label>

        <fieldset>
          <legend>I am a...</legend>
          <label>
            <input
              type="radio"
              value="boy"
              checked={gender === 'boy'}
              onChange={event =>
 setGender(event.target.value)}
            />
            Boy
          </label>
          <label>
            <input
              type="radio"
              value="girl"
              checked={gender === 'girl'}
              onChange={event =>
 setGender(event.target.value)}
            />
            Girl
          </label>
        </fieldset>

        <label>
          <span>My address is:</span>
          <input
            type="text"
            value={address}
            placeholder="Where do you live?"
            onChange={event =>
 setAddress(event.target.value)}
          />
        </label>

        <fieldset>
          <legend>I have a fireplace?</legend>
          <label>
            <input
              type="radio"
              value={true}
              checked={hasFireplace}
              onChange={event =>
 setHasFireplace(event.target.value)}
            />
            Yes
          </label>
          <label>
            <input
              type="radio"
              value={false}
              checked={hasFireplace === false}
              onChange={event =>
 setHasFireplace(event.target.value)}
            />
            No
          </label>
        </fieldset>

        <fieldset>
          <legend>This year I have been naughty or nice?</legend>
          <label>
            <input
              type="radio"
              value="naughty"
              checked={naughtyOrNice === 'naughty'}
              onChange={event =>
 setNaughtyOrNice(event.target.value)}
            />
            Naughty
          </label>

          <label>
            <input
              type="radio"
              value="nice"
              checked={naughtyOrNice === 'nice'}
              onChange={event =>
 setNaughtyOrNice(event.target.value)}
            />
            Nice
          </label>
        </fieldset>

        <div>
          <h2>My wishes this year:</h2>
          <label>
            <span>I want:</span>
            <input
              type="text"
              value={wish}
              placeholder="Write a wish"
              onChange={event =>
 setWish(event.target.value)}
            />
          </label>

          <button
            type="button"
            value="Add wish"
            onClick={() =>
 {
              setWishList(wishList.concat(wish));
              setWish('');
            }}
          />

          <h3>My wish list:</h3>
          <ul>
            {wishList.map(wish => (
              <li>{wish}</li>
            ))}
          </ul>
        </div>

        <div>
          <h2>Santa, I also want to tell you...</h2>
          <textarea
            placeholder="Do you want to say something to Santa?"
            onChange={event =>
 setLetterToSanta(event.target.value)}
            value={letterToSanta}
          />
        </div>

        <button type="submit" onClick={submitMyInformationToSanta} />
      </form>
    </div>
  );
};

export default EditMyInformationToSanta;
组件本身并没有复杂的内容,但由于这个表单十分庞大,包含很多信息,因此文件非常大,或者说非常长。为了改进这一点,我将代码分割成了多个组件。

有几个问题很明显,比如重复的输入框和单选按钮的布局,所以我们可以将此作为切入点。如下便是重构后的代码:

import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';
import TextInputWithLabel from './TextInputWithLabel';
import RadioToggle from './RadioToggle';

const EditMyInformationToSanta = () => {
  const [name, setName] = useState('');

  const [age, setAge] = useState('');
  const [gender, setGender] = useState(null);
  const [address, setAddress] = useState('');
  const [hasFireplace, setHasFireplace] = useState(null);
  const [naughtyOrNice, setNaughtyOrNice] = useState(null);
  const [letterToSanta, setLetterToSanta] = useState('');
  const [wish, setWish] = useState('');
  const [wishList, setWishList] = useState([]);

  const submitMyInformationToSanta = async event => {
    event.preventDefault();

    await saveMyInformationToSanta({
      name,
      age,
      gender,
      address,
      hasFireplace,
      naughtyOrNice,
      letterToSanta,
      wishList,
    });
  };

  return (
    <div>
      <h1>Hi, Santa! This is me</h1>
      <form>
        <h2>About me</h2>

        <TextInputWithLabel
          label="My name is:"
          placeholder="Write your name"
          value={name}
          onChange={event => setName(event.target.value)}
        />

        <TextInputWithLabel
          label="My age is:"
          placeholder="Tell Santa your age"
          value={age}
          onChange={event => setAge(event.target.value)}
        />

        <RadioToggle
          question="I am a..."
          label1="Boy"
          toggleValue1="boy"
          label2="Girl"
          toggleValue2="girl"
          value={gender}
          onChange={event => setGender(event.target.value)}
        />

        <TextInputWithLabel
          label="My address is:"
          placeholder="Where do you live?"
          value={address}
          onChange={event => setAddress(event.target.value)}
        />

        <RadioToggle
          question="I have a fireplace?"
          label1="Yes"
          toggleValue1={true}
          label2="No"
          toggleValue2={false}
          value={hasFireplace}
          onChange={event => setHasFireplace(event.target.value)}
        />

        <RadioToggle
          question="This year I have been naughty or nice?"
          label1="Naughty"
          toggleValue1="naughty"
          label2="Nice"
          toggleValue2="nice"
          value={naughtyOrNice}
          onChange={event => setNaughtyOrNice(event.target.value)}
        />

        <div>
          <h2>My wishes this year:</h2>
          <TextInputWithLabel
            label="I want:"
            placeholder="Write a wish"
            value={wish}
            onChange={event => setWish(event.target.value)}
          />

          <button
            type="button"
            value="Add wish"
            onClick={() => {
              setWishList(wishList.concat(wish));
              setWish('');
            }}
          />

          <h3>My wish list:</h3>
          <ul>
            {wishList.map(wish => (
              <li>{wish}</li>
            ))}
          </ul>
        </div>

        <div>
          <h2>Santa, I also want to tell you...</h2>
          <textarea
            placeholder="Do you want to say something to Santa?"
            onChange={event => setLetterToSanta(event.target.value)}
            value={letterToSanta}
          />
        </div>

        <button type="submit" onClick={submitMyInformationToSanta} />
      </form>
    </div>
  );
};

export default EditMyInformationToSanta;
注意,尽管我不确定输入框和单选按钮是否会在应用程序的其他地方使用,但我还是决定将它们移动到单独的文件中。这些文件应该保存在尽可能靠近原来组件的位置,以方便使用。


如何设计 React 代码结构?

保持组件分离!


许多开发人员喜欢将类似上例中的局部组件放到一个文件中,但我倾向于一个文件最多只保存一个组件。对于我来说,在理解代码全貌的时候,一个文件只包含一个组件的形式比一个文件中包含多个组件更容易。

在本例中,差别也许不是太大,由于我要重构的组件并不是太大,但组件规模增大后问题就会浮现。几个星期后你就不得不在文件中来回上下滚动才能找到“根”组件,这会非常麻烦。

我喜欢简单的规则,这样就可以避免重构时的内部讨论,也不需要讨论什么时候应该将组件放到单独的文件中。


如何设计 React 代码结构?

按照域提取


回到我们的例子。尽管我们将重复的代码提取到了可重用的组件中,我认为依然还有可以重构的地方。我想按照代码的功能,重构同一功能的代码。在下面的例子中,我把表单每一部分的组件都提取出来,分别是AboutMe、LetterToSanta 和 MyWishes。

import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';
import AboutMe from './AboutMe';
import MyWishes from './MyWishes';
import LetterToSanta from './LetterToSanta';

const EditMyInformationToSanta = () => {
  const [me, setMeState] = useState({
    name'',
    age'',
    address'',
    gendernull,
    hasFireplacenull,
    naughtyOrNicenull,
  });

  const [letterToSanta, setLetterToSanta] = useState('');
  const [wish, setWish] = useState('');
  const [wishList, setWishList] = useState([]);

  const submitMyInformationToSanta = async event => {
    event.preventDefault();

    await saveMyInformationToSanta({
      ...me,
      letterToSanta,
      wishList,
    });
  };

  return (
    <div>
      <h1>Hi, Santa! This is me</h1>
      <form>
        <AboutMe me={me} onMeChange={updatedMeState => setMeState(updatedMeState)} />
        <MyWishes wish={wish} wishList={wishList} onWishChange={setWish} onWishListChange={setWishList} /> 
        <LetterToSanta letterToSanta={letterToSanta} onLetterChange={setLetterToSanta} />

        <button type="submit" onClick={submitMyInformationToSanta} />
      </form>
    </div>
  );
};

export default EditMyInformationToSanta;
项目的完整代码在此(https://github.com/mathilwa/WishesToSanta)。

现在,这个目录中除了文件 EditMyInformationToSanta.jsx 之外,还有一堆简单的组件文件。每个文件都很小,很容易单独理解。

表单每个部分对应的组件都仅限于 EditMyInformationToSanta.jsx 文件使用,因此我将它们放到了同一个目录下。重构的目的是为了让主文件看起来干净、容易理解。我们也可以将其他相关的文件放在这个目录中,比如样式、图像、文本、工具函数或其他资源等。


如何设计 React 代码结构?

域 vs 组件


应用程序会不断增长,例如利用 SantaLocation API 来实时跟踪圣诞老人的位置,或者显示前十个最期待的礼物,总有一天你会遇到重用表现层组件的情况。我喜欢将代码分成两个目录:/components 和 /domain,目的是将表现层组件分离开来。

/domain 包含应用程序的域逻辑。EditMyInformationToSanta 以及相关的文件就在这个目录下。

/components 包含整个项目中所有可重用的组件。典型的例子就是 TextInputWithLabel,我们将它从 /domain/edit-my-information-to-santa 目录移动到 /components/text-input-with-label。与 /domain 目录一样,这个目录也可以包含相关文件,如文本、样式等。


如何设计 React 代码结构?

保持整洁


我们刚才一直在讨论怎样设计React代码的布局。总结起来就是:

我喜欢将表现层逻辑提取到单独的组件中,这样利于重用,也利于简化根组件。

我将每个组件放到各自的文件中,即使用户界面变得十分复杂,也应该遵循这一条原则。原因在于,每个文件仅包含一个组件更加方便理解。每个文件都很短,因此可以看到整体情况,而好的组件命名(以及文件命名)可以帮助我们理解每个组件的作用。

将组件分割成一次性的域逻辑组件和可重用的组件,可以让浏览代码和删除代码更加容易。

作为一名咨询师,你需要花大量时间去阅读别人的代码。确保阅读代码的过程顺利非常重要。大多数时间我都在修改或扩展已有代码,而简单、健壮、易于理解的代码结构可以帮你更快地完成工作。


接受他人的意见


最后我想用本文开头的观点来结束本文。团队中不同的人有不同的选择和喜好,接受这一点并不容易。但一定要接受吗?我想说,是的。并不是说你要接受关于什么是美观,而是要接受在项目中使用某种代码风格和结构,即使有些人并不完全同意。

让整个团队都在“整洁”或“易阅”问题上达成一致是不现实的。最重要的是,每个人都会表达自己的意见,每个人都会强调为何使用这种方式,并积极讨论团队应该使用这种方式。团队中的大多数人(即使不是全部)都会发表观点,但我认为,最好能够接受统一的代码风格和结构,而不是为了满足每个人的喜好而混合多种风格。

不管最后的选择是什么,接受统一的解决方案,才能在项目中创建整洁、干净、易于理解且标准化的代码风格。

原文:https://react.christmas/2019/22

本文为 CSDN 翻译,转载请注明来源出处。

热 文 推 荐 




你点的每个“在看”,我都认真当成了喜欢