React 世界的一等公民 - 组件

Choerodon猪齿鱼平台使用 React 做为前端应用框架,对前端的展现作了必定的封装和处理,并配套提供了前端组件库Choerodon UI。结合实际业务状况,不断对组件优化设计,提升代码质量。css

本文将结合Choerodon猪齿鱼平台使用案例,简单说明组件的分类、设计原则和设计模式,帮助开发者在不一样场景下选择正确的设计和方案编写组件(示例代码基于ES6/ES7的语法,适于有必定前端基础的读者)。前端

本文做者:Choerodon猪齿鱼社区 王柯react

文章的主要内容包括:git

  • React 组件简介
  • 组件分类
  • 组件设计原则、最佳实践
  • 组件设计模式简介

React 组件简介

React是指用于构建用户界面的 JavaScript 库。换言之,React是一个构建视图层的类库(或框架)。无论 React 自己如何复杂,无论其生态如何庞大,构建视图始终是它的核心。github

能够用个公式说明:web

UI = f(data)

React的基础原则有三条,分别是:bootstrap

  1. React 界面彻底由数据驱动;
  2. React 中一切都是组件;
  3. props 是 React 组件之间通信的基本方式。

那么组件又是什么?设计模式

组件是一个函数或者一个 Class(固然 Class 也是 function),它根据输入参数,最终返回一个 React Element。简单地说,React Element 描述了“你想”在屏幕上看到的事物。抽象地说,React Element 元素是一个描述了 Dom Node 的对象。api

因此实际上使用 React Component 来生成 React Element,对于开发体验有巨大的提高,好比不须要手写React.createElement等。前端框架

那么全部 React Component 都须要返回 React Element 吗?显然是不须要的。 return null; 或者返回其余的 React 组件都有存在的意义,它能完成并实现不少巧妙的设计、思想和反作用,在下文会有所扩展。

能够说,在 React 中一切皆为组件:

  • 用户界面就是组件;
  • 组件能够嵌套包装组成复杂功能;
  • 组件能够用来实现反作用。

React 也提供了多种编写组件的方法适用于各类场景实例。

组件分类

如何在场景下快速正确地选择组件设计模式和方案,首先得有一个本身接受和经常使用的组件分类,以便从分类中快速肯定编写方法,再考虑设计模式等后续问题。

Vue的做者尤雨溪在一场Live中也表达过本身对前端组件的见解,“组件能够是函数,是有分类的。”从功能维度对组件进行了分类,这四种分类方式也适用于Choerodon猪齿鱼前端开发中的业务场景:

  • 纯展现型组件:数据进,DOM出,直观明了
  • 接入型组件:在React场景下的container
  • component,这种组件会跟数据层的service打交道,会包含一些跟服务器或者说数据源打交道的逻辑,container会把数据向下传递给展现型组件、
  • 交互型组件:典型的例子是对于表单组件的封装和增强,大部分的组件库都是以交互型组件为主,好比说Element UI,特色是有比较复杂的交互逻辑,可是是比较通用的逻辑,强调组件的复用
  • 功能型组件:以Vue的应用场景举例,路由的router-view组件、transition组件,自己并不渲染任何内容,是一个逻辑型的东西,做为一种扩展或者是抽象机制存在

在此以Choerodon猪齿鱼平台的一个建立界面来分析。

  • 红色布局:功能型组件
  • 蓝色菜单:交互型组件,菜单项:遍历菜单数据输出DOM的纯展现型组件
  • 右块内容:接入型组件(容器组件)
  • Table、btn等:交互型组件

能够看到,一个复杂界面能够分割成不少简单或复杂的组件,复杂组件还包括子组件等。此外,除了从功能维度对组件进行划分,也能够从开发者对组件的使用习惯进行分类(如下分类非对立关系):

  • 无状态组件
  • 有状态组件
  • 容器组件
  • 高阶组件
  • Render Callback组件

简单说明一下几种组件:

  • 无状态组件:无状态组件(Stateless Component)是最基础的组件形式,因为没有状态的影响因此就是纯静态展现的做用。基本组成结构就是属性(props)加上一个渲染函数(render)。因为不涉及到状态的更新,因此这种组件的复用性也最强。例如在各UI库中开发的按钮、输入框、图标等等。
  • 有状态组件:组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件一般会带有生命周期(lifecycle),用以在不一样的时刻触发状态的更新。在写业务逻辑时经常使用到,不一样场景所用的状态和生命周期也会不一样。
  • 容器组件:为使组件的职责更加单一,耦合性进一步地下降,引入了容器组件(Container Component)的概念。重要负责对数据获取以及处理的逻辑。下文的设计模式也会提到。
  • 高阶组件:“高阶组件(HoC)”也算是种组件设计模式。作为一个高阶组件,能够在原有组件的基础上,对其增长新的功能和行为。如打印日志,获取数据和校验数据等和展现无关的逻辑的时候,抽象出一个高阶组件,用以给基础的组件增长这些功能,减小公共的代码。
  • Render Callback组件:组件模式是在组件中使用渲染回调的方式,将组件中的渲染逻辑委托给其子组件。也是种重用组件逻辑的方式,也叫render props 模式。

以上这些组件编写模式基本上能够覆盖目前工做中所须要的模式。在写一些复杂的框架组件的时候,仔细设计和研究组件间的解耦和组合方式,可以使后续的项目可维护性大大加强。

对立的两大分类:

  • 基于类的组件:基于类的组件(Class based components)是包含状态和方法的。
  • 基于函数的组件:基于函数的组件(Functional Components)是没有状态和方法的。它们是纯粹的、易读的。尽量的使用它们。

固然,React v16.7.0-alpha 中第一次引入了 Hooks 的概念,Hooks 的目的是让开发者不须要再用 class 来实现组件。这是React的将来,基于函数的组件也可处理状态。

了解了这些之后就须要有一个本身开发新组件前的思考,遵循组件设计原则,快速肯定分类开始编写Code。

设计原则/最佳实践

React 的组件实际上是软件设计中的模块,其设计原则也需听从通用的组件设计原则,简单说来,就是要减小组件之间的耦合性(Coupling),让组件简单,这样才能让总体系统易于理解、易于维护。

即,设计原则:

  1. 接口小,props 数量少;
  2. 划分组件,充分利用组合(composition);
  3. 把 state 往上层组件提取,让下层组件只须要实现为纯函数。

就像搭积木,复杂的应用和组件都是由简单的界面和组件组成的。划分组件也没有绝对的方法,选择在当下场景合适的方式划分,充分利用组合便可。实际编写代码也是逐步精进的过程,努力作到:

  1. 功能正常;
  2. 代码整洁;
  3. 高性能。

取Choerodon猪齿鱼平台Devops项目的应用管理模块实例,导入应用:

这个界面看起来很简单,功能简介 + 导入步骤条,实际由于存在步骤条,内容很丰富。

首先组件叫作AppImport,组件内包含简介和步骤条,须要记录当前步骤条第几步状态’current‘,因此须要维持状态(state),能够确定,AppImport 是一个有状态的组件,不能只是一个纯函数,而是一个继承自 Component 的类。

class AppImport extends React.Component {
    constructor() {
    super(...arguments);
    this.state = {
      current: 0,
    };
  }
  render() {
     //TODO: 返回全部JSX
  }
}

接下来划分组件,按照数据边界来分割组件:

  • 使用了choerodon-front-boot 中定义好的容器组件,Page、Header、Content;
  • 渲染 Header,返回上级菜单,渲染当前界面title。
  • 渲染 Content,封装好的组件处理了导入应用和其详情简介;
  • 渲染 Steps 卡片,步骤条卡片渲染,state 为当前步以及后续须要导入提交的数据 data;
  • 最后,Steps 每一步数据需求都不一样,均拆成单独子组件。

在 React 中,有一个误区,就是把 render 中的代码分拆到多个 renderXXXX 函数中去,好比下面这样:

class AppImport extends React.Component {
  render() {
    const Header = this.renderHeader();
    const Content = this.renderContent();
    const Steps = this.renderSteps();

    return (
       <Page>
          {Header}
          {Content}
          {Steps}
       </Page>
    );
  }

  renderHeader() {
     //TODO: 返回上级菜单,渲染当前界面title
  }

  renderContent() {
     //TODO: 导入应用和其详情简介
  }

  renderSteps() {
     //TODO: 返回步骤条卡片
  }
}

用上面的方法组织代码,固然比写一个巨大的 render 函数要强,可是,实现这么多 renderXXXX 函数并非一个明智之举,由于这些 renderXXXX 函数访问的是一样的 props 和 state,这样代码依然耦合在了一块儿。更好的方法是把这些 renderXXXX 重构成各自独立的 React 组件,像下面这样

class AppImport extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      data: {},
      current: 0,
    };
  }

  next = () => {}

  cancel = () => {}

  render() {
    return (
      <Page>
        <Header title='xxx' backPath='xxxxxx' />
        <Content code="app.import" values={{ appName }}>
          <div className="c7n-app-import-wrap">
            <Steps current={current} className="steps-line">
              <Step key={item.key} title={item.title} />
            </Steps>
            <div className="steps-content">
              <Step0 onNext={this.next} onCancel={this.cancel} values={data} />
            </div>
          </div>
        </Content>
      </Page>
    );
  }
}

const Step = (props) => {
  //TODO: 返回步骤条Content
};

const Steps = (props) => {
  //TODO: Steps
};

const Page = (props) => {
  //TODO: Page
}

// Header / Content 

// 根据代码量,尽可能每一个组件都有本身专属的源代码文件 导出,再导入
// 示例代码中 Page、Header、Content 使用了choerodon-front-boot 中定义好的容器组件,
// Steps 使用了choerodon-ui 库
// 因此在头部导入便可
// import { Steps } from 'choerodon-ui';
// import { Content, Header, Page } from 'choerodon-front-boot';

实际状况下,步骤条不止一步,处理函数也不止那么简单,可是通过划分和抽取,做为展现组件的 AppImport 结构清晰,代码整洁,接口少(props只涉及公共的 store、history 等 )。再处理下StepN(子组件根据实际内容处理,这里略过),整个 AppImport 代码不超过150行,相比不划分组件,代码随便超过1000+行,划分优化后思路清晰,可维护性高。

最终代码:

import React, { Component, Fragment } from 'react';
import { observer } from 'mobx-react';
import { withRouter } from 'react-router-dom';
import { injectIntl, FormattedMessage } from 'react-intl';
import { Steps } from 'choerodon-ui';
import { Content, Header, Page, stores } from 'choerodon-front-boot';
import '../../../main.scss';
import './AppImport.scss';
import { Step0, Step1, Step2, Step3 } from './steps/index';

const { AppState } = stores;
const Step = Steps.Step;

@observer
class AppImport extends Component {
  constructor() {
    super(...arguments);
    this.state = {
      data: {},
      current: 0,
    };
  }

  next = (values) => {
    // 点击下一步处理函数,略
  };

  prev = () => {
    // 点击上一步处理函数,略
  };

  cancel = () => {
    // 点击取消处理函数,略
  };

  importApp = () => {
    // 点击导入,数据处理,略
  };

  render() {
    const { current, data } = this.state;
    // const ...

    const steps = [{
      key: 'step0',
      title: <FormattedMessage id="app.import.step1" />,
      content: <Step0 onNext={this.next} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step1',
      title: <FormattedMessage id="app.import.step2" />,
      content: <Step1 onNext={this.next} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step2',
      title: <FormattedMessage id="app.import.step3" />,
      content: <Step2 onNext={this.next} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }, {
      key: 'step3',
      title: <FormattedMessage id="app.import.step4" />,
      content: <Step3 onImport={this.importApp} onPrevious={this.prev} onCancel={this.cancel} store={AppStore} values={data} />,
    }];

    return (
      <Page>
        <Header title='xxx' backPath='xxxxxx' />
        <Content code="app.import" values={{ name }}>
          <div className="c7n-app-import-wrap">
            <Steps current={current} className="steps-line">
              {steps.map(item => <Step key={item.key} title={item.title} />)}
            </Steps>
            <div className="steps-content">{steps[current].content}</div>
          </div>
        </Content>
      </Page>
    );
  }
}

export default withRouter(injectIntl(AppImport));

过程当中会接触到一些最佳实践和技巧:

  1. 避免 renderXXXX 函数
  2. 给回调函数类型的 props 加统一前缀(onNext、onXXX 或 handleXXX 规范,可读性好)
  3. 使用 propTypes 来定义组件的 props
  4. 尽可能每一个组件都有本身专属的源代码文件(StepN)
  5. 用解构赋值(destructuring assignment)的方法获取参数 props 的每一个属性值
  6. 利用属性初始化(property initializer)来定义 state 和成员函数

组件设计模式

不一样的业务情境下使用合适的设计模式能大大提升开发效率和可维护性。了解以上内容后能更好的理解和选择设计模式。

经常使用的设计模式有:

  1. 容器组件和展现组件(Container and Presentational Components);
  2. 高阶组件;
  3. render props 模式;
  4. 提供者模式(Provider Pattern);
  5. 组合组件。

网上介绍这些模式的文章有不少,每一个模式均可以长篇详解。可是,模式就是特定于一种问题场景的解决办法。

模式(Pattern) = 问题场景(Context) + 解决办法(Solution)

明确使用场景才能正确发挥模式的功能。因此,简单介绍一下各模式实际应用于什么场景较好。

容器组件和展现组件

React最简单也是最经常使用的一种组件模式就是“容器组件和展现组件”。其本质就是把一个功能分配到两个组件中,造成父子关系,外层的父组件负责管理数据状态,内层的子组件只负责展现,让一个模块都专一于一个功能,这样更利于代码的维护。

上文步骤条的实例就是把获取和管理数据这件事和界面渲染这件事分开。作法就是,把获取和管理数据的逻辑放在父组件,也就是容器组件;把渲染界面的逻辑放在子组件,也就是展现组件。有关数据处理的变更就只须要对容器组件进行修改,例如修改数据状态管理方式,彻底不影响展现组件。

高阶组件

高阶组件适用场景于“不要重复本身”(DRY,Don't Repeat Yourself)编码原则,某些功能是多个组件通用的,在每一个组件都重复实现逻辑,浪费、可维护行低。第一想法是共用逻辑提取为一个 React 组件,可是共用逻辑单独没法使用,不足以抽象成组件,仅仅是对其余组件的功能增强。固然,高阶组件并非 React 中惟一的重用组件逻辑的方式,下文的 render props 模式也可处理。

例如,不少网站应用,有些模块都须要在用户已经登陆的状况下才显示。好比,对于一个电商类网站,“退出登陆”按钮、“购物车”这些模块,就只有用户登陆以后才显示,对应这些模块的 React 组件若是连“只有在登陆时才显示”的功能都重复实现,那就浪费了。

render props 模式

所谓 render props,指的是让 React 组件的 props 支持函数这种模式。由于做为 props 传入的函数每每被用来渲染一部分界面,因此这种模式被称为 render props。适用场景和高阶组件差很少,可是与其仍是有一些差异:

  1. render props 模式的应用,是一个 React 组件,而高阶组件,虽然名为“组件”,其实只是一个产生 React 组件的函数
  2. 高阶组件可链式调用,由于实质是函数
  3. render props 相对于高阶组件还有一个显著优点,就是对于新增的 props 更加灵活

因此以上对比,当须要重用 React 组件的逻辑时,建议首先看这个功能是否能够抽象为一个简单的组件;若是行不通的话,考虑是否能够应用 render props 模式;再不行的话,才考虑应用高阶组件模式。固然,没有绝对的使用顺序,实际场景为准。

提供者模式

在 React 中,props 是组件之间通信的主要手段,可是,有一种场景单纯靠 props 来通信是不恰当的,那就是两个组件之间间隔着多层其余组件。避免 props 逐级传递,便是提供者模式的适用场景。实现方式也分老Context API和新Context API。新版本的 Context API 才是将来,在 React v17 中,可能就会删除对老版 Context API 的支持,因此,如今你们都应该使用第二种实现方式。新版API详解

典型用例就是实现“样式主题”(Theme),多语言支持等。

组合组件

组合组件模式要解决的是这样一类问题:父组件想要传递一些信息给子组件,可是,若是用 props 传递又显得十分麻烦。利用 Context?固然还有其余解决方案,就是组合组件模式。

应用组合组件场景的每每是共享组件库,把一些经常使用的功能封装在组件里,让应用层直接用就行。在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。将复杂度都封装起来了,从使用者角度,连 props 都看不见。实例扩展

总 结

对前端来讲,前端不是不用设计模式,而是已经把设计模式融入到了开发的基础当中。Choerodon猪齿鱼平台前端真实的业务场景每每须要应用多个设计模式,界面也会包含多个大小不一的组件。开发设计时,符合程序设计的原则:「高内聚,低耦合」便可。本文只是简单总结,提供一些思路和简单的应用场景给开发者,真正的熟练把握和应用还得多实践开发使用,多对本身欠缺的知识点去深挖学习和思考,不断进步。

参考/引用资料:

关于Choerodon猪齿鱼

Choerodon猪齿鱼做为开源多云应用平台,是基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

你们也能够经过如下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

相关文章
相关标签/搜索