React组件封装思路拓展

9,980 阅读14分钟

如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。
如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉

TL;DR

  • 前言
  • 常规组件化思路
  • Hooks组件
  • 非常规组件(有奇特的效果,重点是这个)

前言

在我的一些文章中,不管是自己写的还是翻译国外优秀开发人员博客的。其中一个主线就是,了解了JS基础,来进行前端模块化实践。尤其现在前端比较流行的框架ReactVue都是提倡进行对页面利用组件进行拆分。这其实就是MVVM的设计思路。

而我们今天以React项目开发,来聊聊如何在实际项目中实现组件化,或者如何进行高逼格的逻辑服用。

以下所讲内容,都是本人平时开发中用到的,并且都实践过的(如果大家有更好的想法,也可以留言讨论,毕竟每个人对一个事件的看法或多或少有一些认知的差异)。

我信奉一句话,实践出真知,没有实践就没有发言权

常规组件化思路

这里起的标题是常规,是因为这个篇幅中所讲的内容也是React官方毕竟推崇的常规方案(非常规方案,例如Hooks由于API比较新颖对于一些开发中,比较陌生,同时受公司React版本的影响,只是停留在理论阶段。我将Hooks会单独拎出来,讲一些小🌰)

如果大家经常翻阅React官网,在ADVANCED GUIDES中明确指出了,两个实现逻辑服用的方式:HOCRender 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中所定义变量的访问权限。

如果大家想对闭包作用域有一个比较深刻的了解可以参考

  1. JS闭包(Closures)了解一下
  2. 带你走进JS作用域(Scope)的世界

这题有点跑偏了。我们进入正题。

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有一个更深的了解可以参考

  1. 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的简单应用。

  1. 后生,React-Hooks了解一下

首先,有一点需要明确,hooks的推出,是针对函数组件。是让函数组件,也能享受state/生命周期带来的愉悦感。

给大家一个应用场景,就是有一个组件,有一个定时功能,需要一个定时器,暂且定位15min内定时器走完,并在定时器完成之后进行额外的操作。

通过上面的需求,我们来分析一波。

  1. 有一个定时功能,那势必就是在组件初始化时,要触发这个定时器,在触发定时器之后,定时器会额外的开出一个线程进行数据处理。
  2. 每次时间变化,都需要显示到页面中
  3. 我们需要监听定时器是否到达要求,(15min之后,需要额外操作)
  4. 触发额外操作
  5. 如果在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)。

比方说,现在有如下的组件,需要新增一个弹窗,而新增一个弹窗,我们来简单的细数一下需要的变量

  1. visibleForXX
  2. 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封装成一个组件前提下)具备的条件(在调用处需要准备的东西)

  1. visible
  2. this.handleVisible(false)
  3. this.handleOk(这里可能有额外操作)

列举的,是最简单的,可能有比这还复杂的。this.handleVisible(false)是控制Modal关闭的。

然后你又继续bala bala的写。如果一个页面存在1-2个modal还是可以接受,但是如果是4-5个呢,你还是用这种方式,我的天,恭喜你成为了CV俱乐部高级会员。

我可以弱弱的卑微的提出两点

  1. Modal的壳子封装成组件,你只负责content的书写
  2. 封装的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)的升级版本,只提供一个思路。主要代码如下。

该组件具备的功能:

  1. form表单和非form表单
  2. 自控显隐
  3. 防重复提交
  4. from 提交之后,能够获取到接口结果,进行其他操作
  5. 父组件也可以调用显示隐藏
  6. ....

组件调用方式

<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.ItemgetFieldDecorator这些看起来很碍眼。是不是有一种处理方式,就是简单的配置一些页面需要的组件,而这些处理放在一个壳子里呢。

 <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中组件化,是一个编程思路,我感觉,懒人才可以真正的领略到他的魅力,因为懒人才可以想出让自己书写代码,更快,更高,更强的方式。

这也是一种编程习惯和方式。用最短的时间,写最少的代码,干最漂亮的代码。

希望我啰嗦这么多,能对你有所帮助。如果大家看到有哪里不是很明白的,可以评论区,留言。如果感觉还不是很尽兴。没关系,这些东西,有很多。

其实每天都在吵吵组件化,组件化,组件化是在实际开发项目中,在解决实际业务中,才会有灵感。他主要的目的是解决实际问题,而不是闭门造车