从上传组件 引发的用React Hooks来实现的思考过程

2,988 阅读5分钟

引言

本文从最基本的实现,一步步探索 通过react hooks来实现的方案

小明同学在写代码的时候遇到这样的问题
 

95805.jpeg

到底是怎么样的问题呢?我们往下看

遇到的问题

小明有个页面包含这两个功能,分别是上传文件 和上传图片,二者除了样式不同,上传逻辑基本一致,该怎么样优雅合理实现呢?(即图中的 红色 和 蓝色 组件)

image.png

分析两个组件构成元素
a)上传文件组件

  •  上传button
  •  上传路径显示文本区

b)上传图片组件

  •  上传button
  •  上传描述文案

方案

方案1 最原始解决方案 - 耦合在一起

小明: 两个组件都有 上传button,试试放在一起,这样就实现逻辑共用啦~

import { Upload } from 'antd';

class Upload extends Component {
  state = {
    fileList: [],
    uploadUrl: '',
  };

  render() {
    const { uploadType, btnText, btnDes, ... } = this.props;
    const { fileList, uploadUrl } = this.state;

    const uploadProps = {
      name: 'file',
      action: '/api/uploadFile',
      fileList: fileList,
      onChange: info => {
        const { file } = info;
        // 判断当前状态
        if (file.status === 'done') {
          const { response: { url } } = file;
          this.setState({
            uploadUrl: url,
          });
        } else if (file.status === 'error') {
          // do something
        }
        // 更新文件列表显示:只显示一个 上传文件
        this.setState({
          fileList: info.fileList.slice(-1),
        });
      },
    };

    // 渲染组件样式
    const renderElement = () => {
      switch (uploadType) {
        case 上传图片: return <UploadImage/>
        case 上传文件: return <UploadFile/>
      }
    }

    return (
      <div className={styles.container}>
        <Upload {...uploadProps}>
          {renderElement()}
        </Upload>
      </div>
    );
  }
}

27739.jpeg

来自不知名的小绿同学点评:两个组件耦合在一起,需要外部传入uploadType来区分当前显示的样式,合并在一起不好维护,可以考虑拆分

方案2 原始的进阶-拆分成两个组件

小明:竟然说我代码不好,那我拆成两个组件!

import { Upload } from 'antd';

class UploadImage extends Component {
  state = {
    ...如上
  };

  render() {
    const { fileList, uploadUrl, btnText, btnDes } = this.state;

    const uploadProps = {
      name: 'file',
      action: '/api/uploadFile',
      fileList: fileList,
      onChange: info => {
        ... 上传处理逻辑,同上 ... 
      },
    };

    return (
      <div className={styles.container}>
        <Upload {...uploadProps}>
          ...上传图片样式...
        </Upload>
      </div>
    );
  }
}

// 上传文件 UploadFile 也如上定义,但

class UploadFile extends Component {
    ... 

    return (
      <div className={styles.container}>
        <Upload {...uploadProps}>
          ...上传文件样式...
        </Upload>
      </div>
    );
  }
}

27739.jpeg

还是小绿点评:上传逻辑存在重复,考虑抽取出来

方案3 从原始到进阶 - render child的方式

小绿同学给了提示:试试把 上传逻辑 uploadProps 抽取出来
小明咔哧咔哧搬砖实现...

import { Upload } from 'antd';

class UploadRender extends Component {
  state = {
    ...如上
  };

  render() {
    const { fileList, uploadUrl } = this.state;

    const uploadProps = {
      name: 'file',
      action: '/api/uploadFile',
      fileList: fileList,
      onChange: info => {
        ... 处理上传逻辑,同上 ...
      },
    };

    return (
      <div className={styles.container}>
        <Upload {...uploadProps}>
          // 关键点 this.props.children
          {this.props.children}
        </Upload>
      </div>
    );
  }
}

export default UploadRender

使用

// 上传图片
<UploadRender>
    <UploadImage/>
</UploadRender>

// 上传文件
<UploadRender>
    <UploadFile/>
</UploadRender>

小绿继续点评:存在致命缺陷: 被包裹(UploadImage 子组件) 不好直接拿到 包裹(UploadRender 父组件)内部的属性,

小明:卒...

image.png

方案4 进阶方案 - 高阶组件

小明继续改进,试试高阶组件...
step1.定义一个高阶组件

const UploadWrapper = WrapperedComponent => class WrapperComponent extends Component {
    state = {
      ...如上
    };

    render() {
      const { fileList, uploadUrl } = this.state;

      const uploadProps = {
        name: 'file',
        action: '/api/uploadFile',
        fileList: fileList,
        onChange: info => {
          ... 处理上传逻辑,同上 ...
        },
      };
			// 关键点
      return (
        <WrapperedComponent uploadProps={props} {...this.props}/>
      );
    }
  }

export default UploadWrapper;

step2.定义样式

import { Upload } from 'antd';

// 定义上传图片样式
const UploadImage = ({ uploadProps, btnText, btnDes }) => {
  return <Upload {...uploadProps}>
    <div className={styles.container}>
      ...上传图片样式...
    </div>
  </Upload>
};

// 定义上传文件样式
const UploadFile = ({ uploadProps, btnText, btnDes }) => {
  return <Upload {...uploadProps}>
    <div className={styles.container}>
      ...上传文件样式...
    </div>
  </Upload>
};

3.使用姿势

// 使用高阶组件UploadWrapper包裹
const UploadImageWrapper = UploadWrapper(UploadImage);
const UploadFileWrapper = UploadWrapper(UploadFile);

<UploadImageWrapper btnText={...} {...其他属性}/>
<UploadFileWrapper btnText={...} {...其他属性}/>

小明内心os: 终于把逻辑完美抽出来了
**小绿:**使用多层嵌套,似乎有点繁琐?  那换种姿势?

方案5 进阶方案的进阶-Render Props

小明继续搬砖...

import { Upload } from 'antd';

class UploadRender extends Component {
  state = {
    ...如上
  };

  render() {
    // 将渲染样式 交由外部传入
    const {render} = this.props;
    const { fileList, uploadUrl } = this.state;

    const props = {
      name: 'file',
      action: '/api/uploadFile',
      fileList: fileList,
      onChange: info => {
        ... 处理上传逻辑,同上 ...
      },
    };

    return (
      <div className={styles.container}>
        <Upload {...props}>
          // 关键点
          {render(fileList, uploadUrl)}
        </Upload>
      </div>
    );
  }
}

使用姿势

// 上传图片
<UploadRender render={(fileList, uploadUrl) => {
    	return <UploadImage fileList={fileList} uploadUrl={uploadUrl}/>
    }/>

// 上传文件    
<UploadRender render={(fileList, uploadUrl) => {
    	return <UploadImage fileList={fileList} uploadUrl={uploadUrl}/>
    }/>

小绿继续点评:
好像比高阶组件好了一丢丢?但总感觉哪里怪怪的(利用属性入参的方式渲染,阅读代码有点反人性)

方案6 终极方案 - React Hooks

step1.自定义hooks

import { useState } from '@alipay/bigfish/react';

export function useUpload() {
  const [uploadUrl, setUploadUrl] = useState('');
  const [fileList, setFileList] = useState([]);
  const uploadProps = {
    name: 'file',
    action: '/api/uploadFile',
    fileList: fileList,
    onChange(info) {
      const { file } = info;
      // 判断当前状态
      if (file.status === 'done') {
        const { response: { url } } = file;
        setUploadUrl(url);
      } else if (file.status === 'error') {
        // do something
      }
      // 只显示一个 上传文件
      setFileList(info.fileList.slice(-1));
    },
  };
  return {
    uploadProps,
    uploadUrl,
    fileList,
  };
}

step2.使用姿势

import { useUpload } from '../../Hooks/upload';

// 上传图片组件
const UploadImage = ({ btnText = '上传图片', btnDes = '供预览参考' }) => {
  const { uploadProps, uploadUrl } = useUpload();

  return (
    <div className={styles.container}>
      <Upload {...uploadProps}>
        ...上传图片样式...
        </div>
      </Upload>
    </div>
  );
};

// 上传文件组件
const UploadImage = ({ btnText = '上传文件' }) => {
  const { uploadProps, uploadUrl } = useUpload();

  return (
    <div className={styles.container}>
      <Upload {...uploadProps}>
        ...上传文件样式...
        </div>
      </Upload>
    </div>
  );
};

小明:我厉害吧       
小绿

241640.jpeg

方案对比

方案 点评 个人喜好
方案1 最原始解决方案-耦合在一起 如果每个样式差异性较大,不方便维护
方案2 原始的进阶-拆分成两个组件 存在逻辑重复的问题
方案3 从原始到进阶 - render child的方式 父、子组件 不好直接通信,比较适合视图层 个人挺喜欢
方案4 进阶方案 - 高阶组件 嵌套使用,再多层高阶组件包裹下,可能不好 个人挺喜欢
方案5 进阶方案的进阶-Render Props 阅读复杂代码不太直观
方案6 终极方案 - React Hooks 函数式的代码更简洁,更好复用,提供了便于测试的可能性,版本要求高 个人推荐

最后总结

具体选择方案当然要看开发时间来定