精读《React 代码整洁之道》
本期精读的文章是:React 代码整洁之道。
1 引言
编程也是艺术行为,当我们思考代码复用、变量命名时,就是在进行艺术思考。
可能这篇文章没法提高面试能力、开发效率,因为涉及的内容都是 “软能力”。但如果与我一样,时常害怕自己代码不够优雅,那就在茶余饭后看看这篇文章,也许,可以解决一部分你心中的困惑。
2 内容概要
作者整理了几个好的思维习惯,尝试认同它,再看看如何实践。
不冗余
避免重复代码段,对 JSX 同理:
// Dirty
const MyComponent = () => (
<div>
<OtherComponent type="a" className="colorful" foo={123} bar={456} />
<OtherComponent type="b" className="colorful" foo={123} bar={456} />
</div>
);
// Clean
const MyOtherComponent = ({ type }) => (
<OtherComponent type={type} className="colorful" foo={123} bar={456} />
);
const MyComponent = () => (
<div>
<MyOtherComponent type="a" />
<MyOtherComponent type="b" />
</div>
);
但也不要过度优化,过度优化和搞破坏没什么区别。
可预测、可测试
如果使用 Jest 测试,可以考虑截图测试插件:Jest Image Snapshot
自我解释
尽可能减少代码中的注释。可以通过让变量名更语义化、只注释复杂、潜在逻辑,来减少注释量,同时也提高了可维护性,毕竟不用总在代码与注释之间同步了。
// Dirty
const fetchUser = (id) => (
fetch(buildUri`/users/${id}`) // Get User DTO record from REST API
.then(convertFormat) // Convert to snakeCase
.then(validateUser) // Make sure the the user is valid
);
// Clean
const fetchUser = (id) => (
fetch(buildUri`/users/${id}`)
.then(snakeToCamelCase)
.then(validateUser)
);
上面的例子,方法 convertFormat
含义是 “转换格式”,太过于笼统,以至于不得不添加注释。如果换成 snakeToCamelCase
(转换为驼峰风格),这个名字就解释了自己的功能。
斟酌变量名
布尔值或者返回值是布尔类型的函数,命名以
is
has
should
开头:
// Dirty
const done = current >= goal;
// Clean
const isComplete = current >= goal;
函数以其效果命名,而不是怎么做的来命名
// Dirty
const loadConfigFromServer = () => {
...
};
// Clean
const loadConfig = () => {
...
};
很多时候我也经常犯这种错误,毕竟写代码的时候总要考虑实现,一不小心就将实现的方式带入了函数名中。
遵循设计模式
这里的设计模式,并不是指工程上的,而是更广泛的开发中的设计模式,比如 “你应该使用 tslint 校验代码格式” “typescript 开启 stricts 模式” “编写一个 React 函数,应当将 React 作为 peerDependency
” 等等(当然,不要随意设置 peerDependency
也是一种江湖约定)。
对于 React,遵循以下几个最佳实践:
- 单一责任原则, 确保每个功能都完整完成一项功能,比如更细粒度的组件拆分,同时也更利于测试。
- 不要把组件的内部依赖强加给使用方。
- lint 规则尽量严格。
根据我的体验,尤为痛恨违背第二条的组件,比如当 React 组件使用了数据流,但必须依赖项目初始化该数据流才能执行,如果不是被生活所迫,我才不会使用这种组件。
第三条也一样,如果你是一个知名轮子的作者,请毫不留情的使用最严格的 lint 规则。如果使用者的 lint 规则比你还严格,你的组件将无法使用。
考虑到以上几点并不会降低编码速度
编写整洁的代码在开始一定会放慢开发速度,因为你需要转变自己的思维模式,但随着不断迭代,它的带来的效率提升会逐渐弥补前面的损失,并不断带来开发效率的提升。
写组件库也是同理,用脚写固然能快速完成,但后续往往要重构掉。我很羡慕函数式工作环境的开发者,他们几乎只要为每个功能写一遍,剩下的就是记住并调用它。
在 React 中的实践
略过几个没意思的例子。。
在 React 使用 defaultProps 代替在代码中动态判断
显然,利用 React 组件的规则,将入参的默认值预先定义好是最高效的。但顺带一句,如果在 ts 最严格的 stricts
模式里,依然会报错:变量可能未定义。这是因为 defaultProps
依然是个约定,而不能通过强类型推导出,目前还没有更优雅的解决思路。
渲染与判断逻辑分开
// Dirty
class User extends Component {
state = { loading: true };
render() {
const { loading, user } = this.state;
return loading
? <div>Loading...</div>
: <div>
<div>
First name: {user.firstName}
</div>
<div>
First name: {user.lastName}
</div>
...
</div>;
}
componentDidMount() {
fetchUser(this.props.id)
.then((user) => { this.setState({ loading: false, user })})
}
}
// Clean
import RenderUser from './RenderUser';
class User extends Component {
state = { loading: true };
render() {
const { loading, user } = this.state;
return loading ? <Loading /> : <RenderUser user={user} />;
}
componentDidMount() {
fetchUser(this.props.id)
.then(user => { this.setState({ loading: false, user })})
}
}
逻辑与渲染分离,便于维护,其次便于测试。
当然有人可能会问 “就算逻辑与渲染分离了,但组成的大组件不还是逻辑耦合的吗”,对,这就像函数单一指责一样,render
是过程代码,但过程中涉及到的逻辑,分配给单一指责的渲染组件渲染,如果把逻辑与渲染写在一起,就类似一个函数把功能全做完,这样做显然诸事不利。
提倡无状态组件
// Dirty
class TableRowWrapper extends Component {
render() {
return (
<tr>
{this.props.children}
</tr>
);
}
}
// Clean
const TableRowWrapper = ({ children }) => (
<tr>
{children}
</tr>
);
性能是一个原因,原文比较强调性能与代码量。我认为 stateless
重点在于阻碍了内部状态的使用,移除了生命周期,所以提高了组件的可控性,也就拓宽了组件的使用场景。
受控与非受控组件都有其适用场景,像非常基础的底层组件库,往往倾向提供两套机制,通过 value
与 defaultValue
决定是否受控。拥有这样能力的组件源码就没法通过 stateless
写,所以无状态组件的面向对象并不是基础底层组件,而且这些基础组件也没必要完全无状态,两者都提供是最好的选择。
说到这,也就是考虑到成本问题,那么无状态组件也就更适合上层具有业务含义的组件。页面级别组件状态太多,不适合,所以我认为无状态组件比较适合 Wrapper
层,也就是对基础组件包裹并增强业务能力这一层。
解构
// Dirty
const splitLocale = locale.split('-');
const language = splitLocale[0];
const country = splitLocale[1];
// Clean
const [language, country] = locale.split('-');
ES6 新增的语法可以提升不少代码可读性,需要刻意训练去培养这个习惯。
3 精读
本周精读已经融于内容概要中 ^_^。最后推荐在 typescript 中开启 strict
模式,强制使用良好的开发习惯。
// Bad
onChange(value => console.log(value.name))
// Dirty
onChange((value) => {
if (!value) {
value = {}
}
console.log(value.name)
})
// Clean
onChange((value = {}) => console.log(value.name))
// Clean
onChange(value => console.log(value?.name))
不要信任任何回调函数给你的变量,它们随时可能是 undefined
,使用初始值是个不错的选择,但有的时候初始值没什么意义,使用 ?.
语法可以安全的访问属性,是时候抛弃 _.get
了。
4. 总结
我要回去重构代码了,你呢?
更多讨论
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。
发表评论 (审核通过后显示评论):