AlanWei / blog

Personal Blog
242 stars 22 forks source link

组件库设计实战 - 组件分类、文档管理与打包发布 #2

Open AlanWei opened 6 years ago

AlanWei commented 6 years ago

在上篇《重新设计 React 组件库》中我们从宏观层面一起探讨了结构自由且数据解耦的 React 组件库应当如何设计,在本文中让我们从具体实践的角度来看如何将这样的设计落地。

组件分类

在传统的组件库设计中,组件分类一直都不是一个必选项,大多数人都认为一个组件究竟是属于组件类还是控件类,不过是名字上的不同而已,并没有实际意义。但在将组件代码写法区分为纯函数与 ES6 class 两种之后,我们发现组件的写法同时也代表着组件的类型,这时就可以给予不同组件一个更清晰的定义,分别是:

在进行了这样清晰的分类之后,每当我们需要新增一个组件时,我们都可以从是否含有内部状态,是否有交互等几个方面来将其纳入组件或控件,并以此来确定其相应的代码规范。

延伸来说,除了基础的组件与控件的区别之外,我们还推荐大家从业务的角度出发再划分出一种新的组件类型,即容器。

举例来说,在 Material Design 大行其道的今天,应该不会有人对卡片这样一种基础的内容展示形式感到陌生。对应到前端组件库中,作为展示内容的骨架,卡片本身应当是一个纯渲染组件,但在将其带入具体的业务场景中后就会发现,卡片本身其实是有状态的,常见的如数据加载中、数据为空、数据错误等。这样一个无交互但含有自身状态的组件无论归于上述的哪个分类都会让人感到奇怪,所以我们又引入了容器这样一个新的分类,专门用来存放卡片这类组件。看到这里,相信聪明的你应该能体会到组件分类的真正意义了,那就是用组件分类这样一种形式来强迫工程师去思考每一个组件的本质,然后再利用 pure render 等方法去优化组件性能。作为离用户最近的一批工程师,前端工程师所应该关心的,除了代码本身之外,用户体验,人机交互等领域方面的经验与知识,也是判断前端工程师是否优秀的另一把标尺。

另一方面来讲,我们又可以从容器组件延伸出强依赖数据的组件应当如何设计这样一个更加抽象的问题。从组件库设计的角度来讲,正如上一篇文章中所提到的,不建议将数据获取等逻辑放在组件里去做的。但结合业务场景来说,统一数据获取等逻辑确实是提升业务开发效率的不二选择,这方面的具体实践大家可以参考琼玖之前的文章《React实践 - Component Generator》。简而言之,使用高阶组件在这里是一个不错的选择。

回到代码本身,抛开纯函数组件不谈,我们这里再来讨论一个编写智能组件时经常会踩到的坑。

在 React 的生命周期函数中,有一个功能十分强大的函数,那就是 componentWillReceiveProps,在这个函数中,我们既可以拿到 this.props 又可以拿到 nextProps,所以理论上来讲,我们可以在这里利用这些数据对组件做任何逻辑上的变更。另一方面,智能组件一般需要支持木偶与智能两种调用方式,以方便使用者在使用时根据是否需要在业务代码中保存组件状态使用。木偶组件标配的 props 一般为 value 加一个回调函数 onChange,这时组件本身就只需要负责根据接收到的 props 进行渲染。而智能组件的标配 props 一般只需要设置一个 defaultValue,也就是外部只负责定义组件的初始状态,接下来组件自己会根据交互来改变内部状态。这里我们可以通过在 componentWillReceiveProps 中同步 props 到 state 的方式来支持两种不同的调用方式,即如果外部直接改变了 value 值,那么就将新的 value 值同步到组件内部的 state 上,如果外部没有改变 value 值,那么就交由组件内部的 state 全权负责组件状态的更新。

constructor(props) {
  super(props);

  this.state = {
    value: props.defaultValue,
  };
}

componentWillReceiveProps(nextProps) {
  // sync state to props
  if (this.props.value !== nextProps.value) {
    this.setState({
      value: nextProps.value,
    });
  }
}

handleChange(value) {
  this.setState({
    value,
  });
  this.props.onChange(value);
}

render() {
  const { value } = this.state;
  return <input value={value} onChange={::this.handleChange} />;
}

文档管理

编写组件库本身并不是最终目的,让更多的人在业务开发中使用起来才是。组件库作为一个自身封装程度较高,内聚性较强的技术项目,开发文档是否足够清晰,完善,也是决定项目成败的另一个关键因素。

优秀的组件库文档起码要满足以下两个要求:

属性全覆盖的重要性在这里不再赘述,使用者在不阅读源码的前提下想要了解组件的所有功能,阅读组件文档是唯一的途径。

另一方面,由于 React 组件本身是高度可定制的,所以如果开发者不能够提供具体的示例,使用者在使用组件进行一个复杂业务开发时就将因为缺少指导而变得异常痛苦。从代码质量管控的角度来讲,丰富的示例也是对组件单元测试的一次具象。在未来维护组件增加新功能时,示例丰富的好处就将体现得淋漓尽致:当组件新增了一些逻辑后,原先所有的示例都仍能完美运行时,我们也会对新加的这个功能更有信心并避免 regression 的发生。

打包发布

作为业务项目的基础依赖,组件库一般都需要打包发布至 npm 以方便业务项目使用。在对组件库进行打包时,为了方便业务项目在具体业务场景下的使用,组件库需要支持以下两种打包方式。

第一种打包方式是使用 webpack 将所有组件打包成一个文件至 dist/ 文件夹中:

dist/xui.js
dist/xui.css

在业务项目中可以通过

import { XXX } from 'xui';

的方式直接调用相应组件。

另一种打包方式是使用 babel 将每个组件都分别编译至对应的 lib/ 文件夹中,并分别编译每个组件的 CSS 文件:

lib/carousel/index.js
lib/carousel/index.css
lib/input/index.js
lib/input/index.css
...

在业务项目中可以通过

import Carousel from 'xui/lib/carousel';
import 'xui/lib/carousel/index.css';

的方式按需调用组件。

小结

在本文中,我们主要从组件分类、文档管理、打包发布三个方面阐述了如何将结构自由且数据解耦的 React 组件库落到实处。

在下一篇文章中,我们将与大家分享组件库国际化方案,敬请期待。