翻译:刘小夕javascript
原文连接:dmitripavlutin.com/7-architect…html
原文的篇幅很是长,不过内容太过于吸引我,仍是忍不住要翻译出来。此篇文章对编写可重用和可维护的React组件很是有帮助。但由于篇幅实在太长,我不得不进行了分割,本篇文章重点阐述 SRP
,即单一职责原则。java
更多文章可戳: github.com/YvetteLau/B…react
————————————我是一条分割线————————————ios
我喜欢React组件式开发方式。你能够将复杂的用户界面分割为一个个组件,利用组件的可重用性和抽象的DOM操做。git
基于组件的开发是高效的:一个复杂的系统是由专门的、易于管理的组件构建的。然而,只有设计良好的组件才能确保组合和复用的好处。github
尽管应用程序很复杂,但为了知足最后期限和意外变化的需求,你必须不断地走在架构正确性的细线上。你必须将组件分离为专一于单个任务,并通过良好测试。axios
不幸的是,遵循错误的路径老是更加容易:编写具备许多职责的大型组件、紧密耦合组件、忘记单元测试。这些增长了技术债务,使得修改现有功能或建立新功能变得愈来愈困难。api
编写React应用程序时,我常常问本身:数组
幸运的是,可靠的组件具备共同的特性。让咱们来研究这7个有用的标准(本文只阐述 SRP
,剩余准则正在途中),并将其详细到案例研究中。
当一个组件只有一个改变的缘由时,它有一个单一的职责。
编写React组件时要考虑的基本准则是单一职责原则。单一职责原则(缩写:SRP
)要求组件有一个且只有一个变动的缘由。
组件的职责能够是呈现列表,或者显示日期选择器,或者发出 HTTP
请求,或者绘制图表,或者延迟加载图像等。你的组件应该只选择一个职责并实现它。当你修改组件实现其职责的方式(例如,更改渲染的列表的数量限制),它有一个更改的缘由。
为何只有一个理由能够改变很重要?由于这样组件的修改隔离而且受控。单一职责原则制了组件的大小,使其集中在一件事情上。集中在一件事情上的组件便于编码、修改、重用和测试。
下面咱们来举几个例子
实例1:一个组件获取远程数据,相应地,当获取逻辑更改时,它有一个更改的缘由。
发生变化的缘由是:
示例2:表组件将数据数组映射到行组件列表,所以在映射逻辑更改时有一个缘由须要更改。
发生变化的缘由是:
你的组件有不少职责吗?若是答案是“是”,则按每一个单独的职责将组件分红若干块。
若是您发现SRP有点模糊,请阅读本文。 在项目早期阶段编写的单元将常常更改,直到达到发布阶段。这些更改一般要求组件在隔离状态下易于修改:这也是 SRP 的目标。
当一个组件有多个职责时,就会发生一个常见的问题。乍一看,这种作法彷佛是无害的,而且工做量较少:
props
和 callbacks
这种幼稚的结构在开始时很容易编码。可是随着应用程序的增长和变得复杂,在之后的修改中会出现困难。同时实现多个职责的组件有许多更改的缘由。如今出现的主要问题是:出于某种缘由更改组件会无心中影响同一组件实现的其它职责。
不要关闭电灯开关,由于它一样做用于电梯。
这种设计很脆弱。意外的反作用是很难预测和控制的。
例如,<ChartAndForm>
同时有两个职责,绘制图表,并处理为该图表提供数据的表单。<ChartandForm>
就会有两个更改缘由:绘制图表和处理表单。
当你更改表单字段(例如,将 <input>
修改成 <select>
时,你无心中中断图表的渲染。此外,图表实现是不可重用的,由于它与表单细节耦合在一块儿。
解决多重责任问题须要将 <ChartAndForm>
分割为两个组件:<Chart>
和<Form>
。每一个组件只有一个职责:绘制图表或处理表单。组件之间的通讯是经过props
实现。
多重责任问题的最坏状况是所谓的上帝组件(上帝对象的类比)。上帝组件倾向于了解并作全部事情。你可能会看到它名为 <Application>
、<Manager>
、<Bigcontainer>
或 <Page>
,代码超过500行。
在组合的帮助下使其符合SRP,从而分解上帝组件。(组合(composition)是一种经过将各组件联合在一块儿以建立更大组件的方式。组合是 React 的核心。)
设想一个组件向一个专门的服务器发出 HTTP
请求,以获取当前天气。成功获取数据时,该组件使用响应来展现天气信息:
import axios from 'axios'; // 问题: 一个组件有多个职责 class Weather extends Component { constructor(props) { super(props); this.state = { temperature: 'N/A', windSpeed: 'N/A' }; } render() { const { temperature, windSpeed } = this.state; return ( <div className="weather"> <div>Temperature: {temperature}°C</div> <div>Wind: {windSpeed}km/h</div> </div> ); } componentDidMount() { axios.get('http://weather.com/api').then(function (response) { const { current } = response.data; this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }); } } 复制代码
在处理相似的状况时,问问本身:是否必须将组件拆分为更小的组件?经过肯定组件可能会如何根据其职责进行更改,能够最好地回答这个问题。
这个天气组件有两个改变缘由:
componentDidMount()
中的 fetch
逻辑:服务器URL或响应格式可能会改变。
render()
中的天气展现:组件显示天气的方式能够屡次更改。
解决方案是将 <Weather>
分为两个组件:每一个组件只有一个职责。命名为 <WeatherFetch>
和 <WeatherInfo>
。
<WeatherFetch>
组件负责获取天气、提取响应数据并将其保存到 state
中。它改变缘由只有一个就是获取数据逻辑改变。
import axios from 'axios'; // 解决措施: 组件只有一个职责就是请求数据 class WeatherFetch extends Component { constructor(props) { super(props); this.state = { temperature: 'N/A', windSpeed: 'N/A' }; } render() { const { temperature, windSpeed } = this.state; return ( <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { axios.get('http://weather.com/api').then(function (response) { const { current } = response.data; this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }); }); } } 复制代码
这种结构有什么好处?
例如,你想要使用 async/await
语法来替代 promise
去服务器获取响应。更改缘由:修改获取逻辑
// 改变缘由: 使用 async/await 语法 class WeatherFetch extends Component { // ..... // async componentDidMount() { const response = await axios.get('http://weather.com/api'); const { current } = response.data; this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }); } } 复制代码
由于 <WeatherFetch>
只有一个更改缘由:修改 fetch
逻辑,因此对该组件的任何修改都是隔离的。使用 async/await
不会直接影响天气的显示。
<WeatherFetch>
渲染 <WeatherInfo>
。后者只负责显示天气,改变缘由只多是视觉显示改变。
// 解决方案: 组件只有一个职责,就是显示天气 function WeatherInfo({ temperature, windSpeed }) { return ( <div className="weather"> <div>Temperature: {temperature}°C</div> <div>Wind: {windSpeed} km/h</div> </div> ); } 复制代码
让咱们更改<WeatherInfo>
,如不显示 “wind:0 km/h”
而是显示 “wind:calm”
。这就是天气视觉显示发生变化的缘由:
// 改变缘由: 无风时的显示 function WeatherInfo({ temperature, windSpeed }) { const windInfo = windSpeed === 0 ? 'calm' : `${windSpeed} km/h`; return ( <div className="weather"> <div>Temperature: {temperature}°C</div> <div>Wind: {windInfo}</div> </div> ); } 复制代码
一样,对 <WeatherInfo>
的修改是隔离的,不会影响 <WeatherFetch>
组件。
<WeatherFetch>
和 <WeatherInfo>
有各自的职责。一种组件的变化对另外一种组件的影响很小。这就是单一职责原则的做用:修改隔离,对系统的其余组件产生影响很轻微而且可预测。
按职责使用分块组件的组合并不老是有助于遵循单一责任原则。另一种有效实践是高阶组件(缩写为 HOC
)
高阶组件是一个接受一个组件并返回一个新组件的函数。
HOC
的一个常见用法是为封装的组件增长新属性或修改现有的属性值。这种技术称为属性代理:
function withNewFunctionality(WrappedComponent) { return class NewFunctionality extends Component { render() { const newProp = 'Value'; const propsProxy = { ...this.props, // 修改现有属性: ownProp: this.props.ownProp + ' was modified', // 增长新属性: newProp }; return <WrappedComponent {...propsProxy} />; } } } const MyNewComponent = withNewFunctionality(MyComponent); 复制代码
你还能够经过控制输入组件的渲染过程从而控制渲染结果。这种 HOC
技术被称为渲染劫持:
function withModifiedChildren(WrappedComponent) {
return class ModifiedChildren extends WrappedComponent {
render() {
const rootElement = super.render();
const newChildren = [
...rootElement.props.children,
// 插入一个元素
<div>New child</div>
];
return cloneElement(
rootElement,
rootElement.props,
newChildren
);
}
}
}
const MyNewComponent = withModifiedChildren(MyComponent);
复制代码
若是您想深刻了解HOCS实践,我建议您阅读“深刻响应高阶组件”。
让咱们经过一个例子来看看HOC的属性代理技术如何帮助分离职责。
组件 <PersistentForm>
由 input
输入框和按钮 save to storage
组成。更改输入值后,点击 save to storage
按钮将其写入到 localStorage
中。
复制代码
input
的状态在 handlechange(event)
方法中更新。点击按钮,值将保存到本地存储,在 handleclick()
中处理:
class PersistentForm extends Component { constructor(props) { super(props); this.state = { inputValue: localStorage.getItem('inputValue') }; this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); } render() { const { inputValue } = this.state; return ( <div className="persistent-form"> <input type="text" value={inputValue} onChange={this.handleChange} /> <button onClick={this.handleClick}>Save to storage</button> </div> ); } handleChange(event) { this.setState({ inputValue: event.target.value }); } handleClick() { localStorage.setItem('inputValue', this.state.inputValue); } } 复制代码
遗憾的是: <PersistentForm>
有2个职责:管理表单字段;将输入只保存中 localStorage
。
让咱们重构一下 <PersistentForm>
组件,使其只有一个职责:展现表单字段和附加的事件处理程序。它不该该知道如何直接使用存储:
class PersistentForm extends Component { constructor(props) { super(props); this.state = { inputValue: props.initialValue }; this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); } render() { const { inputValue } = this.state; return ( <div className="persistent-form"> <input type="text" value={inputValue} onChange={this.handleChange} /> <button onClick={this.handleClick}>Save to storage</button> </div> ); } handleChange(event) { this.setState({ inputValue: event.target.value }); } handleClick() { this.props.saveValue(this.state.inputValue); } } 复制代码
组件从属性初始值接收存储的输入值,并使用属性函数 saveValue(newValue)
来保存输入值。这些props
由使用属性代理技术的 withpersistence()
HOC提供。
如今 <PersistentForm>
符合 SRP
。更改的惟一缘由是修改表单字段。
查询和保存到本地存储的职责由 withPersistence()
HOC承担:
function withPersistence(storageKey, storage) { return function (WrappedComponent) { return class PersistentComponent extends Component { constructor(props) { super(props); this.state = { initialValue: storage.getItem(storageKey) }; } render() { return ( <WrappedComponent initialValue={this.state.initialValue} saveValue={this.saveValue} {...this.props} /> ); } saveValue(value) { storage.setItem(storageKey, value); } } } } 复制代码
withPersistence()
是一个 HOC
,其职责是持久的。它不知道有关表单域的任何详细信息。它只聚焦一个工做:为传入的组件提供 initialValue
字符串和 saveValue()
函数。
将 <PersistentForm>
和 withpersistence()
一块儿使用能够建立一个新组件<LocalStoragePersistentForm>
。它与本地存储相连,能够在应用程序中使用:
const LocalStoragePersistentForm = withPersistence('key', localStorage)(PersistentForm); const instance = <LocalStoragePersistentForm />; 复制代码
只要 <PersistentForm>
正确使用 initialValue
和 saveValue()
属性,对该组件的任何修改都不能破坏 withPersistence()
保存到存储的逻辑。
反之亦然:只要 withPersistence()
提供正确的 initialValue
和 saveValue()
,对 HOC
的任何修改都不能破坏处理表单字段的方式。
SRP的效率再次显现出来:修改隔离,从而减小对系统其余部分的影响。
此外,代码的可重用性也会增长。你能够将任何其余表单 <MyOtherForm>
链接到本地存储:
const LocalStorageMyOtherForm = withPersistence('key', localStorage)(MyOtherForm); const instance = <LocalStorageMyOtherForm />; 复制代码
你能够轻松地将存储类型更改成 session storage
:
const SessionStoragePersistentForm = withPersistence('key', sessionStorage)(PersistentForm); const instance = <SessionStoragePersistentForm />; 复制代码
初始版本 <PersistentForm>
没有隔离修改和可重用性好处,由于它错误地具备多个职责。
在很差分块组合的状况下,属性代理和渲染劫持的 HOC
技术可使得组件只有一个职责。
谢谢各位小伙伴愿意花费宝贵的时间阅读本文,若是本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的确定是我前进的最大动力。 github.com/YvetteLau/B…