Перевод: Лю Сяоси
Оригинальная ссылка:Рисовое путешествие avlutin.com/7-architect…
длина оригиналаОченьОно длинное, но содержание слишком привлекательно для меня, поэтому я не могу не перевести его. Эта статья очень полезна для написания повторно используемых и поддерживаемых компонентов React. Но поскольку длина слишком велика, я разделил статью, эта статья посвященаЧистые и почти чистые компоненты. Из-за ограниченного уровня некоторые переводы в тексте могут быть недостаточно точными, если у вас есть идеи получше, укажите их в комментариях.
Более качественные изделия можно штамповать: GitHub.com/Иветт Л.А. Ю/Б…
——————————————— Я — разделительная линия ————————————————
Чистые и почти чистые компоненты
Чистые компоненты всегда отображают один и тот же элемент для одного и того же значения свойства. Почти чистые компоненты всегда отображают один и тот же элемент для одного и того же значения свойства, но имеют побочные эффекты.
В функциональном программировании чистая функция всегда возвращает одни и те же выходные данные при одних и тех же входных данных и не оказывает побочных эффектов на внешний мир.
function sum(a, b) {
return a + b;
}
sum(5, 10); // => 15
Для заданных двух чиселsum()
Функция всегда будет возвращать один и тот же результат.
Когда функция имеет одинаковые входные данные и разные выходные данные, это не чистая функция. Когда функция зависит от глобального состояния, это не чистая функция, например:
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
.
Даже если одинаковые входные параметры, являются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;
}
}
пс: опущено для простоты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
value отображает тот же элемент. Этот компонент становится почти чистым компонентом.
Протестируйте почти чистую версию по сравнению с нечистой версией<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>
Компонент может быть преобразован в идеальный чистый компонент.
мы придемfetch
Обратный вызов извлечен в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()
И другие компоненты высшего порядка, поэтому, в целом, следует ли преобразовать практически чистые компоненты в чистые компоненты для нас.
Наконец, спасибо за вашу готовность потратить свое драгоценное время на чтение этой статьи. Если эта статья дала вам небольшую помощь или вдохновение, пожалуйста, не скупитесь на лайки и звезды. двигаться вперед.GitHub.com/Иветт Л.А. Ю/Б…
Подпишитесь на официальный аккаунт и присоединитесь к группе технического обмена