Choerodon猪齿鱼平台使用 React 做为前端应用框架,对前端的展现作了必定的封装和处理,并配套提供了前端组件库Choerodon UI。结合实际业务状况,不断对组件优化设计,提升代码质量。css
本文将结合Choerodon猪齿鱼平台使用案例,简单说明组件的分类、设计原则和设计模式,帮助开发者在不一样场景下选择正确的设计和方案编写组件(示例代码基于ES6/ES7的语法,适于有必定前端基础的读者)。前端
本文做者:Choerodon猪齿鱼社区 王柯react
文章的主要内容包括:git
React是指用于构建用户界面的 JavaScript 库。换言之,React是一个构建视图层的类库(或框架)。无论 React 自己如何复杂,无论其生态如何庞大,构建视图始终是它的核心。github
能够用个公式说明:web
UI = f(data)
React的基础原则有三条,分别是:bootstrap
那么组件又是什么?设计模式
组件是一个函数或者一个 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猪齿鱼前端开发中的业务场景:
在此以Choerodon猪齿鱼平台的一个建立界面来分析。
能够看到,一个复杂界面能够分割成不少简单或复杂的组件,复杂组件还包括子组件等。此外,除了从功能维度对组件进行划分,也能够从开发者对组件的使用习惯进行分类(如下分类非对立关系):
简单说明一下几种组件:
以上这些组件编写模式基本上能够覆盖目前工做中所须要的模式。在写一些复杂的框架组件的时候,仔细设计和研究组件间的解耦和组合方式,可以使后续的项目可维护性大大加强。
对立的两大分类:
固然,React v16.7.0-alpha 中第一次引入了 Hooks 的概念,Hooks 的目的是让开发者不须要再用 class 来实现组件。这是React的将来,基于函数的组件也可处理状态。
了解了这些之后就须要有一个本身开发新组件前的思考,遵循组件设计原则,快速肯定分类开始编写Code。
React 的组件实际上是软件设计中的模块,其设计原则也需听从通用的组件设计原则,简单说来,就是要减小组件之间的耦合性(Coupling),让组件简单,这样才能让总体系统易于理解、易于维护。
即,设计原则:
就像搭积木,复杂的应用和组件都是由简单的界面和组件组成的。划分组件也没有绝对的方法,选择在当下场景合适的方式划分,充分利用组合便可。实际编写代码也是逐步精进的过程,努力作到:
取Choerodon猪齿鱼平台Devops项目的应用管理模块实例,导入应用:
这个界面看起来很简单,功能简介 + 导入步骤条,实际由于存在步骤条,内容很丰富。
首先组件叫作AppImport,组件内包含简介和步骤条,须要记录当前步骤条第几步状态’current‘,因此须要维持状态(state),能够确定,AppImport 是一个有状态的组件,不能只是一个纯函数,而是一个继承自 Component 的类。
class AppImport extends React.Component { constructor() { super(...arguments); this.state = { current: 0, }; } render() { //TODO: 返回全部JSX } }
接下来划分组件,按照数据边界来分割组件:
在 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));
过程当中会接触到一些最佳实践和技巧:
不一样的业务情境下使用合适的设计模式能大大提升开发效率和可维护性。了解以上内容后能更好的理解和选择设计模式。
经常使用的设计模式有:
网上介绍这些模式的文章有不少,每一个模式均可以长篇详解。可是,模式就是特定于一种问题场景的解决办法。
模式(Pattern) = 问题场景(Context) + 解决办法(Solution)
明确使用场景才能正确发挥模式的功能。因此,简单介绍一下各模式实际应用于什么场景较好。
React最简单也是最经常使用的一种组件模式就是“容器组件和展现组件”。其本质就是把一个功能分配到两个组件中,造成父子关系,外层的父组件负责管理数据状态,内层的子组件只负责展现,让一个模块都专一于一个功能,这样更利于代码的维护。
上文步骤条的实例就是把获取和管理数据这件事和界面渲染这件事分开。作法就是,把获取和管理数据的逻辑放在父组件,也就是容器组件;把渲染界面的逻辑放在子组件,也就是展现组件。有关数据处理的变更就只须要对容器组件进行修改,例如修改数据状态管理方式,彻底不影响展现组件。
高阶组件适用场景于“不要重复本身”(DRY,Don't Repeat Yourself)编码原则,某些功能是多个组件通用的,在每一个组件都重复实现逻辑,浪费、可维护行低。第一想法是共用逻辑提取为一个 React 组件,可是共用逻辑单独没法使用,不足以抽象成组件,仅仅是对其余组件的功能增强。固然,高阶组件并非 React 中惟一的重用组件逻辑的方式,下文的 render props 模式也可处理。
例如,不少网站应用,有些模块都须要在用户已经登陆的状况下才显示。好比,对于一个电商类网站,“退出登陆”按钮、“购物车”这些模块,就只有用户登陆以后才显示,对应这些模块的 React 组件若是连“只有在登陆时才显示”的功能都重复实现,那就浪费了。
所谓 render props,指的是让 React 组件的 props 支持函数这种模式。由于做为 props 传入的函数每每被用来渲染一部分界面,因此这种模式被称为 render 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猪齿鱼做为开源多云应用平台,是基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。
你们也能够经过如下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:
欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。