如何编写更好的React组件

前言

不知道有没有同窗跟我同样,学习了不少react源码,却仍是写不出更优雅的代码。咱们知道了dom-diff原理,了解setState是如何更新状态的,而后呢?仍是会写出难以维护的代码甚至bug一堆。
html

不少时候你写出bug不是由于你不懂dom-diff,而是由于你的属性和状态胡乱命名。
react

不少时候你的代码难以维护,也不是由于你不懂dom-diff,而是你的组件划分太不合理了。
git

因此我开始读一些应用层框架的源码,也但愿把学习的过程和你们分享。github

为何是antd-design-mobile

antd-design-mobile是一个不错的学习项目,能够学习到如何更好的使用ReactTypeScriptless,也能够学到优秀的团队是如何思考可维护性可扩展性组件化的。
segmentfault

antd-design-mobile项目集成了一些编译工具,而且把各个组件分散在不通的项目中,看起来比较累。因此我建立了一个相对简单且更适合阅读的项目(不含动画且从新设计了Ui),看原项目吃力的同窗能够看这个:
安全

项目地址

github.com/alive1541/d…bash

预览地址

http://39.100.100.217微信

预览二维码

由于没有绑定域名,微信打开会有问题,可使用支付宝等其余应用antd

源码分析——以<Modal>为例

项目结构

入口

先看看入口文件里是什么内容

能够看到,除了这个提示按需引入的警告以外,其实就是导出了当前目录下的全部组件。下面我以Modal组件为例子继续分析。

<Modal>组件解析

先看下Modal组件总体的结构:
app

接下来详细看一下各个组件内部的细节:

Modal组件

Modal组件是根组件,也就是默认导出的组件。下图从下往上看,文件导出的是Modal组件,它继承成了ModalComponent抽象类,并传入了ModalProps泛型接口。

若是对TypeScript不太了解能够看 www.tslang.cn/docs/handbo… ,通读一下“手册指南”这一章基本就能够看懂Ts代码了

从接口定义上能够看出,这个组件容许接受prefixClstransitionName等属性,而且须要挂载三个静态方法alertpromptoperation

组件内部有cls和一个renderFooterButton方法,其中cls结合prefixCls处理了类名(prefixCls容许用户自定义前缀),以方便用户统一处理自定义的样式。而renderFooterButton方法用来渲染弹窗中的按钮。

接下来就是alertpromptoperation三个静态方法(官网中有相应的使用方法)。他们的做用是经过方法唤出Modal组件,以alert为例:

import { Modal } from 'antd-mobile';

const alert = Modal.alert;

const App = () => (
    <Button
      onClick={() =>
        alert('Delete', 'Are you sure???', [
          { text: 'Cancel', onPress: () => console.log('cancel') },
          { text: 'Ok', onPress: () => console.log('ok') },
        ])
      }
    >
      confirm
    </Button>
)

复制代码

从接口中还能看到,alertpromptoperation这三个方法都有一个函数类型的返回值close,这个方法的做用是关闭当前Modal。逻辑也比较简单,这个静态方法也比较简单,就是建立一个div元素而后插入到body中,再把Modal组件插入这个div中,最后导出一个移除这个div的close方法。核心代码以下:

export default function operation(){
    ...
    
    const div = document.createElement("div");
    document.body.appendChild(div);
    
    ReactDOM.render(
        <Modal
          visible
          operation
          transparent
          prefixCls={prefixCls}
          onClose={close}
          footer={footer}
          className="d-modal-operation"
          platform={platform}
          wrapProps={{ onTouchStart: onWrapTouchStart }}
        />, 
        div
    );
    
    function close() {
        ReactDOM.unmountComponentAtNode(div);  //销毁指定容器内的全部React节点
        if (div && div.parentNode) {
          div.parentNode.removeChild(div);
        }
    }
    
    return { close }
}
 
复制代码

DialogWraaper

这个组件被放在了react-component这个库里,这个库是一个基础组件库,antd和antd mobile这两个项目都依赖了它。
这层组件很轻,并无处理逻辑,只是建立了一个portal,同时对react16如下的版本作了兼容,兼容写法是这样的:

...

const IS_REACT_16 = !!(ReactDOM as any).createPortal;
componentDidUpdate() {
    if (!IS_REACT_16) {
      this.renderDialog();
    }
}
renderDialog() {
    ReactDOM.unstable_renderSubtreeIntoContainer(
      this,
      this.getComponent(),
      this.getContainer()
    );
}
render() {
    const { visible } = this.props;
    if (IS_REACT_16 && visible ) {
      return ReactDOM.createPortal(this.getComponent(), this.getContainer());
    }
    return null as any;
 }
复制代码

Dialog

这个组件主要作了三件事,一、渲染遮罩 二、渲染弹框 三、处理关闭事件 这里渲染了弹出框的元素,代码以下:

...
onMaskClick = (e: any) => {
    if (e.target === e.currentTarget) {
    //e.target和e.currentTarget的区别参考https://www.jianshu.com/p/1dd668ccc97a
      this.close(e);
    }
};
close = (e: any) => {
    if (this.props.onClose) {
      this.props.onClose(e);
    }
};
render() {
    const { props } = this;
    const { prefixCls, maskClosable } = props;
    return (
      <div>
        {this.getMaskElement()}   //渲染遮罩
        <div
          className={`${prefixCls}-wrap ${props.wrapClassName || ""}`}
          onClick={maskClosable ? this.onMaskClick : undefined}
          {...props.wrapProps}
        >
          {this.getDialogElement()}   //渲染弹框
        </div>
      </div>
    );
  }
复制代码

getMaskElementgetDialogElement两个方法渲染元素时,元素外层包裹了LazyRender组件,意思很好理解,就是避免没必要要的渲染。代码也很简单,就是在shouldComponentUpdate中作是否更新的判断。

export default class LazyRender extends React.Component<lazyRenderProps, any> {
  shouldComponentUpdate(nextProps: lazyRenderProps) {
    return !!nextProps.visible;
  }
  render() {
    const props: any = { ...this.props };
    delete props.visible;
    return <div {...props} />;
  }
}
复制代码

到此,这个组件已经结束了。上面只粘贴了部分代码,有表达不是很清晰的地方能够查看 github.com/alive1541/d… 相比于源码,这里的代码更加简单、清晰。

学到了什么

  1. 经过react-component库统一封装了基础组件,在这一层只处理了基本逻辑和样式。方便在antd-mobile这类上层库中经过prefixCls统一重写样式,极大的提升了组件的复用能力。同时,若是也方便其余人使用这个库去开发本身的组件。
  2. 使用TypeScript,提升了代码的健壮性和可读性,这种代码维护起来会很轻松。
  3. 深度使用了less,经过变量、Mixin、函数等特性,对关键变量统一维护在了themes文件中的default.less中,对1px的处理封装在了hairline.less文件中。也经过prefixCls前缀的方式避免了全局样式污染的问题。

其余小知识点

1px处理

antd-mobile对1px的处理是经过transform缩放来实现了,方法就是二倍屏缩小一半,三倍屏缩小到三分之一,详细能够查看个人项目中的/src/components/style/mixins/hairline.less

html:not([data-scale]) & {
    @media (min-resolution: 2dppx) {  //判断是2倍屏幕
      border-left: none;

      &::before {
        width: 1px;
        height: 100%;
        transform-origin: 100% 50%;   //设置缩放原点
        transform: scaleX(0.5);  //x轴缩放50%

        @media (min-resolution: 3dppx) {  //判断是3倍屏幕
          transform: scaleX(0.33);  //x轴缩放三分之一
        }
      }
    }
  }
复制代码

iphoneX适配

有一段less是这样的

.@{prefixCls}-content {
      padding-top: env(safe-area-inset-top);
}
复制代码

这段代码的意思是设置padding-top为iphoneX的顶部部安全区域,也就是iPhoneX的顶部刘海的高度。

除此以外还safe-area-inset-bottom,由于手机有可能横屏使用,因此还有safe-area-inset-leftsafe-area-inset-right,想详细了解的话能够看看这篇文章 segmentfault.com/a/119000001…

结语

以上是我学习的一些感悟和分享,但愿一块儿学习交流。

参考连接

antd-mobile官网 mobile.ant.design/index-cn

antd-mobile仓库 github.com/ant-design/…

react-component仓库 github.com/react-compo…

相关文章
相关标签/搜索