34. 精读《React 代码整洁之道》

2022-03-14 15:58:11 浏览数 (1)

本期精读的文章是:React 代码整洁之道。

1 引言

编程也是艺术行为,当我们思考代码复用、变量命名时,就是在进行艺术思考。

可能这篇文章没法提高面试能力、开发效率,因为涉及的内容都是 “软能力”。但如果与我一样,时常害怕自己代码不够优雅,那就在茶余饭后看看这篇文章,也许,可以解决一部分你心中的困惑。

2 内容概要

作者整理了几个好的思维习惯,尝试认同它,再看看如何实践。

不冗余

避免重复代码段,对 JSX 同理:

代码语言:javascript复制
// 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

自我解释

尽可能减少代码中的注释。可以通过让变量名更语义化、只注释复杂、潜在逻辑,来减少注释量,同时也提高了可维护性,毕竟不用总在代码与注释之间同步了。

代码语言:javascript复制
// 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 开头:

代码语言:javascript复制
// Dirty
const done = current >= goal;
// Clean
const isComplete = current >= goal;

函数以其效果命名,而不是怎么做的来命名

代码语言:javascript复制
// 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 依然是个约定,而不能通过强类型推导出,目前还没有更优雅的解决思路。

渲染与判断逻辑分开

代码语言:javascript复制
// 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 是过程代码,但过程中涉及到的逻辑,分配给单一指责的渲染组件渲染,如果把逻辑与渲染写在一起,就类似一个函数把功能全做完,这样做显然诸事不利。

提倡无状态组件

代码语言:javascript复制
// Dirty
class TableRowWrapper extends Component {
  render() {
    return (
      <tr>
        {this.props.children}
      </tr>
    );
  }
}

// Clean
const TableRowWrapper = ({ children }) => (
  <tr>
    {children}
  </tr>
);

性能是一个原因,原文比较强调性能与代码量。我认为 stateless 重点在于阻碍了内部状态的使用,移除了生命周期,所以提高了组件的可控性,也就拓宽了组件的使用场景。

受控与非受控组件都有其适用场景,像非常基础的底层组件库,往往倾向提供两套机制,通过 valuedefaultValue 决定是否受控。拥有这样能力的组件源码就没法通过 stateless 写,所以无状态组件的面向对象并不是基础底层组件,而且这些基础组件也没必要完全无状态,两者都提供是最好的选择。

说到这,也就是考虑到成本问题,那么无状态组件也就更适合上层具有业务含义的组件。页面级别组件状态太多,不适合,所以我认为无状态组件比较适合 Wrapper 层,也就是对基础组件包裹并增强业务能力这一层。

解构

代码语言:javascript复制
// Dirty
const splitLocale = locale.split('-');
const language = splitLocale[0];
const country = splitLocale[1];

// Clean
const [language, country] = locale.split('-');

ES6 新增的语法可以提升不少代码可读性,需要刻意训练去培养这个习惯。

3 精读

本周精读已经融于内容概要中 ^_^。最后推荐在 typescript 中开启 strict 模式,强制使用良好的开发习惯。

代码语言:javascript复制
// 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. 总结

我要回去重构代码了,你呢?

0 人点赞