如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。
如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉
TL;DR
- 前言
- 常规组件化思路
- Hooks组件
- 非常规组件(有奇特的效果,重点是这个)
前言
在我的一些文章中,不管是自己写的还是翻译国外优秀开发人员博客的。其中一个主线就是,了解了JS基础,来进行前端模块化实践。尤其现在前端比较流行的框架React
、Vue
都是提倡进行对页面利用组件进行拆分。这其实就是MVVM的设计思路。
而我们今天以React
项目开发,来聊聊如何在实际项目中实现组件化,或者如何进行高逼格的逻辑服用。
以下所讲内容,都是本人平时开发中用到的,并且都实践过的(如果大家有更好的想法,也可以留言讨论,毕竟每个人对一个事件的看法或多或少有一些认知的差异)。
我信奉一句话,实践出真知,没有实践就没有发言权
常规组件化思路
这里起的标题是常规,是因为这个篇幅中所讲的内容也是React
官方毕竟推崇的常规方案(非常规方案,例如Hooks
由于API比较新颖对于一些开发中,比较陌生,同时受公司React
版本的影响,只是停留在理论阶段。我将Hooks
会单独拎出来,讲一些小🌰)
如果大家经常翻阅React
官网,在ADVANCED GUIDES中明确指出了,两个实现逻辑服用的方式:HOC
和Render Props
。
所以我们来简单介绍一下。
HOC
来看一下官方的解释:
A higher-order component (HOC) is an advanced technique in React for reusing component logic.
其实如果对JS高级函数有过了解的童鞋,看到这个其实不会感到很陌生。而HOC
就是模仿高级函数,来对一些比较共用的逻辑,进行提炼。从而达到代码复用的效果。
继续给大家深挖一下,高阶函数可以接收函数做为参数,同时将函数返回。
function hof(f){
let name="北宸";
return f(name);
}
function f(name){
console.log(`${name} 是一个萌萌哒的汉子!`)
}
看到这点是不是有点闭包的身影。也就是说,被包裹的函数拥有hof
中所定义变量的访问权限。
如果大家想对闭包、作用域有一个比较深刻的了解可以参考
这题有点跑偏了。我们进入正题。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
这是对HOC
的一个简单公式的概括。通过刚才讲解hof
其实对这个就轻车熟路了吧。
Talk is cheap,show your the code
function EnhancedComponent(WrappedComponent){
//其实,这里也可以定义一些比较共用的参数和逻辑
return class extends React.Component{
//
state={
//定义一些公共属性
name:'北宸'
}
componentDidMount() {
//做一些数据初始化处理
}
componentWillUnmount() {
//针对一些注册事件例如轮询事件的解绑
}
handleChange() {
//自定义事件处理
}
render() {
//这里最好做一下结构处理
const {name} = this.state;
return <WrappedComponent
parentData={name} //将一些共用的数据传入
{...this.props} //简单的传入传出
/>;
}
}
}
这就是一个简单的HOC
例子。
如果大家想对HOC
有一个更深的了解可以参考
- React-HOC了解一下(这是我早期写的,可能有点Low)
Render Props
这也是一个常规逻辑复用的方式。
先来一个总结,其实就是在组件属性中有一个属性,而该属性是一个函数,函数返回了一个组件。
<RenderProps renderRegion={(value)=><CommonConponenet value={value}/>}
稍微解释一下RenderProps
组件包含中一些共有逻辑和数据,而这些逻辑和数据,恰恰是CommonConponenet
需要的。
Talk is cheap,show your the code
class RenderProps extends React.Component{
state={
name:'北宸'
}
//一堆生命周期方法
//一堆自定义方法
render(
const {name} = this.state;
return (
<section>
<>
//RenderProps自己的页面结构
</>
//如果传入的数据过多,这里可以使用一个对象把数据进行包装
{this.props.renderRegion(name)}
</section>
)
)
}
想必,大家在平时开发中,或多或少都用到这些比较常规的方式。
上面只是简单的介绍了一下。其实本文的重点是下面的一些非常规操作。
Hooks组件
想必大家在翻阅一些技术文档,有很多,关于Hooks
用法的介绍。本文就不在啰嗦了,而今天给大家准备说一些非常规操作。如果实在想了解可以参考我翻译的一篇关于Hooks
的简单应用。
首先,有一点需要明确,hooks
的推出,是针对函数组件。是让函数组件,也能享受state
/生命周期带来的愉悦感。
给大家一个应用场景,就是有一个组件,有一个定时功能,需要一个定时器,暂且定位15min内定时器走完,并在定时器完成之后进行额外的操作。
通过上面的需求,我们来分析一波。
- 有一个定时功能,那势必就是在组件初始化时,要触发这个定时器,在触发定时器之后,定时器会额外的开出一个线程进行数据处理。
- 每次时间变化,都需要显示到页面中
- 我们需要监听定时器是否到达要求,(15min之后,需要额外操作)
- 触发额外操作
- 如果在15min之内,把组件销毁,还需要将定时器销毁
Talk is cheap,show your the code
import React,{useState,useEffect} from 'react';
function Test(props){
//从父组件来的数据
const {name}=props;
const [msg, setMsg] = useState("二维码有效时间:15分00秒");
let timer, maxtime = 15 * 60;
//1 初始化一个定时器,(在组件初始化时),
useEffect(()=>{
timer = setInterval(() => {
if (maxtime >= 0) {
let minutes = Math.floor(maxtime / 60);
let seconds = Math.floor(maxtime % 60);
let msg = "二维码有效时间:" + minutes + "分" + seconds + "秒";
--maxtime;
//2. 这里定义一个state,变量,用于页面显示
setMsg(msg)
} else {
clearInterval(timer);
//如果在正常情况下时间走完,进行额外操作
//3. 这里可以是更新页面,也可以进行事件回调
}
}, 1000)
return ()=>{
clearInterval(timer); //在组件销毁时,将定时器销毁
}
},[])//这里为什么需要第二个参数?可以参考我刚才给的链接,寻找原因
function stopTimer(){
clearInterval(timer);
}
return(
<section>
<Button onClick={()=>{this.stopTimer()}}>停止计时</Button>
{mag}
</section>
)
}
非常规组件
该部分,是本人在平时项目开发中,有时候会用到的组件封装思路,如果大家有好意见和建议,可以互相讨论。总之,想到达的目的就是取其精华,去其槽粕。
因为在项目开发中用的UI库是Antd,有些使用方式也是基于它的使用规则去使用的。
RandomKey
如果你在React
项目中,通过遍历一个数组来渲染一些组件,例如:li
。如果你不给加一个key
在控制台会有警告出现。这个key
也是React
中隐藏属性。是为了Diff
算法用的。
但是,有一些场景,比方说,一些弹窗,是需要用户填写一些信息的,而有一些用户在填写的时候,有取消的操作,常规的处理方式是不是,需要遍历,并通过对比原来的信息进行数据的恢复。
而,如果你能够在组件调用处监听到用户的取消事件,那提供一个比较方便,但是这也是属于无奈之举的方式。RandomKey
。
<Test key={Math.random() />}
State组件
这个名词是本人,擅自命名的,如果大家感觉有好的名字,在评论区告诉我。
如果在React开发项目中,你前期可能规划的很好,按部就班的去写页面,但是,但是,但是,你架不住,产品每天不停的改需求,而改来改去,发现自己原先规划的东西自己都看不懂了。
或者,你们老大让你去维护别人写的代码,追加一个新的需求,而当你看到别人写的代码时候,心凉了。这TM是人能看懂的代码吗,不说逻辑是否,负责,TM一个页面的所有功能都堆砌在一个文件中。(这里说堆砌一点都不为过,我看过别人写一个页面1000行代码,20多个state
)。
比方说,现在有如下的组件,需要新增一个弹窗,而新增一个弹窗,我们来简单的细数一下需要的变量
- visibleForXX
- valueXX
暂且就按最少的来
class StateComponent extends React.Component{
state={
//这里忽略7-10个参数
visibleForXX:false,
valueXX:'北宸',
}
handleVisible=(visible)=>{
this.setState({
visibleForXX:visible
})
}
render(
const {visibleForXX,valueXX}= this.state;
const {valueFromPorpsA,valueFromPorpsB} = this.props;
return (
<section>
//这里是700多行的魔鬼代码
<Button onClick=(()=>this.handleVisible(true))>我叫一声你敢答应吗</Button>
<Button onClick=(()=>this.handleVisible(false))>我不敢</Button>
{visibleForXX?
<Modal
visible={visibleForXX}
valueXX={valueXX}
valueFromPorpsA={valueFromPorpsA}
//....如果逻辑负责,可能还很多
>
//bala bala 一大推
</Modal>
:null
}
</section>
)
)
}
这样写有毛病吗,一点毛病都没有,能实现功能吗,能。能交差吗,能。能评优吗,评优和代码质量有毛关系。后面维护你的代码的人,能问候你亲戚吗,我感觉也能。
同时大家发现一个问题吗,虽然我在写代码的时候,特意用了解构,但是每次不管是否和该组件相关的有关的渲染,都会进行一次按作用域链查找。有没有必要,这个还没有。
那有啥可以解决呢,有人会说,那你不会拆一个组件出来啊。可以,如果按我平时开发,这个新功能一般都是一个组件。但是,把上述Modal
代码拆出去,其实还是会有每次render
作用域链查找问题。
其实,我们可以换种思考思路。
直接上菜吧。
import Modal from '../某个文件路径,当然也可以用别名'
class StateComponent extends React.Component{
state={
//这里忽略7-10个参数
ModalNode:null
}
handleClick=()=>{
const {valueFromPorpsA,valueFromPorpsB} = this.props;
this.setState(
ModalNode:<Modal
visible={visibleForXX}
valueXX={valueXX}
valueFromPorpsA={valueFromPorpsA}
//....如果逻辑负责,可能还很多
/>
)
}
render(
const {ModalNode}= this.state;
return (
<section>
//这里是700多行的魔鬼代码
<Button onClick=(this.handleClick)>我叫一声你敢答应吗</Button>
{ModalNode?
ModalNode
:null
}
</section>
)
)
}
把组件换一个位置,他不香吗。用最密集的代码做相关的事。虽然,有可能接收你代码的人,可能会骂街,但是等他替换一个简单的参数。就不需要在1000行代码中遨游了。
上述例子,只是简单说了一下思路,但是肯定能实现,由于篇幅问题,就不CV又臭又长的代码了。
自我控制显隐的Modal
有没有遇到这么一个需求,需要在某一个页面中,新增一个弹窗,而这个新增的任务又双叒叕交给你了。
而你欣然接受,发现需要新增的地方,又是一堆XX代码。(你懂我说的意思)
我们继续分析一下常规Modal
(在已经将Modal封装成一个组件前提下)具备的条件(在调用处需要准备的东西)
- visible
- this.handleVisible(false)
- this.handleOk(这里可能有额外操作)
列举的,是最简单的,可能有比这还复杂的。this.handleVisible(false)
是控制Modal
关闭的。
然后你又继续bala bala的写。如果一个页面存在1-2个modal还是可以接受,但是如果是4-5个呢,你还是用这种方式,我的天,恭喜你成为了CV俱乐部高级会员。
我可以弱弱的卑微的提出两点
- 将
Modal
的壳子封装成组件,你只负责content
的书写 - 封装的
Modal
自己去控制显隐
我们着重说一下2
,因为这个篇幅是2
的主场。
Talk is cheap,show your the code
组件调用方式,会发现,只要引入对应的组件,并且定义一个孩子节点,就可以实现一个控制显隐的组件。
<OperatingModal>
<Button>点击</Button>
</OperatingModal>
Modal简单实现思路
import React from 'react';
import { Modal } from 'antd';
export class OperatingModal extends React.Component {
static getDerivedStateFromProps(nextProps, state) {
if ('visibleFromParent' in nextProps && nextProps.visibleFromParent && !state.visible) {
return {
visible: nextProps.visibleFromParent
}
}
return null;
}
state = {
visible: false
}
handleVisible = (visible) => {
this.setState({
visible: visible
});
}
handleOk = (secondNode) => {
//控制显示隐藏
}
renderFooter = () => {
const { footer, footerPosition } = this.props;
let footerNode = footer;
if (footer != null) {
let footerChidren = footerNode.props.children;
footerNode = <div >
{footerChidren.length > 1 ?
<React.Fragment>
//克隆处理
</React.Fragment>
: footer}
</div>
}
return footerNode;
}
render() {
const { title, children, content, width, closable, zIndex } = this.props;
const { visible } = this.state;
return (
<section>
{children ?
React.cloneElement(children, { onClick: () => this.handleVisible(true) })
: null
}
<Modal
//常规配置
>
{content}
</Modal>
</section>
)
}
}
OperatingModal.defaultProps = {
title: '提示',
content: '我是内容',
children: null,//需要包裹的孩子节点
footer: null,
visibleFromParent: false,
width: 300,
closable: true,
footerPosition: 'center',
zIndex: 1000,
}
这是我简单的一个版本,你如果正好用antd
,你可以尝试用一下。
有一点需要说明,这里用到了,一个React
属性React.cloneElement
。具体API讲解可以参考官网。
自带form提交的Modal
这个例子是基于2
(自我控制显隐的Modal)的升级版本,只提供一个思路。主要代码如下。
该组件具备的功能:
- form表单和非form表单
- 自控显隐
- 防重复提交
- from 提交之后,能够获取到接口结果,进行其他操作
- 父组件也可以调用显示隐藏
- ....
组件调用方式
<AdaptationFromModal>
<From.Item>
//.....
</From.Item>
//很多From.Item
</AdaptationFromModal>
组件实现大致思路
import React, { Component } from 'react';
import {
Modal, Form, Button, message,
} from 'antd';
import throttle from 'lodash.throttle';
class AdaptationFromModal extends Component {
constructor(props, context) {
super(props, context);
this.state = {
visible: false,
isSubmitLoading: false,
randomKey: 1,
};
this.handleOK = this.handleOK.bind(this);
this.handleOKThrottled = throttle(this.handleOK, 4000);
}
componentWillUnmount() {
this.handleOKThrottled.cancel();
}
renderFormCotent = () => {
let formContentNode = null;
const { formItemLayout } = this.props;
formContentNode = (
<Form >
//对renderCotent进行遍历
</Form>
);
return formContentNode;
}
renderNormalContent = () => {
let normalContentNode = null;
normalContentNode = this.props.renderCotent(//可以将组件值抛出去);
return normalContentNode;
}
webpackModalVisibleStatus = (visible = false) => {
this.setState({
visible: visible,
}, () => {
//根据不同的visible处理相关的代码
});
}
handleOK() {
const { isIncludeForm, callbackForOK, callbackForOKAfter } = this.props;
this.setState({
isSubmitLoading: true,
});
if (isIncludeForm) {
// 进行接口数据的处理
}
}
renderFooter = () => {
const {
isHasPersonFooter, defineFooterNode, callbackForOK, callbackForOKAfter,
} = this.props;
const { isSubmitLoading } = this.state;
let footerNode = null;
if (isHasPersonFooter) {
if (defineFooterNode !== null) {
footerNode = defineFooterNode(//这里可以将组件内部的值,抛出去 );
}
} else {
footerNode = (
//常规渲染
);
}
return footerNode;
}
render() {
const { visible, randomKey } = this.state;
const {
width, isIncludeForm, title, children, style,
} = this.props;
return (
<section style={style || {}}>
{children
? React.cloneElement(children, { onClick: () => this.webpackModalVisibleStatus(true) })
: null}
{visible
? (
<Modal
//常规Modal配置
>
{
isIncludeForm ? this.renderFormCotent() : this.renderNormalContent()
}
</Modal>
)
: null}
</section>
);
}
}
AdaptationFromModal.defaultProps = {
title: 'xxx', //
width: 600,
isIncludeForm: true,
isHasPersonFooter: false,
callbackForOkClick: null, // 点击确定的回调函数
formItemLayout: {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
},
style: undefined,
};
export default Form.create()(AdaptationFromModal);
Antd Form壳子
有些公司业务中,可能有表单啊,类似的功能,就是简单的form处理。但是,antd的在进行表单处理的时候,需要很多冗余的配置。如下:
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>,
)}
</Form.Item>
</Form>
比方说有些Form.Item
,getFieldDecorator
这些看起来很碍眼。是不是有一种处理方式,就是简单的配置一些页面需要的组件,而这些处理放在一个壳子里呢。
<SearchBoxFactory
callBackFetchTableList={this.callBackFetchTableList}
callBackClearSearchCondition={this.callBackClearSearchCondition}
>
<Input
placeholder={'请输入你的姓名'}
formobj={{
apiField: 'XXA', initialValue: '', maxLength: 50, label: '北宸',
}}
/>
<Input
placeholder={'请输入你的性别'}
formobj={{
apiField: 'XXB', initialValue: '', maxLength: 50, label: '南蓁',
}}
/>
<Select
style={{ width: 120 }}
formobj={{
apiField: 'status', initialValue: '1',
}}
>
<Option value="1">我帅吗?</Option>
<Option value="2">那必须滴。(锦州语气)</Option>
</Select>
</SearchBoxFactory>
这种处理方式他不香吗。就问你香不香。
总结
其实React
中组件化,是一个编程思路,我感觉,懒人才可以真正的领略到他的魅力,因为懒人才可以想出让自己书写代码,更快,更高,更强的方式。
这也是一种编程习惯和方式。用最短的时间,写最少的代码,干最漂亮的代码。
希望我啰嗦这么多,能对你有所帮助。如果大家看到有哪里不是很明白的,可以评论区,留言。如果感觉还不是很尽兴。没关系,这些东西,有很多。
其实每天都在吵吵组件化,组件化,组件化是在实际开发项目中,在解决实际业务中,才会有灵感。他主要的目的是解决实际问题,而不是闭门造车。