React组件设计原则-单一职责原则 (SRP)

单一职责原则 (SRP)

每个函数/类/组件都应该只做一件事

坦率地说,这是在用 React 编写前端时要遵循的最重要的原则。SRP 鼓励我们将代码从包含数千行的整体文件分割成数十个 50-100 行的较小文件。这是因为它鼓励我们将文件中的功能提取到单独的函数中,这样我们的代码库就会变得更加模块化。这使得它更易于维护,因为可以很容易地看到某些特定功能的各种活动部件。它还使我们的代码库更加健壮,因为测试许多较小的独立文件比测试一个大文件要容易得多。简而言之,如果您正在努力实施良好的测试,或者您的文件经常超过 150 行代码,这可能表明您需要进一步分割代码。

注意: 50-150 行代码没有什么特别之处。这不是硬性规定。它可以简单地用作您需要考虑是否可以提取更多功能的可能指示。

大的功能/组件通常表明它们在做不止一件事;尽量保持功能/组件小以确保模块化。

“一件事”是什么意思?

我已经多次提到“一件事”这个短语,所以尝试辨别它的含义可能很有用。Clean Code的作者和 SOLID 原则的鼻祖 Robert Martin是这样定义它的:[4]

“如果你不能从一个函数中有意义地提取另一个函数,那么它只做一件事。如果一个函数包含代码,并且你可以从中提取另一个函数,那么原来的函数做了不止一件事。” - Robert C Martin,干净的代码,第 1 课 [5]

你把它带到什么水平取决于你;正如我将多次提到的,热心遵循任何原则弊大于利。但是,这确实为我们提供了一个起点和一个在不确定时可以遵循的一般经验法则。

在 React 中使用它

用 React 的话说,我们应该模块化我们的组件,让每个组件负责一件事,而不是制作包含我们整个应用程序的臃肿组件。但是,这是否意味着“一块 UI”或“一块 UI 关联的一块逻辑”?例如,下面的组件将从 API 中获取待办事项列表,将前 10 个切片,并将其显示给用户(尽管有错误处理)。<TodosPage />

const TodosPage = () => {
    const [todos, setTodos] = useState([]);

    // 1. Fetching data from API.
    useEffect(() => {
        async function getTodos() {
            const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
            const firstTen = data.slice(0, 10);
            setTodos(firstTen);
        };
        getTodos();
    }, []);

    // 2. Converting todo array into list of React elements.
    const renderTodos = () => {
        return todos.map(todo => {
            return (
                <li>
                    {`ID: ${todo.id}, Title: ${todo.title}`}
                </li>
            )
        });
    };

    // 3. Structuring and displaying the todos.
    return (
        <div>
            <h1>My Todos:</h1>
            <ul>
                {renderTodos()}
            </ul>
        </div>
    )
};

让我们考虑一下我们实际上在这里做什么:

  1. 我们正在从外部 API 获取一些待办事项。
  2. 我们正在将这些变成一些要显示的元素列表。
  3. 我们正在向我们的用户显示列表。

实际上,我们的组件不应该关心 todos 从哪里来,或者它们以什么格式显示。所有人都应该关心它实际向用户显示我们的待办事项列表,所以我们应该将这个组件分成两部分:<TodosPage /><TodosPage />

  • <TodosPage />这将向我们的用户显示包含我们的待办事项的页面。
  • <TodosList />它将处理列表的实际创建。
const TodosPage = () => {
  return (
    <div>
      <h1>My Todos</h1>
      <TodosList />
    </div>
  )
};

const TodosList = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const firstTen = data.slice(0, 10);
          setTodos(firstTen);
      };
      getTodos();
  }, []);


  const renderTodos = () => {
      return todos.map(todo => {
          return (
              <li>
                  {`ID: ${todo.id}, Title: ${todo.title}`}
              </li>
          )
      });
  };

  return <ul>{renderTodos()}</ul>;
}

我们开始分离出 UI,但实际上我们只是将大部分责任转移到 UI 中。所以让我们再分手一点。首先,我们可以提取出渲染每个待办事项的细节,而不是每次渲染一个单独的。<TodosList /><TodosList /><TodoItem />

const TodosList = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const firstTen = data.slice(0, 10);
          setTodos(firstTen);
      };
      getTodos();
  }, []);

  const renderTodos = () => {
      return todos.map(todo => {
          return <Todoitem id={todo.id} title={todo.title} />
      });
  };

  return <ul>{renderTodos()}</ul>;
}

const TodoItem = ({id, title}) => {
  return <li>{`ID: ${id}, Title: ${title}`}</li>
};

同样,这要好一些——我们将每个待办事项的实际格式卸载到不同的组件中,这样我们的组件就做得更少了。在这一点上,我们的 UI 与之前相比已经相对破碎了。但是,如果我们想分离出逻辑,仍然有 API 调用看起来有点乱。相反,如果我们可以将它需要的待办事项列表作为道具传递,那可能会很好。这将极大地清理我们的组件,并使其完全负责 UI,而不是混合 UI 和逻辑。<TodosList /><TodosList />

const TodosList = ({todos}) => {

  const renderTodos = () => {
      return todos.map(todo => {
          return <Todoitem id={todo.id} title={todo.title} />
      });
  };

  return <ul>{renderTodos()}</ul>;
}

要做到这一点(双关语),我们可以将我们包装在一个组件中,一旦从 API 检索到待办事项就将其传入。我们可以使用组件上的元素来做到这一点,并在 using 中传递道具。<TodosList /><APIWrapper />props.childrenReact.Children.map()

const APIWrapper = ({children}) => {

  const [todos, setTodos] = useState([]);

  useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const firstTen = data.slice(0, 10);
          setTodos(firstTen);
      };
      getTodos();
  }, []);

  const todoListWithTodos = React.Children.map(
    children,
    (child) => {
      return React.cloneElement(child, { todos: todos })
    }
  )
  
  return (
    <div>
      {todos.length > 0 ? todoListWithTodos : null}
    </div>
  )
};

所以我们有了它,我们的最终代码完全遵守 SRP:

// The main component in our web application which controls the TodosPage.
const TodosPage = () => {
  return (
    <div>
      <h1>My Todos</h1>
      <APIWrapper>
        <TodosList />
      <APIWrapper />
    </div>
  )
}
// The subcomponents we have created to modularise our program.
const APIWrapper = ({children}) => {

  const [todos, setTodos] = useState([]);

  useEffect(() => {
      async function getTodos() {
          const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
          const firstTen = data.slice(0, 10);
          setTodos(firstTen);
      };
      getTodos();
  }, []);

  const todoListWithTodos = React.Children.map(
    children,
    (child) => {
      return React.cloneElement(child, { todos: todos })
    }
  )
  
  return (
    <div>
      {todos.length > 0 ? todoListWithTodos : null}
    </div>
  )
}

const TodosList = ({todos}) => {

  const renderTodos = () => {
      return todos.map(todo => {
          return <Todoitem id={todo.id} title={todo.title} />
      });
  };

  return <ul>{renderTodos()}</ul>;
}

const TodoItem = ({id, title}) => {
  return <li>{`ID: ${id}, Title: ${title}`}</li>
}

现在,这可能是一个人为的例子,但它说明了每个组件只关心一件事的想法:

  • <TodosPage />不关心待办事项,它们是如何检索的,或者它们是如何格式化的。它只知道它需要显示一个包含它们的页面。
  • <APIWrapper />不关心格式化任何东西或待办事项。它只是处理检索它们并通过 TodosList 发送它们。
  • <TodosList />不关心 todos 从哪里来,它只知道它得到了一个 todos 列表并且应该显示一些区域来渲染它们。
  • <TodoItem />不关心有多少待办事项,它们来自哪里,或者它们将显示在哪个页面上。它只知道它会收到一个idand title,并且应该返回一个包含该信息的。<li>

这使得我们的代码库更加模块化并且更易于维护,因为每个组件只处理一件事。

过度

但是,这个解决方案是否比我们开始时的解决方案更复杂?这又是包含所有内容的原始组件。

const TodosPage = () => {
    const [todos, setTodos] = useState([]);

    useEffect(() => {
        async function getTodos() {
            const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/");
            const firstTen = data.slice(0, 10);
            setTodos(firstTen);
        };
        getTodos();
    }, []);

    const renderTodos = () => {
        return todos.map(todo => {
            return (
                <li>
                    {`ID: ${todo.id}, Title: ${todo.title}`}
                </li>
            )
        });
    };

    return (
        <div>
            <h1>My Todos:</h1>
            <ul>
                {renderTodos()}
            </ul>
        </div>
    )
};

可以争辩说,是的;将所有事情分散太多实际上会导致对正在发生的事情的了解减少。任何看我们原作的人都可以相对容易地看到发生的一切,但是,当每条逻辑都被提取成最小的形式时,我们可能会开始失去洞察力。如果我们想了解该组件的工作原理,我们实际上需要调出所有四个组件。我们可以将此称为过度碎片化<TodosPage /><TodosPage />

我在这里想说明的是,这些原则在这里至少有助于实现我们在开始时谈到的三个主要目标中的一个:

  • 为我们的用例介绍所需的功能。
  • 通过使其更易于阅读、更具可扩展性等,使我们的代码更易于维护。
  • 通过提高代码的可测试性或添加更好的错误处理来提高我们代码的健壮性。

当武断地遵循一项原则对这些目标起反作用时,可能是时候后退一步了。

碎片化组件的“React-Way”

上面展示的模式(将 UI 组件与逻辑组件分开)可以被认为具有处理逻辑和外部性 ( ) 的“容器”组件,以及完全处理 UI ( ) 的“展示”组件。这是 Dan Abramov 认可的一种模式,目的是为了整齐地组织代码,并将我们关注的区域分开。[6]然而,自从钩子发布以来,我们有了一种不同的分离方法:自定义钩子。我们现在不会深入研究它们,只是举例说明如何使用自定义挂钩来分离我们的,同时又不会失去对它正在做什么的看法。<APIWrapper /><TodosList /><TodosPage />

const TodosPage = () => {
    const todos = useTodos();

    const renderTodos = () => {
        return todos.map(todo => {
            return <Todoitem id={todo.id} title={todo.title} />
        });
    };

    return (
        <div>
            <h1>My Todos:</h1>
            <ul>
                {renderTodos()}
            </ul>
        </div>
    )
};

const TodoItem = ({id, title}) => {
  return <li>{`ID: ${id}, Title: ${title}`}</li>
};

function useTodos(){
    const [todos, setTodos] = useState([]);

    useEffect(() => {
        async function getTodos() {
            const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos");
            setTodos(data);
        };
        getTodos();
    }, []);

    return todos;
};

在这里,我们已经将 API 逻辑提取到自定义挂钩中。我们还将每个单独的待办事项的格式提取到组件中。我相信这在遵循 SRP 和让读者清楚我们的组件正在做什么之间取得了平衡。这些组件/功能中的每一个也可以相对容易地进行测试,因为我们可以在测试时简单地模拟功能,并进行自己的测试。useTodos()<TodoItem /><TodosPage />useTodos()<TodosPage />useTodos()

使用单独的组件和自定义挂钩的组合来模块化更大的组件。

相关标签:
  • React组件设计原则
  • 职责原则
0人点赞

发表评论

当前游客模式,请登陆发言

所有评论(0)