原文链接:React Code Style Guide
阅读大概需要15分钟
我使用React开发有一段时间了,在这段时间里,我一直试图寻找一种“代码风格指南”, 可以在混合使用 JSX 和 JS 时,让代码更清晰,可读性更强。现在,我已经形成了自己的代码风格,很乐意分享一下。也许这些能够帮助到你,当然,也很欢迎在评论区分享类似的指南。
规则 #1:解构 props
解析赋值是我最喜欢的 ES6 特性之一,它可以使通过对象给变量赋值变得更清爽,下面来让我们举个例子。
假设我们有一只狗,我们把它展示在一个类名是它的种类的 div 元素中,在这个 div 中,描述了这只狗的颜色和品性。
class Dog extends Component {
render () {
return <div className={this.props.breed}>My {this.props.color} dog is {this.props.isGoodBoy ? "good" : "bad"}</div>;
}
}
上面的代码完全能满足我们的需求,但看起来是一大块代码,虽然实际上只有三个变量和一个HTML标签。
我们可以通过把 props
中的属性赋值给本地变量来改善代码。
let breed = this.props.breed;
let color = this.props.color;
let isGoodBoy = this.props.isGoodBoy;
使用ES6,可以像下面这样精简为一句代码:
let { breed, color, isGoodBoy } = this.props;
我们使用三元运算符(下文更多相关内容),让整体代码更清爽,展示如下:
class Dog extends Component {
render () {
let { breed, color, isGoodBoy } = this.props;
let identifier = isGoodBoy ? "good" : "bad";
return <div className={breed}>My {color} dog is {identifier}</div>;
}
}
看起来更方便了!
规则 #2:一个标签, 一行代码
现在,我们大概都经历过把一个函数写成由一个操作符和几个参数组成的丑陋的、迅速的、不易读的功能函数的时刻,但是,当你在编写一个无状态的React组件时,完全可以像下面这样让代码更清晰。
class Dog extends Component {
render () {
let { breed, color, goodOrBad } = this.props;
return <div className={breed}>My {color} dog is {goodOrBad}</div>;
}
}
vs.
let Dog = (breed, color, goodOrBad) => <div className={breed}>My {color} dog is {goodOrBad}</div>;
如果你只是需要编写一个简单的元素,在HTML标签中展示一些属性,就没必要把所有东西都写到一个函数中,并封装成一个独立的类,一行代码就可以搞定了。
如果你需要通过一个对象传递属性,有一个使用ES6 的扩展函数小技巧。利用this.props.content
,会自动帮你把字符串插入到开始结束标签中间。
let propertiesList = {
className: "my-favorite-component",
id: "myFav",
content: "Hello world!"
};
let SimpleDiv = props => <div {... props} />;
let jsxVersion = <SimpleDiv props={propertiesList} />;
什么时候使用扩展函数:
- 不需要三元操作符
- 只需要 HTML 标签属性和内容
- 可重复性高
什么时候不应该使用扩展函数:
- 动态属性
- 需要数组或对象格式的属性
- render 中需要内置的标签
规则 #3:三个属性的规则
假设你有三个或三个以上的属性,然后在实例和 render 函数中,都写在一行。
如果只有一行属性也还好:
class GalleryImage extends Component {
render () {
let { imgSrc, title } = this.props;
return (
<figure>
<img src={imgSrc} alt={title} />
<figcaption>
<p>Title: {title}</p>
</figcaption>
</figure>
);
}
}
但是,考虑一下下面的情况:
class GalleryImage extends Component {
render () {
let { imgSrc, title, artist, clas, thumbnail, breakpoint } = this.props;
return (
<figure className={clas}>
<picture>
<source media={`(min-width: ${breakpoint})`} srcset={imgSrc} />
<img src={thumbnail} alt={title} />
</picture>
<figcaption>
<p>Title: {title}</p>
<p>Artist: {artist}</p>
</figcaption>
</figure>
);
}
}
或者这样:
<GalleryImage imgSrc="./src/img/vangogh2.jpg" title="Starry Night" artist="Van Gogh" clas="portrait" thumbnail="./src/img/thumb/vangogh2.gif" breakpoint={320} />
这样的代码块读起来很不方便。把每个属性单独写一行,更易读:
let { imgSrc,
title,
artist,
clas,
thumbnail,
breakpoint } = this.props;
或者
<GalleryImage
imgSrc="./src/img/vangogh2.jpg"
title="Starry Night"
artist="Van Gogh"
clas="landscape"
thumbnail="./src/img/thumb/vangogh2.gif"
breakpoint={320} />
规则 #4: 非常多的属性?
在任何层次的属性管理都是很有技巧性的,但如果使用ES6的解构和 基于状态控制的React开发,有一些方式可以让大量的属性看起来更清晰。
假设我们要开发一款地图APP,需要保存一堆当前位置的地址和GPS坐标数据。当前用户的位置信息和大概的地址应该保存在父组件 APP中,像这样:
class App extends Component {
constructor (props) {
super(props);
this.state = {
userLat: 0,
userLon: 0,
isNearFavoriteAddress: false
};
}
}
那么,当我们有一个地址,想要计算出这个地址距离我们的位置有多远,我们把最后两个属性传入 APP 中。
APP render ()
:
<Address
... // Information about the address
currentLat={this.state.userLat}
currentLong={this.state.userLon} />
Address 组件的 render函数:
render () {
let { houseNumber,
streetName,
streetDirection,
city,
state,
zip,
lat,
lon,
currentLat,
currentLon } = this.props;
return ( ... );
}
现在,你可以看到代码变得很笨重,如果我们把两个坐标信息从原来的对象中拆分出来,会更容易管理。
在APP constructor()
中:
this.state = {
userPos: {
lat: 0,
lon: 0
},
isNearFavoriteAddress: false
};
在APP render()
前的坐标点:
let addressList = [];
addressList.push({
houseNumber: "1234",
streetName: "Street Rd",
streetDirection: "N",
city: "City",
state: "ST",
zip: "12345",
lat: "019782309834",
lon: "023845075757"
});
在APP render()
中:
<Address addressInfo={addressList[0]} userPos={this.state.userPos} />
Address 组件的 render函数:
render () {
let { addressInfo, userPos } = this.props;
let { houseNumber,
streetName,
streetDirection,
city,
state,
zip,
lat,
lon } = addressInfo;
return ( ... );
}
非常的清晰!React也有非常好的方式确保这些对象属性存在,并确保时特定的类型,通过JavaScript中没有的 PropTypes
属性来实现,这是一种很棒的OOP的实现。
规则 #5:动态渲染 - 映射出的数组
在很多的HTML文件中,我们会写很多同样的代码,尽管他们只有一点点差别。这也是React最初产生的原因,你可以通过把属性放在一个对象中,然后返回一个复杂、动态的HTML代码块,而不需要重复编写代码块。
JavaScript已经有一种方式实现这个目标:arrays!
React使用.map()
函数,按顺序展示数组元素,并将每个数组元素参数作为关键词key
。
render () {
let pokemon = [ "Pikachu", "Squirtle", "Bulbasaur", "Charizard" ];
return (
<ul>
{pokemon.map(name => <li key={name}>{name}</li>)}
</ul>
);
}
甚至可以通过Object.keys()
方法获取一个对象的键值列表作为参数,使用扩展函数的方式渲染(我们同样也需要一个key
)。
render () {
let pokemon = {
"Pikachu": {
type: "Electric",
level: 10
},
"Squirtle": {
type: "Water",
level: 10
},
"Bulbasaur": {
type: "Grass",
level: 10
},
"Charizard": {
type: "Fire",
level: 10
}
};
return (
<ul>
{Object.keys(pokemon).map(name => <Pokemon key={name} {... pokemon[name]} />)}
</ul>
);
}
规则 #6: 动态渲染 - React中的三元操作符
在React中,你可以像变量声明那样,使用操作符实现一个条件渲染。在规则1中,我们需要一个条件来判断狗是好是坏,就完全没必要另外写一行代码来决定一个语句中一个单词的变化。但当这个语句变成一个大的代码块,则很难发现单个的?
和 :
符。
class SearchResult extends Component {
render () {
let { results } = this.props;
return (
<section className="search-results">
{results.length > 0 &&
results.map(index => <Result key={index} {... results[index] />)
}
{results.length === 0 &&
<div className="no-results">No results</div>
}
</section>
);
}
}
或者,使用三元操作符
class SearchResult extends Component {
render () {
let { results } = this.props;
return (
<section className="search-results">
{results.length > 0
? results.map(index => <Result key={index} {... results[index] />)
: <div className="no-results">No results</div>
}
</section>
);
}
}
即使是我们写的非常规范的映射渲染,你也可以看到很繁琐的大括号嵌套。现在,想象一下,如果我们的渲染不止一行的情况,代码可读性将会变得很差。考虑下这种写法:
class SearchResult extends Component {
render () {
let { results } = this.props;
let outputJSX;
if (results.length > 0) {
outputJSX = (
<Fragment>
{results.map(index => <Result key={index} {... results[index] />)}
</Fragment>
);
} else {
outputJSX = <div className="no-results">No results</div>;
}
return <section className="search-results">{outputJSX}</section>;
}
}
最终,代码的长度都差不多,但还是有一些差别:在第一个例子中,我们可以很快的转换前后两种不同的语法,但视觉解析变得费力和困难,在第二个例子中,使用了一个简单的JavaScript变量赋值语句,遵循了语言中函数只返回一行代码的习惯。
这条经验法则是,如果你放入JSX 代码中的JavaScript对象变量超过两个单词(例如object.property
),就应该在return之前
处理好。
总结
这种语法的组合也许让人觉得有些混乱,但当代码脱离这些规则时,会引发了更麻烦的事。这里有几条基本的概念适用于文中没有提到的地方:
- 使用ES6特性,坦白来说,ES6中有很多很棒的特性可以让你的工作变得更轻松、快捷,编写更少的代码。
- 在
=
和return
右边只使用 JSX。 - 有时候需要在JSX 中使用JavaScript。如果你的JavaScript不能在一行内(像
.map()
函数或者三元操作符这样)完成,最好在使用之前处理好。 - 如果你的代码开始像
<{`${()}`} />
这样,你可能已经离代码规范很遥远了。这种低层次的处理应当在当前语句之前就处理好。
译者有话说:
虽然作者洋洋洒洒总结了六条挺实用的代码规范,但还是在评论区“被怼了”,我觉得评论区提出的意见也非常好,所以在下面也贴一下,也加了点自己的理解。
1)箭头函数的参数:
原文中:
let Dog = (breed, color, goodOrBad) =>
My {color} dog is {goodOrBad}
更推荐:
let Dog = ({breed, color, goodOrBad}) =>
My {color} dog is {goodOrBad}
把参数写成对象的方式,可以忽略参数次序和省略参数,更为灵活,其实还可以给参数一些默认值。
2)const VS let:
对于从props
传递过来的属性,其实是 immutable
,不建议更改,默认使用 const 来声明,除非一些特殊的情况,不得不使用 let。
3)关于条件表达式:
原文:
class SearchResult extends Component {
render () {
let { results } = this.props;
let outputJSX;
if (results.length > 0) {
outputJSX = (
<Fragment>
{results.map(index => <Result key={index} {... results[index] />)}
</Fragment>
);
} else {
outputJSX = No results;
}
return <section className="search-results">{outputJSX}</section>;
}
}
推荐:
class SearchResult extends Component {
render () {
const { results } = this.props
const Results = () => <Fragment>
{results.map(index => <Result key={index} {...results[index]} />)}
</Fragment>
const NoResults = () => No results
return <section className="search-results">
{
results.length
? Results()
: NoResults()
}
</section>
}
}
推荐写法其实逻辑更清晰,对条件表达式更好的应用,不建议在render 中写太多的条件判断语句。
4)对象遍历:
Object.entries
貌似比Object.keys
方法更优雅。
Object.entries(obj).map(([key, val]) => This value is {val})
译者还是新手,文中有翻译不合理的地方,欢迎大家提建议。