【第1334期】组件开发的单元素模式
前言
今日早读文章由饿了么@JeLewine翻译授权分享。
@JeLewine,坐标帝都,废柴宅男,不断学习的技术渣。95后,热爱游戏(PS4 && Blizzard && Board game),拖延癌晚期。
正文从这开始~
使用react和其他基于组件的库构建可靠基础组件的基本准则与最佳实践
早在2002年——当我开始从事web开发时,包括我在内的大多数开发者都在使用<table>
标签来构建页面的布局。
到了2005年我才开始遵循web标准
当一个网站或网页被称为符合web标准时,通常意味着该网站/网页具有有效的HTML,CSS和JavaScript。HTML还应当具有可访问性和遵循语义化的准则。
我学习到了语义化与可访问性,然后开始使用正确的HTML标签和外部CSS。我非常骄傲的将W3C徽章放在了我制作的每一个站点上。
我编写的HTML代码与经过浏览器输出出来的代码几乎相同。这也意味着通过使用W3C校验器和其他工具可以帮助我写出更好的代码。
时间流逝。为了去分离前端复用的部分,我开始使用PHP,模板系统,JQuery,Polymer,Angular和React。特别是后者,这三年来我一直都在使用。
伴随着时间的推移,我们编写的代码合最终呈现出给用户的代码越来越不同。如今,我们以许多不同的方式来编写代码(例如,使用babel和typescript)。我们写的是ES2015和JSX,但是最后的输出代码却仅仅是普通的HTML和JavaScript。
今天,即使我们依然可以使用W3C校验器去校验我们的网站,但是这却不会再对我们的代码编写有帮助了。我们仍然在追求最佳实践去让我们的代码具有一致性和可维护性。而且,如果你正在阅读这篇文章,我想你也在寻找同样的东西。
现在,我想给你看个大宝贝。
单元素模式(Singel)
我不知道至今为止我已经写了多少个组件。不过,如果把Polymer,Angular和React加一起,我可以打包票说:’至少超过了1000个’。
除了公司的项目,我还维护着一个带有40多个样例组件的React脚手架。此外,我还在与Raphael Thomazella合作。他也为这个项目做贡献,在一个UI库中大量使用了他们。
很多开发者都有一个误解,就是如果他们以完美的文件结构启动项目,他们就不会有问题。不过事实上,文件结构的一致性并不重要。如果你的组件不遵循一些明确定义的规则,那么你的项目最终都将会变得难以维护。
在创建和维护了这么多组件之后,我发现了一些使它们更加一致和可靠的特性,这样用起来能够让人更加舒服。一个组件如果越像HTML元素,他就会变得越可靠。
没有什么是比
<div>
更可靠的了
【译者注:评论有一条评论被点赞最多,非常有趣——“我信任<div>
胜过我爸爸”】
当使用一个组件的时候,你应该问问你自己以下列表中一个或多个问题:
问题1:如果我需要将props传递给嵌套元素会怎样?
问题2:这么做有可能会出于某种原因中断程序运行么?
问题3:如果我想传递id或其他HTML属性呢?
问题4:我可以通过className或Style属性设置样式么?
问题5:事件响应怎么处理?
可靠性意味着,在当前上下文内,不需要打开文件去查看代码来理解它是怎样工作的。例如,如果你正在使用<div>
,你将会立刻知道下面问题的答案:
规则1:只渲染一个元素
规则2:永远不中断程序
规则3:渲染所有作为属性来传递来的HTML属性
规则4:始终会合并作为属性传递的样式
规则5:添加所有作为属性传递的事件处理函数
这就是我们称之为单元素模式(Singel)的一组规则!
重构驱动开发
先让它跑起来,然后再使它变得更好
当然,想让所有的组件都遵循Singel是不可能的。在一些时候——事实上,在许多时候——你一定会打破第一条准则。
应当遵循这些规则的组件是应用程序中最重要的部分:原子,基石,元素或者你称之为基础组件的任何内容。在这篇文章里,我会将它们都称作单元素。
这其中有一些是很容易立即抽象出来的:Button
,Image
,Input
。换句话说,就是那些与HTML元素有直接关系的组件。在其他一些情况下,你只能在开始复用代码时认出它们。不过那没关系。
通常,每当你需要更改某个组件,添加一些新功能或修复错误时,你会看到或者开始写一些重复的样式和行为。这是将它们抽象成单元素的信号。
与其他组件相比,代码中单元素的百分比越高,你的程序会越容易维护和具有一致性。
将它们放到一个单独的文件夹中——那些元素,原子或是基石。每当你从中导入一些组件时,你就会清楚的知道它所遵循的规则。
一个例子
在这篇文章中,我将会把重点放在React上。相同的规则可以应用于任何基于组件的库之上。
举例来说,假设我们拥有一个Card
组件。它由Card.js
和Card.css
构成,我们有.card
,.top-bar
,.avatar
和其他类选择器的样式。
const Card = ({ profile, imageUrl, imageAlt, title, description }) => (
<div className="card">
<div className="top-bar">
<img className="avatar" src={profile.photoUrl} alt={profile.photoAlt} />
<div className="username">{profile.username}</div>
</div>
<img className="image" src={imageUrl} alt={imageAlt} />
<div className="content">
<h2 className="title">{title}</h2>
<p className="description">{description}</p>
</div>
</div>
);
在某些时候,我们必须将头像放在应用的另一部分。我们将创建一个新的单元素Avatar
,而不是复制HTML和CSS,以便我们可以重用它。
规则1:只渲染一个元素
新的Avatar
由Avatar.js
和Avatar.css
组成,它具有我们从Card.css
中提取的.avatar
样式。它呈现出来只是一个<img>
:
const Avatar = ({ profile, ...props }) => (
<img
className="avatar"
src={profile.photoSrc}
alt={profile.photoAlt}
{...props}
/>
);
下面的例子是我们如何在Card和应用的其他部分去使用它:
<Avatar profile={profile} />
规则2:永远不会中断程序
即使你没有传递src
属性,<img>
也不会打断应用。即便这是一个必须的属性。不过,在我们的组件中,如果不传递profile
,整个应用将会被打断。
React16提供了一个名为componentDidCatch的新的生命周期方法,可用于优雅的处理组件内的各种错误。尽管在你的应用中使用error boundaries是一种很好的做法,但它仍然可能会掩盖掉我们单元素中的错误。
我们必须要确保Avatar
本身是可靠的,而且我们要假设它的父组件可能不会提供那些必要的props。在这种情况下,我们在使用profile
之前需要先检查一下其是否存在。我们应该使用诸如Flow
,Typescript
或PropTypes
之类的工具去发出警告
const Avatar = ({ profile, ...props }) => (
<img
className="avatar"
src={profile && profile.photoUrl}
alt={profile && profile.photoAlt}
{...props}
/>
);
Avatar.propTypes = {
profile: PropTypes.shape({
photoUrl: PropTypes.string.isRequired,
photoAlt: PropTypes.string.isRequired
}).isRequired
};
现在我们可以渲染一个没有props的<Avatar />
来看看控制台上它期望接收到什么:
通常,我们可能忽略掉这些警告,然后控制台的警告会越积越多。这便让PropTypes
失去了它该有的作用,即使出现了一些新的警告我们可能也不会注意到。所以,请务必在事情继续恶化前解决掉这些警告。
规则3:渲染所有作为属性来传递来的HTML属性
到目前为止,我们的单元素组件使用了一个我们自定义的profile
属性。我们应当避免使用这种自定义属性,特别是当他们直接映射到HTML属性时。在文章后面,我会详细的介绍这一点:避免添加自定义props。
通过将所有的props传递给底层元素,我们可以在单元素中轻易的接收所有的HTML属性。我们可以通过设置期望相映射的HTML属性来解决自定义属性的问题:
const Avatar = props => <img className="avatar" {...props} />;
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired
};
现在Avatar
更像一个HTML元素了:
<Avatar src={profile.photoUrl} alt={profile.photoAlt} />
这条规则同样适用于当基础元素试图渲染children
的时候。
规则4:始终会合并作为属性传递的样式
在你应用的某些地方,你希望单元素组件能够具有些不同的样式。无论是使用className
或者style
属性,你都应当能够自定义它。
单元素的内部样式应当等同于浏览器应用在原生HTML元素身上的样式。话虽如此,我们的Avatar
在接收到className
属性时,不应当替换内部类名,而是应当追加上去。
const Avatar = ({ className, ...props }) => (
<img className={`avatar ${className}`} {...props} />
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string
};
如果我们将一个style
属性添加到Avatar
上,它应当很轻松的通过拓展运算符来进行添加:
const Avatar = ({ className, style, ...props }) => (
<img
className={`avatar ${className}`}
style={{ borderRadius: "50%", ...style }}
{...props}
/>
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object
};
现在我们可以非常信任的将任意样式应用于我们的单元素上了。
<Avatar
className="my-avatar"
style={{ borderWidth: 1 }}
/>
如果你发现自己需要复用一些新样式,不要犹豫,通过组合Avatar
来创建另一个单元素吧。创建一个渲染另一个单元素的单元素,是多见且很有必要的。
规则5:添加所有作为属性传递的事件处理函数
由于我们已经将所有的props传递了下去,现在我们的单元素组件已经做好了接收任意事件处理函数的准备了。不过,如果我们已经在内部添加了事件处理程序,我们该怎么办呢?
在这种情况下,我们有两个选择:我们可以完全用属性替换掉,或者同时调用它们。这取决于你,只需要确保始终是通过prop来添加事件处理。
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));
const internalOnLoad = () => console.log("loaded");
const Avatar = ({ className, style, onLoad, ...props }) => (
<img
className={`avatar ${className}`}
style={{ borderRadius: "50%", ...style }}
onLoad={callAll(internalOnLoad, onLoad)}
{...props}
/>
);
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object,
onLoad: PropTypes.func
};
建议
建议1:避免使用自定义props
当创建单元素时——尤其是在应用中开发新功能时,你会想要通过添加自定义props的方式来实现自定义配置。
拿Avatar
为例子,一些设计师的怪癖可能会让你在某些地方展示方角,某些地方展示圆角。你也许会认为往Avatar
中添加一个rounded
属性是一个不错的主意。
除非你正在创建一个记录良好的开源库,否则请抵制这种行为。除了文档需要,这样还会导致它不可拓展和代码的不易维护。始终去尝试创建一个新的单元素组件——例如AvatarRounded
——通过渲染Avatar
并修改它,而不是添加自定义props。
如果你持续使用独特且具有描述性的名字去构建可靠的组件,你最终可能会产生数百个组件。这仍然是具有高可维护性的。这些组件的名称本身就是一个文档。
建议2:接收底层的HTML元素作为props
不是所有的自定义属性都是恶魔。你可能经常想要更改由单元素组件渲染的基础HTML元素。添加自定义属性是实现这一目标的唯一方法。
const Button = ({ as: T, ...props }) => <T {...props} />;
Button.propTypes = {
as: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
Button.defaultProps = {
as: "button"
};
下面是一个将Button
渲染为<a>
的例子:
<Button as="a" href="https://google.com">
Go To Google</Button>
或者是渲染为另一个组件:
<Button as={Link} to="/posts">
Posts</Button>
如果你对这种功能感兴趣,我建议你可以看一眼ReaKit,这是一个基于Singel原则构建的React UI工具包。
通过使用Singel CLI来校验你的单元素组件
最后,在阅读完所有的内容之后,你可能想知道是否有工具来根据此模式来进行自动校验。我开发了一个工具,Singel CLI
如果你想要在正在开发中的项目中使用它,我建议你创建一个新的文件夹并开始将你的单元素组件放在那里。
如果你正在使用React,你可以通过npm安装singel
并同故宫以下方式运行:
$ npm install --global singel
$ singel components/*.js
输出将会像下面那样:
另一个比较好的方法就是在项目中将其作为dev依赖项安装,并将一个脚本添加到package.json
中:
$ npm install --dev singel
{
"scripts": {
"singel": "singel components/*.js"
}
}
然后,只需要运行npm脚本就ok了:
$ npm run singel
关于本文
译者:@JeLewine
译文:https://zhuanlan.zhihu.com/p/39814349
作者:@Diego Haz
原文:
https://medium.freecodecamp.org/introducing-the-single-element-pattern-dfbd2c295c5d
最后,他曾分享过:
【第1054期】高阶函数:利用Filter、Map和Reduce来编写更易维护的代码