前端工程化是从软件工程衍生出的概念, 它是指以工程化的方法构建和维护有效、实用和高质量的软件。
前端从刀耕火种到现在社区空前繁荣的发展历史,也是工程化的演变历史。从演变的历史来看,前端工程化主要分模块化,组件化,工具化,自动化。
本篇文章主要谈谈组件化以及组件化的实践方案。
组件化
组件并不是一个新的概念。c++ 中就提出过这样的概念,组件是对数据和方法的封装,即对象。
前端中的组件则是对一块视图区域的封装,多个组件组合成了一个复合组件,多个组件组合成了一个页面,所以页面也是组件。
一个组件中包含了完整视图结构,样式表以及交互逻辑。它是封闭的结构,对外提供属性的输入用来满足用户的多样化需求,对内自己管理内部状态来满足自己的交互需求,所以组件的封装也是对象的封装,同样要做到高内聚,低耦合。
组件化的目的是为了实现代码的更高层次的复用,提高开发效率,同样它也是前端工程化的基础。组件化的页面不仅仅利于开发者在进行单元测试,同样也有益于测试的有效进行。
组件化的实践
Web Components
Web Components 是W3C提出的组件化规范。允许开发者使用规范提供的API 以及HTML 模板来实现可重用的定制元素,与HTML 原本支持的标签元素相同,可以直接在页面中使用。
Web Components 主要有三种技术构成:
- Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在的用户界面中按照需要使用它们。
- Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
-
HTML templates(HTML模板):
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
相关的开发教程,请参见 Web Components
但是由于规范不是很完善,现只有Firefox(版本63)、Chrome和Opera都默认支持Web组件; Safari支持许多web组件特性,但比前面的浏览器少; Edge正在开发一个实现。
Stencil Js
Stencil 是由Ionic 团队开发出的前端视图库,类似react。它可以将开发者开发的组件编译成 Web Components ,可以直接运行在浏览器中。抹平了前端不同技术架构造成的隔离,比如采用Stencil 开发的组件,在Vue 和 React 中都可以使用,不需要再进行额外的兼容工作。
Angular Directives / VUE Components / React Components / Backbone Components / Anything... Components
社区的视图库或者框架提供的组件解决方案,是当前最具代表性的组件化的实践,但是不同的框架之间的隔离也同样是前端需要考虑的问题。
封装React 组件
封装组件最主要要遵循开闭原则和单一职责原则,这也是封装一个高内聚低耦合的组件的核心。
简而言之,就是首先要知道你的组件是做什么的,并且它只做这一件事;其次对你的组件的对外的属性输出和对内的状态管理有明确和清晰的认知,什么开放给用户,什么自己管理。
1.界定一个组价的Scope
这里推荐React 官方文档上拆分一个页面成多个组件的方法,React 设计哲学。
- FilterableProductTable (橙色): 是整个示例应用的整体
- SearchBar (蓝色): 接受所有的用户输入
- ProductTable (绿色): 展示数据内容并根据用户输入筛选结果
- ProductCategoryRow (天蓝色): 为每一个产品类别展示标题
- ProductRow (红色): 每一行展示一个产品
对应的层级:
FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
2.区分Props 和 State
在React 中 UI = Component(Props, State)。
Props 是组件对外的接口,不可变。组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的props属性进行传递,因此props是组件对外的接口。
State 是组件对内的状态,可变。state必须能代表一个组件UI呈现的完整状态集,即组件对应UI的任何改变,都可以从state的变化中反映出来;同时,state还必须是代表一个组件UI呈现的最小状态集,即state中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。
组件中用到的一个变量是不是应该作为组件state,可以通过下面的4条依据进行判断:
- 这个变量是否是通过props从父组件中获取?如果是,那么它不是一个状态。
- 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
- 这个变量是否可以通过state 或props 中的已有数据计算得到?如果是,那么它不是一个状态。
- 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性(除了props 和 state以外的组件属性 ),例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。
同时,并不是组件中用到的所有变量都是组件的State!当存在多个组件共同依赖同一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。
在项目中开发组件
在项目中开发组件,最重要的是明确组件的作用域。
- 业务组件,它会包含你的业务逻辑,方便你对代码的分割。
- UI组件(以上提出的关于组价的封装的方法论都是UI 组件), 即一个公共的组件,它所包含的应该只有该UI 组件的交互逻辑。如果当你觉得它会有一些公共的业务逻辑的时候,请再进行二次封装,即在该UI 组件的基础上在进行一层业务的包裹。
封装UI组件
社区中优秀的React 组件库有很多,但是不同的公司有不同的设计语言,如果单纯的采用某社区UI库的话,比如Ant-design, 势必不能满足业务的需求和风格的需求。
所以对于我们来说,我们现阶段采用的封装复合公司风格的UI 组件时, React-component,在这个基础库上进行二次开发。
- React-component, 是Ant-design 的组件基础库,它提供了完善的API以及简单的样式,所以我们需要对它的样式进行定制,满足公司的设计语言。
import * as React from "react";
import styles from "./Switch.less";
import RcSwitch from "rc-switch";
import "rc-switch/assets/index.css";
type SwitchChangeEventHandler = (
checked: boolean,
event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
) => void;
interface SwitchProps {
className?: string;
prefixCls?: string;
disabled?: boolean;
checkedChildren?: React.ReactNode;
unCheckedChildren?: React.ReactNode;
onChange?: SwitchChangeEventHandler;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onClick?: SwitchChangeEventHandler;
tabIndex?: number;
checked?: boolean;
defaultChecked?: boolean;
loadingIcon?: React.ReactNode;
style?: React.CSSProperties;
title?: string;
text?: string[];
}
interface SwitchState {
checked: boolean;
}
export default class Switch extends React.PureComponent<SwitchProps, SwitchState> {
constructor(props) {
super(props);
this.state = {
checked: !!props.checked
};
}
...
...
...
render() {
const { text } = this.props;
const { checked } = this.state;
const label = checked ? text[0] : text[1];
return (
<div className={styles.switch}>
{text && text.length && <span className={styles.label}>{label}</span>}
<RcSwitch {...this.props} className={styles.bgColor} onChange={this.onChangeHandler} />
</div>
);
}
}