组件库实战:CV大法好 - 7个小版本,实现完美组件

664 阅读6分钟

前言

在很多业务场景中,都会涉及到重复逻辑的封装,比如后台系统中全局通用的权限功能,组件渲染的性能追踪,以及不同页面中组件的复用,比如一键复粘贴功能。

本文将带你一步一步实现一键复制粘贴功能,阅读本文你将收获:

  1. 学会封装一个设计良好高扩展可复用的组件
  2. 学会使用render props,hoc以及hooks来封装重复逻辑
  3. 了解一键复制粘贴功能的实现原理

需求

复制粘贴功能是业务中非常常见第一个需求场景,一般来说,就是有一个按钮,点击这个按钮能把一段文本复制大剪贴版里,复制成功后,会提示用户已完成了复制,并且这个提示会在一段时间后消失。如下所示

现在我们来拆解一个这个需求:用户点击按钮的时候

  1. 首先我们需要获取到需要复制的文本
  2. 复制文本到剪贴板
  3. 复制文本成功后,提示用户已复制完成
  4. 一段时间后提示消失

准备

  1. 创建项目
create-react-app components
cd components
  1. 安装依赖
yarn add react
yarn add styled-components

实现

第一版

结构&样式

首先,从上面的需求我们可以知道有一个输入框和一个蓝色的按钮,以及提示信息, 下面我们通过styled-components来分别封装他们:

接下来,我们将他们组合在一起

业务逻辑

现在我们来实现业务逻辑:首先,我们需要给Button按钮绑定点击事件

获取到需要复制的文本

我们的业务需求是复制输入框中的值,现在我们为输入框添加状态值并且绑定onChage事件

初始化value值,以及实现handleChange

接着,我们就可以获取到state中的value值了

复制文本到剪贴板

复制成功后,提示用户 && 隔一段时间,提示消失

也就是说复制成功后,上述我们自定义的Notice组件需要显示,并显示一段时间后消息,现在我们为组件中添加一个visible状态,并作为props传递给Notice组件

接下来,我们来实现visible显示状态的逻辑

大工告成~

第二版

第二天,产品经理告诉你说页面的其他地方也需要使用这个复制文本到剪贴板功能,不过现在页面中需要复制的是一个列表的数据。那么,我们如何实现复用这个功能呢?

根据上面的实现,我们可以知道要想实现列表数据或页面中其他数据的复制,也就等于我们需要自定义需要复制的文本的值。这样我们可以抽象出一个ClipBoard组件,然后用户可以通过props传入需要复制的文本值,同时也可以自定义提示消失的时间间隔。像下面这样:

现在我们来实现一下:

最后,我们来引用一下

大功告成~

第三版

第三天,产品经理告诉你说,不仅仅点击这个按钮可以复制文本,点击文本本身也可以复制它的内容到剪贴板。那么我们现在要如何修改ClipBoard组件,来让它可以更好的复用呢。可能有同学会通过如下方式来实现:给ClipBoard一个copy props:如下所示

<ClipBoard
	copy={复制文本组件}
	value={需要被复制的文本}
/>

下面,我们来实现一下:

  1. 编写ClipBoard组件

  1. 引用ClipBoard组件

大功告成~,现在我们点击hello world就可以复制该文本到剪贴板啦~

这样做,虽然实现了需求,但是这样的问题是:自定义的复制按钮和提示组件在UI结构上存在耦合关系,两者在UI结构上的关系是由ClipBoard组件定义清楚的,如下所示

如果某个需求需要提示时在某个iframe中进行提示,但是复制按钮组件需要放到iframe外部,要想实现这个需求,根据目前组件的设计,我们只能在修改组件内部的结构,为了适配现在的情况,还需要增加一个props,用来识别是那种类型的复制粘贴组件。伪代码如下:

class ClipBoard extends React.Component {
  render() {
    const { type }  = props;
    
    if (type == 'normal') {
      return (<>
        	{copy(this.handleClick)}
          <Notify visible={visible}>已复制至粘贴板</Notify>
        <>)
    }
    
  	if (type == 'iframe') {
      return (
        <>
        	{copy(this.handleClick)}
          <iframe>
           	<Notify visible={visible}>已复制至粘贴板</Notify>
          </iframe>
        <>
      )
    }  
  }
}

第四版

为了解决上述提到的逻辑和UI结为构的耦合问题,我们可以使用高阶组件(hoc)的方式来重构代码。

// hoc
const Text = ClipBoardHoc(WrappedComponent, {
  value: text
})

const WrappedComponent = (props) => {
  
  return (
    <Notify visible={visible}>已复制至粘贴版</Notify>
		<h1 onClick={props.handleClick}>复制文本到粘贴板</h1>
	)			
}
  1. 编写ClipBoard组件

  1. 引用ClipBoard组件

通过hoc的方式我们可以很好的分离UI和逻辑,解决他们的耦合问题。

但是高阶组件的问题在于:

  1. 固定的props可能会被覆盖
export default withMouse(withPage(MyComponent));

如果这两个高阶组件有相同的props值,比如他们都有一个名为name的props,那么withPage提供的props值会被withMouse提供的值覆盖

  1. 可读性差,不能清晰的识别数据的来源
// 它不会告诉你组件中包含了那些props,增加了调试和修复代码的时间
withMouse(MyComponent, { ... })

第五版

下面我们通过重构render props,来解决下面的问题:

  1. 编写ClipBoard组件

  1. 引用ClipBoard组件

通过render props,我们可以解决hoc中props的可读性以及覆盖问题。但是,无论是render props还是hoc都存在的问题是它们都会使render tree嵌套过深。

所以接下来我们用hooks来重构代码。

第六版

  1. 编写hooks

  1. 引用hooks

第七版

经过我们的一系列改造,目前的代码已经简洁了很多。但是目前仍然存在的问题是外部组件Notify的显示隐藏,是由useClipBoard hook的内部状态来控制的。如果某一个业务方,使用的提示组件是不需要通过布尔值来控制显示隐藏的,而通过调用以下方法来控制显示隐藏的,比如像下面这样

// 在render中编写Toaster组件
<Toaster
	position="top-center"

/>
    
// 调用toaster,显示toaster
toast.success('Successfully toasted!')

在目前封装的hook中,要想实现上述效果,那么该业务方,只能通过修改useClipBoard的内部代码来达到他想要的效果

而这种做法显然是很糟糕的。这个述求的本质其实是希望提示组件的显示隐藏的控制权交给业务方来处理,而useClipBoard组件只需要在需要的时候,通知业务方即可,其他的都交由业务方自己处理。伪代码如下所示

useClipBoard({
  value: value,
  onCopy: () => {
    // 复制成功,通知业务方,调用该回调函数
    toast.success('已复制文本至剪贴板')
  },
  onError: (err) => {
    // 复制失败,通知业务方,调用该回调函数
  }
})

现在,我们来最后重构一次代码:

  1. 安装提示组件
yarn add react-hot-toast
  1. 编写hooks

  1. 引用hooks

大功告成~~~