引言
本文从最基本的实现,一步步探索 通过react hooks来实现的方案
小明同学在写代码的时候遇到这样的问题

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

分析两个组件构成元素
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>
);
}
}

来自不知名的小绿同学点评:两个组件耦合在一起,需要外部传入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>
);
}
}

还是小绿点评:上传逻辑存在重复,考虑抽取出来
方案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 父组件)内部的属性,
小明:卒...

方案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>
);
};
小明:我厉害吧
小绿:

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