翻译:刘小夕javascript
原文连接:dmitripavlutin.com/7-architect…html
原文的篇幅很是长,不过内容太过于吸引我,仍是忍不住要翻译出来。此篇文章对编写可重用和可维护的React组件很是有帮助。但由于篇幅实在太长,我对文章进行了分割,本篇文章重点阐述 纯组件和几乎纯组件 。因水平有限,文中部分翻译可能不够准确,若是你有更好的想法,欢迎在评论区指出。java
更多优质文章可戳: github.com/YvetteLau/B…react
———————————————我是一条分割线————————————————ios
纯组件老是为相同的属性值渲染相同的元素。几乎纯的组件老是为相同的属性值呈现相同的元素,可是会产生反作用。git
在函数编程属于中,对于给定的相同输入,纯函数老是返回相同的输出,而且不会对外界产生反作用。github
function sum(a, b) {
return a + b;
}
sum(5, 10); // => 15
复制代码
对于给定的两个数字,sum()
函数老是会返回相同的结果。编程
当一个函数输入相同,而输出不一样时,它就不是一个纯函数。当这个函数依赖于全局的状态时,就不是一个纯函数,例如:redux
let said = false;
function sayOnce(message) {
if (said) {
return null;
}
said = true;
return message;
}
sayOnce('Hello World!'); // => 'Hello World!'
sayOnce('Hello World!'); // => null
复制代码
sayOnce('Hello World!')
第一次调用时,返回 Hello World
.axios
即便输入参数相同,都是 Hello World
,可是第二次调用 sayOnce('Hello World!')
,返回的结果是 null
。这里有一个非纯函数的特征:依赖全局状态 said
sayOnce()
的函数体内,said = true
修改了全局状态,对外界产生的反作用,这也是非纯函数的特征之一。
而纯函数没有反作用且不依赖于全局状态。只要输入相同,输出必定相同。所以,纯函数的结果是可预测的,肯定的,能够复用,而且易于测试。
React
组件也应该考虑设计为纯组件,当 prop
的值相同时, 纯组件(注意区分React.PureComponent
)渲染的内容相同,一块儿来看例子:
function Message({ text }) {
return <div className="message">{text}</div>;
}
<Message text="Hello World!" />
// => <div class="message">Hello World</div>
复制代码
当传递给 Message
的 prop
值相同时,其渲染的元素也相同。
想要确保全部的组件都是纯组件是不可能的,有时候,你须要知道与外界交互,例以下面的例子:
class InputField extends Component {
constructor(props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
}
handleChange({ target: { value } }) {
this.setState({ value });
}
render() {
return (
<div> <input type="text" value={this.state.value} onChange={this.handleChange} /> You typed: {this.state.value} </div> ); } } 复制代码
<InputField>
组件,不接受任何 props
,而是根据用户的输入内容渲染输出。<InputField>
必须是非纯组件,由于它须要经过 input
输入框与外界交互。
非纯组件是必要的,大多数应用程序中都须要全局状态,网络请求,本地存储等。你所能作的就是将 纯组件和非纯组件隔离,也就是说将你的组件进行提纯。
非纯代码显式的代表了它有反作用,或者是依赖全局状态。在隔离状态下,不纯代码对系统其它部分的不可预测的影响较小。
让咱们详细介绍一下提纯的例子。
我不喜欢全局变量,由于它们打破了封装,创造了不可预测的行为,而且使测试变得困难。
全局变量能够做为可变对象或者是不可变对象使用。
可变的全局变量使得组件的行为难以控制,数据能够随意的注入和修改,影响协调过程,这显然是错误的。
若是你须要可变的全局状态,那么你能够考虑使用 Redux
来管理你的应用程序状态。
不可变的全局变量一般是应用程序的配置对象,这个对象中包含站点名称、登陆用户名或者其它的配置信息。
如下代码定义一个包含站点名称的配置对象:
export const globalConfig = {
siteName: 'Animals in Zoo'
};
复制代码
<Header>
组件渲染应用的头部,包括展现站点名称: Animals in Zoo
import { globalConfig } from './config';
export default function Header({ children }) {
const heading =
globalConfig.siteName ? <h1>{globalConfig.siteName}</h1> : null;
return (
<div> {heading} {children} </div>
);
}
复制代码
<Header>
组件使用 globalConfig.siteName
来展现站点名称,当 globalConfig.siteName
未定义时,不显示。
首先须要注意的是 <Header>
是非纯组件。即便传入的 children
值相同,也会由于 globalConfig.siteName
值的不一样返回不一样的结果。
// globalConfig.siteName is 'Animals in Zoo'
<Header>Some content</Header>
// Renders:
<div>
<h1>Animals in Zoo</h1>
Some content
</div>
复制代码
或:
// globalConfig.siteName is `null`
<Header>Some content</Header>
// Renders:
<div>
Some content
</div>
复制代码
其次,测试变得困难重重,为了测试组件如何处理站点名为 null
,咱们不得不手动地设置 globalConfig.siteName = null
import assert from 'assert';
import { shallow } from 'enzyme';
import { globalConfig } from './config';
import Header from './Header';
describe('<Header />', function () {
it('should render the heading', function () {
const wrapper = shallow(
<Header>Some content</Header>
);
assert(wrapper.contains(<h1>Animals in Zoo</h1>));
});
it('should not render the heading', function () {
// Modification of global variable:
globalConfig.siteName = null;
const wrapper = shallow(
<Header>Some content</Header>
);
assert(appWithHeading.find('h1').length === 0);
});
});
复制代码
为了测试而修改 globalConfig.siteName = null
是不方便的。发生这种状况是由于 <Heading>
对全局变量有很强的依赖。
为了解决这个问题,能够将全局变量做为组件的输入,而非将其注入到组件的做用域中。
咱们来修改一下 <Header>
组件,使其多接受一个 siteNmae
的prop
, 而后使用 recompose
库中的 defaultProps
高阶组件来包装组件,defaultProps
能够保证在没有传入props
时,使用默认值。
import { defaultProps } from 'recompose';
import { globalConfig } from './config';
export function Header({ children, siteName }) {
const heading = siteName ? <h1>{siteName}</h1> : null;
return (
<div className="header"> {heading} {children} </div>
);
}
export default defaultProps({
siteName: globalConfig.siteName
})(Header);
复制代码
<Header>
变成了一个纯函数组合,再也不直接依赖 globalConfig
变量,让测试变得简单。
同时,当咱们没有设置 siteName
时,defaultProps
会传入 globalConfig.siteName
做为 siteName
属性值。这就是不纯代码被分离和隔离开的地方。
如今让咱们测试纯版本的 <Header>
组件:
import assert from 'assert';
import { shallow } from 'enzyme';
import { Header } from './Header'; // Import the pure Header
describe('<Header />', function () {
it('should render the heading', function () {
const wrapper = shallow(
<Header siteName="Animals in Zoo">Some content</Header>
);
assert(wrapper.contains(<h1>Animals in Zoo</h1>));
});
it('should not render the heading', function () {
const wrapper = shallow(
<Header siteName={null}>Some content</Header>
);
assert(appWithHeading.find('h1').length === 0);
});
});
复制代码
如今好了,测试纯组件 <Header>
很简单。测试作了一件事:验证组件是否呈现给定输入的预期元素。无需导入、访问或修改全局变量,无反作用。设计良好的组件易于测试。
回顾 <WeatherFetch>
组件,当其挂载时,它会发出网络请求去获取天气信息。
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 }) }); } } 复制代码
<WeatherFetch>
是非纯组件,由于相同的输入会产生不一样的输出,由于组件渲染依赖于服务端的返回结果。
不幸的是,HTTP
请求的反作用是没法消除的,<WeatherFetch>
的职责就是从服务端请求数据。
可是你可让 <WeatherFetch>
为相同的属性值渲染相同的内容。这样就能够将反作用隔离到 prop
的函数属性 fetch()
上。这样的一个组件类型被称为几乎纯组件。
咱们来将非纯组件<WeatherFetch>
改写成几乎纯组件。 Redux
能够很好的帮助咱们将反作用的实现细节从组件中提取出来。所以,咱们须要设置一些 Redux
的结构。
fetch()
action creater
启动服务器调用:
export function fetch() {
return {
type: 'FETCH'
};
}
复制代码
redux-saga
拦截了 Fetch action
, 实际想服务端请求,当请求完成时,派发 FETCH_SUCCESS
的 action
import { call, put, takeEvery } from 'redux-saga/effects';
export default function* () {
yield takeEvery('FETCH', function* () {
const response = yield call(axios.get, 'http://weather.com/api');
const { temperature, windSpeed } = response.data.current;
yield put({
type: 'FETCH_SUCCESS',
temperature,
windSpeed
});
});
}
复制代码
这个 reducer
负责更新应用的状态。
const initialState = { temperature: 'N/A', windSpeed: 'N/A' };
export default function (state = initialState, action) {
switch (action.type) {
case 'FETCH_SUCCESS':
return {
...state,
temperature: action.temperature,
windSpeed: action.windSpeed
};
default:
return state;
}
}
复制代码
ps: 为了简单起见,省略了 Redux store
和 sagas
的初始化。
尽管使用 Redux
须要额外的结构,例如: actions
,reducers
和 sagas
,可是它有助于使得 <WeatherFetch>
成为几乎纯组件。
咱们来修改一下 <WeatherFetch>
,使其和 Redux
结合起来。
import { connect } from 'react-redux';
import { fetch } from './action';
export class WeatherFetch extends Component {
render() {
const { temperature, windSpeed } = this.props;
return (
<WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { this.props.fetch(); } } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default connect(mapStateToProps, { fetch }); 复制代码
connect(mapStateToProps, { fetch })
HOC 包装了 <WeatherFetch>
.
当组件挂载时,action creator
this.props.fetch()
被调用,触发服务端请求,当请求完成时, Redux
更新应用的 state
,使得 <WeatherFetch>
从 props
中接收 temperature
和 windSpeed
。
this.props.fetch
是为了隔离产生反作用的非纯代码。由于 Redux
的存在,组件内部再也不须要使用 axois
库,请求 URL
或者是处理 promise
。此外,新版本的 <WeatherFetch>
会为相同的props
值渲染相同的元素。这个组件变成了几乎纯组件。
与非纯版本相比,测试几乎纯版本 <WeatherFetch>
更加容易:
import assert from 'assert';
import { shallow, mount } from 'enzyme';
import { spy } from 'sinon';
// Import the almost-pure version WeatherFetch
import { WeatherFetch } from './WeatherFetch';
import WeatherInfo from './WeatherInfo';
describe('<WeatherFetch />', function () {
it('should render the weather info', function () {
function noop() { }
const wrapper = shallow(
<WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
);
assert(wrapper.contains(
<WeatherInfo temperature="30" windSpeed="10" />
));
});
it('should fetch weather when mounted', function () {
const fetchSpy = spy();
const wrapper = mount(
<WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy} />
);
assert(fetchSpy.calledOnce);
});
});
复制代码
你须要检查,给定的 prop
值,<WeatherFetch>
的渲染结果是否与预期一致,并在挂载时调用 fetch()
。简单且明了。
实际上,在这一步,你不在须要分离不纯的代码,几乎纯组件具备良好的可预测性,而且易于测试。
可是...咱们一块儿来看看兔子洞究竟有多深。几乎纯版本的 <WeatherFetch>
组件能够被转换成一个理想的纯组件。
咱们来将 fethc
回调提取到 recompose
库的 lifecycle()
高阶组件中。
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetch } from './action';
export function WeatherFetch({ temperature, windSpeed }) {
return (
<WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default compose( connect(mapStateToProps, { fetch }), lifecycle({ componentDidMount() { this.props.fetch(); } }) )(WeatherFetch); 复制代码
lifecycle()
高阶组件接受一个有生命周期方法的对象。 调用 this.props.fecth()
方法的 componentDidMount()
由高阶组件处理,将反作用从 <WeatherFetch>
中提取出来。
如今,<WeatherFetch>
是一个纯组件,它再也不有反作用,而且当输入的属性值 temperature
和 windSpeed
相同时,输出老是相同。
虽然纯版本的 <WeatherFetch>
在可预测性和捡东西方面很好,可是它须要相似 compose()
、lifecycle()
等高阶组件,所以,一般,是否将几乎纯组件转换成纯组件须要咱们去权衡。
最后谢谢各位小伙伴愿意花费宝贵的时间阅读本文,若是本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的确定是我前进的最大动力。github.com/YvetteLau/B…
关注公众号,加入技术交流群