写这篇文章源于一个问题:如果我可以提出一条建议,来帮助新开发人员编写好的React,那会是什么?
我的答案是:用编写整洁函数(clean functions)的规范来编写整洁的组件(clean components)。
为什么要专注于编写组件(Why focus on writing components)?
我们的目标是编写易于阅读,易于维护和易于扩展的React应用程序。
这涉及很多因素:体系架构(architecture),状态管理(state management),文件结构(file structure),代码格式(code formatting)等。
但是,我们应用程序的大部分(我们团队使用的大部分代码)都会是组件。
如果你的组件都是干净简洁的,那么你的团队就可以更快地向前推进。
这样就可以保证得到一个很好的应用吗? 并不能,因为应用体系架构的其余部分可能是一团糟。
但是用好的组件(干净简洁的组件)更不容易搭建糟糕的架构。
那么我们该如何编写好的组件? 第一步:始终将它们视为函数(always treat them as functions)。
组件作为函数(Components as functions)
一些React组件是函数:
const Button = ({ text, onClick }) => (
<button onClick={onClick}>{text}</button>
)
其他组件不是函数,而是带有render方法的类:
class Button extends Component {
render() {
const { text, onClick } = this.props;
return <button onClick={onClick}>{text}</button>;
}
}
即使在第一种场景(函数组件)中,也很容易让人忽视组件作为函数这一点(it’s easy to stop thinking of components as functions)。我们开始将组件概念化为自己的实体(its own entity),接受不同于函数规则的约束。
对于类组件,人们甚至更容易忘记该组件的核心部分是render方法:一个返回UI片段(a segment of the UI)的函数。
当我们忘记将组件视为函数时,我们会创建庞大且难以推理(hard to reason about)的组件。这些组件做了过多的事情,接收过多的props,或者具有过多的条件,很难使用或是对其进行改进。这类组件总是会让人感到头疼。
始终将组件视为函数(无论它们是基于函数的还是基于类的)是编写好的React的第一步。
这就是为什么。
编写好的函数(Writing great functions)
让我们先放下React,问一个问题:什么才是好的函数?
Robert Martin 经典的《Clean Code》强调了五个因素:
- 小(Small)
- 只做一件事(Does one thing)
- 一个抽象层级(One level of abstraction)
- 少于三个参数(Less than three arguments)
- 描述性名称(Descriptive name)
我们依次讨论上述每一个规则,以及它们对我们的React组件意味着什么。
组件应该足够小(Your component should be small)
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. — Clean Code
函数的第一规则是要短小。第二条规则是还要更短小。— Clean Code
小的函数更容易阅读。 没有人愿意使用一个500行代码的函数。 罗伯特·马丁(Robert Martin)认为函数基本不应该超过20行。
对于React组件,规则有一些不同,因为即使是一个简单的element,JSX也会占用更多行数。
对于你的组件主体(对于类组件,即render方法)50行代码是一个好的规则。
50行是您的组件主体的良好经验法则(对于类组件,即render方法)。 如果查看文件的总行数比较容易,那么绝大多数组件文件都不应超过250行。 低于100行是最理想的。
保证你的组件足够小。
Your component should be small组件应该只做一件事(Your component should do one thing)
关于这个主题,我在我的文章Tiny Components: What could go wrong?中讲了很多。
简而言之,组件应当只做一件事情:基于一个理由而改变(one reason to change)。
如果你决定切换菜单项的顺序而需要更改MenuList.jsx
,那是一个好的行为。 但是,如果当调整了边栏的打开方式,却还需要更改MenuList.jsx
,那就不好的行为方式。
将你的UI拆分成多个只处理一件事的小代码块(tiny chunks)。
Your component should do one thing组件应当只有一个抽象层级(Your component should have one level of abstraction)
这是一个具有多个抽象层级(伪代码)的函数:
const loadThings = async () => {
setIsLoading(true);
const response = await fetchThings();
setIsLoading(false);
const { error, data } = response;
if (error) {
if (error.status === 404) {
redirectTo('/404');
} else if (error.status === 500) {
redirectTo('/error');
}
} else {
const thingsToUpdate = data.ids.reduce((map, id) => {
map[id] = data.things[id];
return map;
}, {});
updateThingsInState(thingsToUpdate);
}
};
我们注意到,loadTings
函数中,一部分功能被抽象为其他函数,包括设置加载状态以及从服务器获取响应。而另一些功能则没有被抽象出来,包括错误时重定向和更新状态。
Note that some things are abstracted away to other functions: setting the loading state and fetching the response from the server. Others are not: redirecting on error, and updating the things in state.
这里有一种更整洁的方法:
const handleResponse = (response) => {
const { error, data } = response;
if (error) {
handleError(error);
} else {
updateThingsInState(data);
}
};
const loadThings = async () => {
setIsLoading(true);
const response = await fetchThings();
setIsLoading(false);
handleResponse(response);
};
现在的loadThings
函数通过逐行调用其他函数来处理与加载数据有关的任务,很容易阅读。 我们的新函数handleResponse
同样很简单,只包含一个条件(containing a single condition)。这样整个函数就只有一个抽象层级了。
这是一个混合抽象(mixed-abstraction)的React组件:
const Dashboard = () => {
return (
<div className="Dashboard">
<header>
<h1>Too Little Abstraction Corp.</h1>
<nav>
<a href="/about">About</a>
<a href="/mission">Mission</a>
<a href="/faq">FAQ</a>
<a href="/contact">Contact</a>
</nav>
</header>
<ProductDescription />
<EmailSubscriptionForm />
<footer>
<h2>Thanks for visiting!</h2>
</footer>
</div>
)
}
一些标记(markup)被抽象为子组件(<ProductDescription />
, <EmailSubscriptionForm />
),但header
和 footer
却不没有。
这也是一个非常简单的例子:在没有规范的情况下,你会遇到将数十行(或数百行)HTML标签与React子组件混合在一起的组件。
Dashboard
组件做了太多事情,有太多的理由来更改这个文件,而且由于缺乏抽象,代码变得很难阅读。
解决方案:
const Dashboard = () => {
return (
<div className="Dashboard">
<Header />
<ProductDescription />
<EmailSubscriptionForm />
<Footer />
</div>
)
}
这样就非常容易阅读了。除非需要再Dashboard
组件中在添加子组件,否则你几乎再也不需要去修改这个文件。
每个子组件也可以根据需要共享和修改。当你修改<Header />
时,也没有破坏<Footer />
的风险。
混合抽象(Mixed abstraction)是一个容易陷入的陷阱,因为在当下它是有意义的(“我只是添加一点标记,它不需要被抽象为自己的组件!”)。但是随着时间的流逝,它会导致难以解析的复杂组件。
如果你尝试将组件大致保持在同一抽象层级上(除了一些不重要的例外,例如包装div
,这是可以接受的),那这些组件将更加易于维护。
将组件限制为同一抽象层级。
Your component should have one level of abstraction组件的参数(props)要尽量少(Your component should have only a few arguments (props))
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.. — Clean Code
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。— Clean Code
是的,严格来说,React组件仅接收两个参数,即props和context。 但是props本质上是函数的参数,也应按上面的原则同样处理。
实际上,编写只带有一个或两个props的组件确实非常困难,特别是一些组件使用props是为了将props传递给子组件。
对于组件有一个更加宽松的规范。三个props会很好(fine),五个props是有代码异味的(code smell,指代码中可能导致深层次问题的症状,译者注),超过七个props会导致严重的危机(crisis).
恰当的构成(Proper composition)可以帮你避免通过多个组件传递props,尽可能尝试在组件树(component tree)中的最低点处对事件进行处理。
附带说明,boolean
类型的props会增加不必要的复杂性。 Filip Danić关于这个主题写了一篇优秀的文章。
将组件的props限制在三个以下。
Your component should have only a few arguments (props)组件应具有描述性名称(Your component should have a descriptive name)
这一点似乎是最简单的,而且应该是!
实际上,你的组件很难命名,说明它做了太多事情。 回答“此组件的作用是什么?” 应该很简单,并用这个答案作为描述性名称。
如果开发人员在浏览你的app的组件树(component tree),那么他应该对每个组件的功能都有一个完整而清晰的了解。这一点没什么好惊喜的。
这是一个更好的规则:扪心自问,“如果我告诉用户这个组件的名称,她能在UI中找到和/或猜测出组件的功能吗?”
组件不应该有技术的、抽象的名称。<TodoListItem>
?很容易理解。<PortfolioLoader>
?更抽象,但仍然直观。<UserViewModelInterface>
?呃…
保证组件名称的具体和描述性。
Your component should have a descriptive name最后的想法(Final thoughts)
你在编写组件时是否遵循了这些规范? 为什么遵循或者为什么没有遵循? 你还遵循了哪些其他规则?
如果你有任何想法,问题或建议,请在评论中告诉我。
谢谢阅读。
网友评论