如何开发一个好用的公共组件

lxf2023-02-16 15:50:13

写在前面

当你对某一个业务场景有自己的理解,想提炼开发了一个很好用的组件,想开放给别的同学使用,或者甚至放在社区给任何一个人使用,你应该会产生以下疑问:

  • 一个标准的组件是怎么样的,在开发过程中有哪些注意事项?
  • 组件打包过程是怎样的,如何按需加载?
  • 别人要如何没有负担的使用我的组件?
  • 组件如何支持同步和异步使用?
  • 如何更好的维护独立组件库?
  • ...

基于以上疑问,我们开始这篇阅读这篇文章

组件设计

如何让别人清晰的使用你的组件,换位思考一下,你在使用别人的组件的时候,什么样的组件是你用起来十分符合预期甚至超出预期的?什么样的组件让你在用的时候,充满问号,产生“不如自己写一个”的想法?

首先要让使用者知道你的组件的主要功能README.md),其次知道怎么用能满足自己的需求(props)。

一个好的README.md应该像antdelementUI的官方文档那样,写清楚props用法,有相应的示例展示,必要的时候使用mock数据支持基本的样式展示。

举个例子(antd):

如何开发一个好用的公共组件

除此之外,第一次开发独立组件或者独立组件库,还应该注意什么呢?

清晰的types类型定义&导出

清晰的类型定义不仅能降低组件使用门槛,也能使组件减少大部分js错误(例如类型错误,取值为空错误等)。

export interface BaseButtonProps {
  type?: ButtonType;
  icon?: React.ReactNode; 
  /**
   * Shape of Button
   *
   * @default default // ✅ 类型注释默认值
   */
  shape?: ButtonShape;
  size?: SizeType;
  disabled?: boolean;
  loading?: boolean | { delay?: number };
  prefixCls?: string;
  className?: string;
  ghost?: boolean;
  danger?: boolean;
  block?: boolean;
  children?: React.ReactNode; // ✅ 开放插槽
}

export type AnchorButtonProps = {
  href: string;
  target?: string;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;

// ✅ 开放原生props 
export type NativeButtonProps = {
  htmlType?: ButtonHTMLType;
  onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps &
  Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;

export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;

可扩展插槽(ReactNode)

提供可消费插槽,允许消费方使用自定义children做进一步扩展。

// 扩展插槽
const {iconNode,kids} = props
 let ButtonNode = (
    <button
      {...(rest as NativeButtonProps)}
      type={htmlType}
      className={classes}
      onClick={handleClick}
      disabled={mergedDisabled}
      ref={buttonRef}
    >
      {iconNode}
      {kids}
    </button>
  );

开放与消费底层组件Props

如果你的组件是基于elementUIantd等底层组件库做了进一步封装,满足更强大的定制需求,不要忘了保留底层组件的原有功能。

  • Props写法:自定义Props extends 底层组件Props(可以通过lib单独引入Props类型)
//此处引入只是为了方便解释,实际项目并非如此,请自行查阅相关组件源码以及文档
import { SelectProps } from 'antd/next/types/select';
import { ISelection } from 'antd/lib/hooks/select/hook';
// 定义props
export interface IEmployeeSelectProps
  extends Omit<SelectProps, 'mode' | 'onChange'>,
  Pick<ISelection, 'maxSelection' | 'mode' | 'onChange'> {
    customPropsA?: string[];
    customPropsB?: string[];
    /**
     * 自定义接口
     */
    list?: QueryListInfoType;
    ...
}
  • 如何接收并消费传入的底层组件Props解构赋值
// 接收
const { customPropsA, customPropsB, ...rest } = props;

// 消费
 <Select
  {...rest}
  onChange={customPropsA}
  onSearch={customPropsB}
  ...
  />

受控组件与非受控组件

受控组件就是支持被Reactstate控制的组件,简单来讲,可以通过下传valueonChange控制组件值。同理反推,非受控组件没有和Reactstate绑定,只能通过绑定 Ref单向获得组件值(ref.current.value)。

因此,通常来讲,一个好用的公共组件需要支持受控模式和非受控模式,能够满足普通用户的最简使用需求(组件内部自己维护状态),也能够满足高级用户的自定义需求(由使用方传入state值和onChange方法)。

这里推荐使用ahooks的useControllableValue,它支持父组件下传state,如果没有下传,则交由组件内部管理状态值

举个例子:

const [visible, onVisibleChange] = useControllableValue(props, {
    trigger: 'onVisibleChange',
    defaultValuePropName: 'defaultVisible',
    valuePropName: 'visible',
  });

样式隔离与CSS变量的使用

CSS没有作用域概念,引入即全局生效,但一个样式是否起作用由多个因素共同决定(重要程度、优先级、样式加载顺序)。组件使用者肯定不希望组件层级的样式影响到全局样式,为了避免样式冲突,我们就需要对样式进行隔离。

一般来讲,我们会在组件内部使用一个特定的前缀,例如element-ui使用el-作为前缀。

举例sass语法

$css_prefix: $css-prefix: 'abc-module-' !default;
$content-border-color: rgba(159, 183, 249, 0.5);


.#{$css_prefix}button {
  background-color: $content-border-color;
}

如果你想开发的是一组统一风格的组件库,那么推荐使用css变量,这样方便外部使用者通过改变css变量值的方式统一调整散落在页面各处的组件的样式风格。

组件多语言以及埋点

如果你的组件涉及国际化相关的,或想开放到社区,让各国的开发中都使用,那么组件的多语言处理是必不可少的,可以使用模版动态注入变量值,并且要设置好默认的兜底文案。

其次就是曝光埋点,可以让你清晰的看到组件的使用情况,可以更好的帮助你改进自己的组件,从而让更多的人使用它。

组件导出方式

组件导出方式取决于你想让使用方如何使用,如果你是单个独立组件,通过在入口文件以这样的方式导出:

import PageProduct from './components/you-module';
export { default as PageProduct } from './components/you-module';
export default PageProduct;
// 导出必要的types
export type { IProductProps } from './components/you-module'; 

// 消费方使用
import PageProduct  from '组件';

// 单独引入types类型定义
import type IProductProps from '组件';

如果是组件库,可以这样导出:

export { default as PageProduct1 } from './components/you-module1';
export { default as PageProduct2 } from './components/you-module2';

// 消费方使用
import { PageProduct1PageProduct2 } from '组件库';

组件调试

开发阶段可以配合文档工具,例如dumivitePress等静态站点工具,可以在开发过程中调试,所见即所得。

打包构建

开发完成后,利用打包工具打包构建生成生产文件。

单元测试

如果你的组件逻辑十分复杂,依赖异步数据返回有不同的表现,那么最好添加单元测试,保证每次正式发布之前。新的迭代不会影响老的功能。单元测试推荐使用Jest

组件发布

修改package.json配置文件,登陆npm账号将组件发布至npm.

// 控制台会返回下一个小版本号 如v1.0.1 
npm version patch 
// 重新发布 
npm publish

组件使用

组件在打包构建的时候,通过不同的配置会形成不同的产物,当然也会有不同的引入方式。“同步引入”是指消费方直接npm install package ,在项目代码库中通过ES Module的方式引入使用。“异步引入”方式通常通过请求一个独立的js文件来加载并使用组件。

通常来讲,如果是独立组件,且不是首屏强依赖组件,推荐使用异步加载方式。如果是组件库,最好使用同步加载方式,通过ESM语法进行Tree Shaking,实现按需引入。

总结

组件是对我们最常用的场景的一些提炼,是为了让我们可以快速开发出想要的功能,而无需再从头开始,因此通用性、可扩展性、使用便捷简单、完善的API等特点是衡量一个组件是否好用的最重要的指标。

好了,如此我们就完成了对一个组件从开发到发布、再到使用的全部过程,是不是很简单呢?快去开发属于你自己的公共组件吧。