作者:王柯
Choerodon猪齿鱼平台使用 React 作为前端应用框架,对前端的展示做了一定的封装和处理,并配套提供了前端组件库Choerodon UI。结合实际业务情况,不断对组件优化设计,提高代码质量。
本文将结合Choerodon猪齿鱼平台使用案例,简单说明组件的分类、设计原则和设计模式,帮助开发者在不同场景下选择正确的设计和方案编写组件(示例代码基于ES6/ES7的语法,适于有一定前端基础的读者)。
文章的主要内容包括:
React是指用于构建用户界面的 JavaScript 库。换言之,React是一个构建视图层的类库(或框架)。不管 React 本身如何复杂,不管其生态如何庞大,构建视图始终是它的核心。
可以用个公式说明:
UI = f(data) 复制代码
React的基础原则有三条,分别是:
那么组件又是什么?
组件是一个函数或者一个 Class(当然 Class 也是 function),它根据输入参数,最终返回一个 React Element。简单地说,React Element 描述了“你想”在屏幕上看到的事物。抽象地说,React Element 元素是一个描述了 Dom Node 的对象。
所以实际上使用 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 的类。
classAppImportextendsReact.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 (
{Header}
{Content}
{Steps}
);
}
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 (
);
}
}
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: ,
content: ,
}, {
key: 'step1',
title: ,
content: ,
}, {
key: 'step2',
title: ,
content: ,
}, {
key: 'step3',
title: ,
content: ,
}];
return (
{steps.map(item => )}
{steps[current].content}
);
}
}
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猪齿鱼核心团队创立的 BuildRun(https://gobuildrun.com),基于猪齿鱼带来了多云架构环境下基于视觉的企业级应用创建、集成、部署、生命周期管理和分发的能力。
BuildRun 以云原生的现代化软件架构来帮助企业提升软件开发生产力和业务敏捷性,提供多云低代码应用平台和多云应用生命周期管理平台,帮助企业隐藏应用开发和运行时的基础架构复杂性,让每个人专注于业务逻辑,促进团队快速、持续地将想法转化为真正的商业价值。
团队在云原生技术和架构(DevOps、持续交付、微服务和容器)应用场景,如企业数字化转型、企业中台等方面拥有丰富的经验,公司客户包括商业地产、建筑工程、医药、家居、汽车配业、大型工业等企业,为您提供最合适的解决方案。
免费注册试用:https://apps.gobuildrun.com/#/base/register-organization