представлять
После прочтения Учителя Ченг Мо«Введение в React и Redux»Наконец, исходя из собственного понимания, создайте приложение, отображающее погоду для подведения итогов.
воплощать в жизнь:
npm insatll
npm start
содержание
- Реагировать на новый фронтенд образ мышления
- Основы реакции
- Напишите экземпляр React
- От Flux к Redux
- промежуточное ПО
Новое фронтенд-мышление React
1.1 create-react-app
В этой главе впервые представлены инструментыcreate-react-app, с помощью этого инструмента мы можем быстро создать среду приложения для реагирования.
Первый — установить:
npm install --global create-react-app
После завершения установки выполните:
create-react-app weather_app
На данный момент в текущем каталоге создано реагирующее приложение. Введите weather_app и выполнитеnpm start
Вы можете запустить приложение (версия, которую я установил здесь, v1.3.0)
Давайте посмотрим на структуру каталогов приложения:
|
|--node_modules/
| |--...
|
|--public/
| |--...
|
|--src/
| |--...
|
|--package.json
|
|...
скопировать код
вnode_modulesзависеть от,publicнекоторые статические ресурсы. существуетsrcизindex.jsЕсть:
ReactDOM.render(<App />, document.getElementById('root'));
То есть после запуска программы онаpublic/index.htmlУзел с идентификатором root отображается какsrc/App.jsКомпонент, определенный в App.
Затем, изменив или заменив App.js, мы можем запустить наши собственные определенные компоненты.
1.2 JSX
JSX — это расширение JS, которое может писать HTML в JS. Добавим в нашу программу компонент приложения Weather_App и выведем его на главную страницу:
// Weather_App.js
import React, { Component } from 'react';
class Weather_App extends Component {
render() {
return (
<div className="weather-app">
<div>Hello world</div>
</div>
);
}
}
export default Weather_App;
Измените index.js и добавьте его на главную страницу:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Weather_App from './Weather_App';
ReactDOM.render(<Weather_App />, document.getElementById('root'));
Лично я считаю, что в JSX есть два самых важных места:
- В операторе return возвращаемый узел DOM может иметь только один корневой узел, то есть не может быть двух узлов верхнего уровня (иначе будет сообщено об ошибке):
// 错误示例
return (
<div>Node 1</div>
<div>Node 2</div>
)
- Остерегайтесь конфликтов ключевых слов в JS и HTML. Возьмите Weather_App в качестве примера, мы не можем написать в нем class=xxx, но должны заменить class на className PS: я только что попробовал и могу использовать class напрямую взамен. . номер версии:
"dependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2"
},
скопировать код
Потом я поискал в интернете и сказал, что React16 позволяет DOM передавать атрибуты, так что с этой операцией все в порядке.Однако официалы этого делать не рекомендуют, при открытии консоли видно, что предупреждение все равно выскакивает.
1.3 Другое
Кроме того, в книге рассказывается о преимуществах того, как работает React — мышление функционального программирования:
UI = render(data)
скопировать код
Другими словами, разработчик может сосредоточиться на обработке источника данных и позволить React обрабатывать детали рендеринга. Это будет постепенно ощущаться в следующих нескольких главах, поэтому я не буду говорить об этом здесь.
Прилагается к пониманию чистой функции:
При фиксированном параметре функции параметр не будет изменен после выполнения функции, а когда входные параметры одинаковы, выходные данные всегда будут одинаковыми.
Например, эти две не удовлетворяют чистым функциям:
// 改变了输入
(a) => {
a += 1;
}
// 输出不固定
() => {
return Math.random();
}
ВикипедияУпоминается и еще один момент:
Функция не должна иметь семантически наблюдаемых побочных эффектов функции, таких как «запуск события», выполнение вывода устройства вывода или изменение содержимого чего-либо, отличного от выходного значения, и т. д.Насколько я понимаю, эта функция не может изменять другие вещи (например, глобальные переменные), то есть она отвечает только за вывод.
Основы реакции
опора и состояние
Данные в компоненте React делятся на два типа - prop и state.Изменения в этих двух типах данных могут вызвать рендеринг компонента (только возможный, не обязательно измененный).
Основное различие между prop и state
Основное различие между prop и state заключается в следующем:
- Prop передается извне и не может быть изменен компонентом
- состояние используется для записи состояния внутри компонента, поэтому компонент может быть изменен
Например, нашему приложению нужен компонент для отображения температуры определенного места, компонент принимает параметр указанного адреса и вызывает интерфейс по адресу для получения локальной температуры.
тогда вот адресlocationреквизит, температураtempatureявляется государством.
Мы создаем два новых компонента — WeatherSelector (раскрывающееся окно, выберите адрес) и WeatherPanel (панель отображения, отображающая выбранное местоположение и его температуру), и внедряем эти два компонента в WeatherApp.
Во-первых, это WeatherPanel:
// WeatherPanel
import React, { Component } from 'react';
class WeatherPanel extends Component {
constructor(props) {
super(props)
this.state = {
temperature: 'NA'
}
this.getTemperature = this.getTemperature.bind(this);
}
getTemperature() {
const mockTemperature = Math.random() * 100;
this.setState({
temperature: mockTemperature
})
}
render() {
const {location} = this.props;
return (
<div className="weather-panel">
<div>{location}的温度是: {this.state.temperature}</div>
<button onClick = {this.getTemperature}>Get Temperature</button>
</div>
);
}
}
export default WeatherPanel;
Мы имитируем процесс получения температуры, нажимая getTemperature, Вот на что следует обратить внимание:
- bind — Яма этого в JS, методы, определенные в компоненте, должны указать это через функцию привязки (конечно, можно использовать и стрелочные функции)
- setState — состояние может быть назначено напрямую только при инициализации конструктора, а в других местах необходимо управлять через setState. Компонент будет обновляться только после выполнения этого метода.
- porps - использовать свойства, переданные извне через this.props.NAME (свойства нельзя изменить)
- Используйте переменные в возврате рендеринга JSX - через фигурные скобки{}Цитировать
Далее идет WeatherSelector, и местоположение адреса также можно передать в качестве реквизита:
// WeatherSelecter
import React, { Component } from 'react';
class WeatherSelecter extends Component {
constructor(props) {
super(props)
}
render() {
const {locationGroup} = this.props;
return (
<div className="weather-selecter">
<select>
{
this.props.locationGroup.map((locationObj) => {
return <option key={locationObj.key}>{locationObj.name}</option>
})
}</select>
</div>
);
}
}
export default WeatherSelecter;
Здесь locationGroup — это массив объектов, отображаемых картой. В option есть ключевой атрибут, который в основном предназначен для оптимизации производительности.В последующих главах будет упомянуто, что WeatherApp изменен следующим образом:
import React, { Component } from 'react';
import WeatherSelecter from './WeatherSelecter'
import WeatherPanel from './WeatherPanel'
import {arrLocation as LocationGroup} from './WeatherLocationGroup'
class WeatherApp extends Component {
render() {
return (
<div className="weather-app">
<WeatherSelecter locationGroup={LocationGroup} />
<WeatherPanel location='undefined' />
</div>
);
}
}
export default WeatherApp;
Здесь есть две проблемы:
- Как обнаружить входящие реквизиты — то есть, как компонент ожидает, что тип выбранного свойства будет передан, и как компонент обрабатывает, когда требуемое свойство не передается в
- Как общаться между компонентами — как значение, выбранное в селекторе, передается на панель
Для проблемы 1 ее можно решить, определив PropTypes. Возьмем в качестве примера WeatherPanel ожидаемое входное свойство — строку с именем location, которую можно записать следующим образом:
// WeatherPanel
...
import PropTypes from 'prop-types';
...
WeatherPanel.propTypes = {
location: PropTypes.string.isRequired
}
PS: React.proptype устарел в Reactv15.5., если вы используете его, вам нужно выполнитьnpm install --save prop-typesУстановить зависимости вручную
После их добавления будут проверены реквизиты - если локация не существует или ее тип не строковый, программа сразу сообщит об ошибке
В книге рекомендуется выносить обнаружение реквизита в среду разработки, а не делать этого при релизе кода (ведь количество кода увеличивается, но для пользователей это бессмысленно)
Что касается связи между компонентами, это временно осуществляется через родительский компонент.
родительский компонентWeatherAppКWeatherSelecterПередайте функцию обратного вызова, чтобы уведомить родительский компонент об изменении содержимого селектора, тем самым повторно отрисовав:
// WeatherApp.js
...
// 新增方法locationUpdae
locationUpdate(locationName) {
this.setState({
selectedLocation: locationName
})
}
...
// 传给WeatherSelecter
<WeatherSelecter locationGroup={LocationGroup} locationUpdate={this.locationUpdate}/>
WeatherSelecter:
// WeatherSelecter.js
...
// 修改render的内容, select的value改变时调用WeatherApp的setState更新整个WeatherApp
// WeatherSelecter里加个onChange方法再在constructor写遍bind太麻烦了,这里直接使用箭头函数
render() {
const {locationGroup, locationUpdate} = this.props;
return (
<div className="weather-selecter">
<select onChange={(event) => {locationUpdate(event.target.value)}}>
{
this.props.locationGroup.map((locationObj) => {
return <option key={locationObj.id} value={locationObj.name}>{locationObj.name}</option>
})
}</select>
</div>
);
}
Изображение эффекта:
PS: Синхронизировать данные с несколькими компонентами очень хлопотно, в будущем будет удобнее изучать Flux/Redux.
жизненный цикл компонента
Компонент может пройти через три процесса в своем жизненном цикле:
- Mount (Монтирование), то есть процесс рендеринга компонента в DOM-дереве в первый раз
- Обновление, процесс повторного рендеринга компонентов
- Размонтировать (Unmount), то есть процесс удаления компонента из DOM-дерева
нагрузка
Процесс загрузки компонента будет проходить через следующие этапы: конструктор -> getInitialState -> getDefaultProps -> componentWillMount -> render -> componentDidMount
Конструктор — это конструктор класса компонента, в котором мы завершаем инициализацию компонента (устанавливаем состояние и привязываем эту среду функции-члена через привязку)
Значения getInitialState и getDefaultProps вступают в силу в React.createClass, но Facebook постепенно отказался от этого метода.
renderЭто самая важная функция во всем компоненте React, и компонент отображается через возвращаемое значение этой функции.Функция рендеринга не выполняет фактическое действие рендеринга, она просто возвращает деструктуризацию, описанную JSX, и, наконец, React управляет процессом рендеринга.
Вызовы componentWillMount и componentDidMount происходят до и после рендера соответственно, здесь нужно обратить внимание на функцию componentDidMount.
Распечатайте процесс жизненного цикла в классах WeatherSelector и WeatherPanel соответственно:
...
componentWillMount() {
console.log('component WeatherXXX WillMount')
}
componentDidMount() {
console.log('component WeatherXXX DidMount')
}
render(){
console.log('component WeatherXXX render')
}
Эффект печати показан на рисунке:
Почему компонентDidMount двух компонентов выполняется в конце одинаково?
В жизненном цикле компонента при вызове componentDidMount компонент должен быть отрендерен
Вызов функции рендеринга просто возвращает структурное описание компонента и немедленно его визуализирует.
Конкретное время рендеринга определяется React, и библиотека React может решить, как рендерить, только после получения рендеринга всех компонентов.
Благодаря этому мы также знаем, что когда необходимо управлять DOM компонента, такие операции необходимо помещать на этап componentDidMount (например, использование jQuery для выбора идентификатора компонента и т. д.).
возобновить
Процесс обновления компонента проходит следующие этапы: componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
Самая важная функция здесь — shouldComponentUpdate.
Когда вызывается рендеринг родительского компонента, дочерний компонент, содержащийся в рендере, запускает процесс обновления, но с точки зрения повышения производительности дочерний компонент не обязательно обновлять каждый раз.
shouldComponentUpdate(nextProps,
nextState) возвращает логическое значение. Значение false означает, что компонент не нуждается в обновлении. Мы можем избежать бессмысленных обновлений, изменив эту функцию.
Сначала добавьте кнопку принудительного обновления в WeatherApp.Нажатие кнопки заставит WeatherApp обновиться:
// WeatherApp.js
...
render(){
...
<button onClick= {() => {this.forceUpdate()}}>Force Update</button>
</div>
}
скопировать код
После завершения рендеринга нажмите Force Fresh, и вы увидите в консоли, что и WeatherSelector, и WeatherPanel были повторно визуализированы:
Затем мы модифицируем WeatherPanel.Когда props.location или state.temperature WeatherPanel не меняются, компонент не обновляется:
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.location !== this.props.location) ||
(nextState.temperature !== this.state.temperature);
}
Нажмите «Принудительно обновить» еще раз, и вы увидите, что только WeatherSelector выполняет рендеринг, а WeatherPanel не рендерится повторно:
После обновления компонента его DOM перерисовывается.Если вам нужно выполнить какие-то операции с DOM над компонентом после перерисовки, вы можете сделать это в componentDidUpdate
Напишите экземпляр React
Книга представляет Flux/Redux из третьей главы.В этой главе в этой статье предполагается использовать React для создания приложения прогноза погоды, а затем преобразовать его в Redux в следующих главах.
Изображение эффекта:
Git Log: 81005f12b7244a98f50314aa36d8028ce245c11f
3.1 Реализация
В этом приложении каждый компонент React разбит на части, как показано ниже:
То есть здесь приложение разбито так:
WeatherApp = WeatherHeader + WeatherPanel
= (<div>{header}</div> + WeatherLocationSelecter) + (WeatherSelectedStatus + WeatherCalenderSelecter)
скопировать код
Итак, какие функции должен выполнять каждый компонент? По компонентам:
- Часть WeatherHeader отвечает за выбор города, при переключении города приложение инициирует сетевой запрос для получения информации о погоде соответствующего города.
- WeatherSelectedStatus — это чистый компонент отображения.
- WeatherCalenderSelector отвечает за отображение информации о следующих двух днях.Нажав на кнопку, конкретная информация дня может быть отображена в WeatherSelectedStatus.
получение информации
WeatherHeader отвечает за выбор города и получение информации о погоде, как уведомить приложение об обновлении после того, как компонент успешно получит информацию о погоде? Здесь добавьте функцию обратного вызова для обновления погоды в WeatherApp, а затем передайте функцию в WeatherHeader в качестве реквизита. После того, как WeatherLocationSelector в WeatherHeader успешно получает информацию, вызывается функция обратного вызова, чтобы уведомить систему о перерисовке:
//WeatherApp.js
...
locationIdUpdate(locationId, dailyInfo) {
this.setState({
daily: dailyInfo,
selectedLocationId: locationId
})
...
Участок WeatherSelectedStatus
Это очень просто, WeatherSelectedStatus — компонент-дурачок, полученный тип данных предустановлен, и рендеринг завершен. Компоненты-пустышки: Компоненты в React, которые фокусируются на рисовании, называются компонентами-пустышками.
Взаимодействие WeatherCalenderSelector и WeatherLocationSelectorStatus
В этом приложении прогноза погоды мы получаем информацию о погоде на следующие три дня в указанном городе через WeatherLocationSelector и вызываем функцию обратного вызова, переданную ей WeatherApp, для перерисовки приложения. Структура данных, полученная WeatherLocationSelecte, на самом деле представляет собой массив объектов:
[{
// dailyInfo of day1
...
}, {
// dailyInfo of day2
...
}, {
// dailyInfo of day3
...
}]
WeatherSelectedStatus принимает первый объект массива и рисует погоду на день
WeatherCalenderSelector принимает весь массив и рисует информацию о будущей погоде.
чтобы достичьВыберите информацию за день в WeatherCalenderSelector и переключите содержимое, отображаемое в WeatherSelectedStatus.На самом деле цель состоит в том, чтобы переключить объект погоды, полученный в WeatherSelectedStatus, щелкнув каждый элемент в CalenderSelector.
Поэтому принимается решение, аналогичное тому, что было в 1, добавление функции обратного вызова в WeatherPanel и передача ее в WeatherCalenderSelector, а также переключение содержимого, переданного в WeatherSelectedStatus WeatherPanel, при щелчке различных элементов.
В заключение прилагается диаграмма анализа:
Примечание. Здесь используется APIЗнай погодуПредусмотрен погодный интерфейс
3.2 Оптимизация производительности компонентов
В книге упоминается в конце главы 4, что с помощью плагиновReact PrefОбнаруживает проблемы с производительностью при рендеринге компонентов React.
В официальной документации React указано, что плагин react-addons-perf устарел после React 16 и может быть напрямую проанализирован с помощью инструмента анализа производительности, который поставляется с браузером.
Chrome -> F12 -> Performance -> User Timing
Здесь вы можете напрямую увидеть события React, а на картинке ниже показан скриншот кнопки выбора первого свидания, которую я нажимал несколько раз:
Вы можете видеть, что WeatherSelectedStatus проходит процесс обновления каждый раз, когда вы нажимаете
Как упоминалось в главе 2, жизненный цикл компонентов React делится на три этапа: Monut -> Update -> Unmount, а процесс обновления можно разделить на:
componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
Изменяя shouldComponentUpdate, можно избежать бессмысленных обновлений компонентов, чтобы достичь цели повышения производительности. Измените правило обновления компонента, чтобы изменить WeatherSelectedStatus на обновление только при изменении currentDayInfo:
// WeatherSelectedStatus
shouldComponentUpdate(nextProps) {
return JSON.stringify(nextProps.currentDayInfo) !== JSON.stringify(this.props.currentDayInfo)
}
Нажмите кнопку выбора первой даты еще несколько раз и обнаружите, что на этот раз WeatherSelectedStatus останавливается только после вызова shouldComponentUpdate:
Затем, когда мы пытаемся несколько раз нажать кнопку «Пекин», мы видим, что весь компонент отрисовывается повторно.Простейший метод оптимизации здесь — напрямую избегать бессмысленных сетевых запросов = =:
// WeatherLocationSelecter
...
// 1. 在构造函数里加一个state,用于记录上次选中的id
...
constructor(props) {
...
this.state = {
currentSelectedId: undefined
}
...
}
...
// 2. 发起网络请求前,比较想要请求的id是否与上次记录的id一致
...
locationIdUpdate(locationId) {
if(this.state.currentSelectedId === locationId) {
return
}
...
}
...
Кроме того, чтобы в наибольшей степени избежать повторных запросов полученных данных за короткий промежуток времени, данные также могут кэшироваться в браузере после первого запроса, а также добавляться время истечения.
3.3 Организация кода проекта
Текущая схема организации кода выглядит следующим образом, вы можете видеть, что в src закинута куча файлов:
Файлы могут быть организованы по роли или функции
- Организация по ролям (инфраструктура MVC)
|
|--controllers/
| |--...
|
|--models/
| |--...
|
|--views/
| |--...
|
|...
скопировать код
- Организуйте по функциям
В книге упоминается, что в React это более применимый способ организации. Здесь мы помещаем каждый функциональный модуль в соответствующую файловую комнату и добавляем соответствующий индексный файл в каждую папку, чтобы экспортировать файл этого модуля для вызова других модулей.
Возьмем для примера WeatherSelectedStatus, создадим новую папку WeatherSelectedStatus, поместим в нее x.js и x.css и добавим index.js:
import view from './WeatherSelectedStatus'
export {view}
Там, где нам нужно использовать WeatherSelectedStatus, мы можем получить его следующими способами:
import {view as WeatherSelectedStatus} from './WeatherSelectedStatus'
Преимущество этого заключается в том, что независимо от того, как изменяется WeatherSelectedStatus, интерфейс, отображаемый через индекс, не изменится, и описанный выше метод импорта может быть установлен непосредственно при его использовании.
Окончательная структура файла показана на рисунке:
От Flux к Redux
Есть две проблемы с предыдущим решением:
- Источники данных могут быть неоднородными
- Слишком много уровней передачи функции ловушки
Что делать, если два компонента зависят от одного и того же состояния данных? Поддерживают ли каждый из этих двух компонентов состояние данных? Или перекачать состояние на предыдущий компонент?
Еще раз взгляните на разнесенную диаграмму компонентов из предыдущей главы:
на картинке вышеWeatherSelectedStatusа такжеWeatherCalenderSelecter, оба компонента должны отображать информацию о погоде.
Если два компонента поддерживают свой собственный статус информации о погоде, то есть при нажатии кнопки переключения города, пусть оба компонента отправляют сетевой запрос для обновления статуса погоды.
Это явно неуместно.Помимо низкой эффективности, когда компонент запрашивает ошибку, что приводит к несогласованности данных между двумя компонентами, какой из них должен преобладать?
Следовательно, лучший способ — абстрагировать информацию о погоде в верхний компонент. Здесь мы получили унифицированный источник данных, используя ежедневную информацию о погоде в качестве компонента верхнего уровня.WeatherAppсостояние.
Почему бы не разместить информацию о погоде здесь?WeatherPanel?
Рассмотрим такую ситуацию: когда мы нажимаемWeatherHeaderКак передать новую информацию о погоде, полученную при нажатии кнопки обновления информации о погоде вWeatherPanel
Поэтому нам нужно поместить информацию о погоде вWeatherAppсередина.
Но это приводит к другой проблеме: передаче нескольких уровней функций ловушек.
После того, как WeatherLocationSelector успешно получит информацию о погоде, ему необходимо вызвать функцию ловушки, переданную WeatherApp.locationIdUpdate, функция этой функции состоит в том, чтобы обновлять selectedId и ежедневно в WeatherApp
Однако WeatherHeader не нуждается в этой функции, единственная цель передачи ему функции - передать эту функцию дочернему компоненту WeatherLocationSelector
4.1 Flux
Структура фрейма Flux показана на рисунке:
В среде Flux данные хранятся в хранилище, а изменения данных инициируются действиями.
Когда диспетчер получает отправленное действие, если действие является зарегистрированным типом, он обрабатывает его.После завершения обработки он отправит уведомление, чтобы уведомить различные компоненты, которые отслеживают действие, чтобы выполнить свои собственные функции обратного вызова.
Тогда давайте посмотрим, как использовать Flux, чтобы избежать вышеуказанных проблем.
Чтобы использовать Flux, нам нужны Dispatcher, Action и Store.
- Сначала определите Dispatcher для получения и обработки информации, отправленной другими компонентами:
import {Dispatcher} from 'flux'
export default new Dispatcher()
- Определите действие, которое является типом информации, отправляемой компонентом:
// ActionTypes
export const UPDATELOCATION = 'updateLocation'
// Actions
import * as ActionTypes from './ActionTypes'
import AppDispatcher from '../AppDispatcher'
export const updateLocation = (locationId) => {
AppDispatcher.dispatch({
type: ActionTypes.UPDATELOCATION,
locationId: locationId
})
}
Здесь обычно используются два js, один используется для хранения типа информации (ActionTypes), а другой используется для определения конструктора действия, то есть информации, отправляемой через функцию (Actions)
Причина этого в том, что магазин будет работать по-разному для разных типов действий, и действия необходимо импортировать отдельно.
3. Определите WeatherStore для хранения информации о погоде:
let locationId = undefined
let daliyInfo = {}
const WeatherStore = Object.assign({}, EventEmitter.prototype, {
getDailyInfo: function() {
return daliyInfo
},
emitChange: function() {
this.emit(CHANGE_EVENT)
},
addChangeListener: function(cb) {
this.on(CHANGE_EVENT, cb)
},
removeChangeListener: function(cb) {
this.removeListener(CHANGE_EVENT, cb)
}
})
Здесь пусть WeatherStore наследует метод EventEmitter, Store получаетaction: UPDATELOCTION, обновите информацию о погоде dailyInfo и вызовите emitChange после завершения обновления, чтобы уведомить все зарегистрированные в нем компоненты:
AppDispatcher.register((action) => {
if(action.type === ActionTypes.UPDATELOCATION) {
if(daliyInfo.locationId === action.locationId) {
return
}
daliyInfo.locationId = action.locationId
daliyInfo.daily = "Getting data ..."
WeatherStore.emitChange()
let requestCode = undefined
LocationGroup.forEach((val) => {
if(val.id === daliyInfo.locationId) {
requestCode = val.code
}
})
const requestURL = `/v3/weather/daily.json?key=${CustomConfig.key}&location=${requestCode}&language=zh-Hans&unit=c&start=0&days=3`
fetch(requestURL)
.then((response) => {
if(response.status !== 200) {
daliyInfo.daily = "Getting data Failed!"
throw new Error('Fail to get response with status ' + response.status)
}
response.json().then((responseJSON) => {
daliyInfo.daily = responseJSON.results[0].daily
WeatherStore.emitChange()
})
}).catch((error) => {
daliyInfo.daily = "Getting data Failed!"
WeatherStore.emitChange()
})
}
})
- Измените компонент, который должен прослушивать сообщения
Вот пример WeatherPanel:
// WeatherPanel
...
onChange() {
this.setState({
dailyInfo: WeatherStore.getDailyInfo().daily
})
}
componentDidMount() {
WeatherStore.addChangeListener(this.onChange)
}
...
После того, как WeatherPanel смонтирована, объявите о прослушивании уведомлений из WeatherStore. Когда dailyInfo WeatherStore обновится, вызовите emitChange, и onChange в WeatherPanel будет вызываться для обновления dailyInfo в WeatherPanel. Аналогично WeatherHeader, добавьте onChange в компонент и зарегистрируйте его в WeatherStore:
import React, { Component } from 'react'
import {view as WeatherLocationSelecter} from '../WeatherLocationSelecter'
import {arrLocation as LocationGroup} from '../utils'
import WeatherStore from '../WeatherStore'
import './WeatherHeader.css'
class WeatherHeader extends Component {
constructor(props) {
super(props)
this.state = {
selectedId: undefined
}
this.onChange = this.onChange.bind(this)
}
onChange() {
this.setState({
selectedId: WeatherStore.getDailyInfo().locationId
})
}
componentDidMount() {
WeatherStore.addChangeListener(this.onChange)
}
componentWillUnmount() {
WeatherStore.removeChangeListener(this.onChange)
}
render() {
const selectedId = this.state.selectedId;
let title = undefined
LocationGroup.forEach((val) => {
if(val.id === selectedId) {
title = val.name;
}
})
return (
<div className="weather-header">
<div className="weather-title">{title}</div>
<WeatherLocationSelecter/>
</div>
);
}
}
export default WeatherHeader;
Видно, что приложение, модифицированное во фреймворк flux, достигает цели единого источника данных (все данные хранятся в хранилище, а обновления данных передаются через действия), а также избегает передачи бессмысленных хуков-функций (обновление хранилища После данных, пусть каждый компонент вызывает свою собственную функцию onChange для получения данных из хранилища)
4.2 Экземпляр Redux
Redux — это реализация Flux, и здесь нужно отметить два основных момента:
- Redux имеет только одно хранилище — в примере реализации Flux в предыдущем разделе, хотя мы использовали только одно хранилище, во Flux приложение может иметь несколько хранилищ; Redux предусматривает, что приложение может иметь только одно хранилище.
- Редуктор - изменения данных выполняются чистой функцией Редуктор
Давайте рассмотримуменьшить функцию:
let a = [1, 2, 3, 4].reduce(function reducer(sum, item) {
return sum + item
}, 0)
console.log(a) // 10
Здесь массив работает со всеми элементами в соответствии с входящей функцией редуктора, где сумма — это результат предыдущей операции, а элемент — объект этой операции. Применительно к Redux есть:
redcucer(state, action)
То есть состояние генерируется в соответствии с действием и состоянием, а результат полностью определяется этими двумя параметрами.
редьюсеры — это чистые функции,Состояние двух параметров и действие не должны изменяться.
Просмотрите предыдущее поведение в flux-WeatherStore:
Эта операция напрямую меняет значение WeatherStore, что не разрешено в Redux, в Redux мы должны возвращать новое состояние напрямую через редюсер.
Затем мы меняем предыдущий экземпляр Flux на экземпляр Redux:
- npm install --save redux
- Измените действие, которое больше не отправляется через Dispather, а напрямую возвращает объект действия.
import * as ActionTypes from './ActionTypes'
import AppDispatcher from '../AppDispatcher'
export const updateLocation = (locationId) => {
return {
type: ActionTypes.UPDATELOCATION,
locationId: locationId
}
}
- Создайте хранилище и редьюсер, где хранилище используется для хранения данных, а редьюсер используется для определения того, как данные обрабатываются:
// store
import {createStore} from 'redux'
import reducer from './Reducer.js'
const initValues = {
daily: undefined,
locationId: 0
}
const store = createStore(reducer, initValues)
export default store
// reducer.js
...
export default (state, action) => {
switch(action.type) {
case ActionTypes.UPDATELOCATION:
let responseJSON = {...}
return {...state, daily: responseJSON.results[0].daily, locationId: action.locationId}
default: return state
}
}
В редьюсере наш метод обработки — это временно синхронная обработка, то есть после получения действия возвращается состояние со случайными погодными условиями вместо асинхронной выдачи сетевых запросов на получение погодных условий.
Асинхронный запрос здесь не выполняется, потому что редьюсер, как чистая функция, непосредственно выполняет запрос и возвращает состояние синхронно после получения запроса, что вызывает отрисовку других компонентов, и не дает возможности для асинхронной работы.
Асинхронными запросами необходимо управлять через промежуточное ПО. Конкретный метод будет представлен в следующей главе.Обратите внимание, что редюсер напрямую возвращает объект состояния вместо изменения состояния входящего параметра.
4. Измените представление
Взяв WeatherPanel в качестве примера, мы можем получить состояние магазина через функцию getState магазина:
...
getOwnState() {
return {
selectedCalender: 0,
dailyInfo: store.getState().daily
}
}
...
Зарегистрировать функцию обратного вызова в магазине
...
componentDidMount() {
store.subscribe(this.onChange)
}
componentWillUnmount() {
store.unsubscribe(this.onChange)
}
...
- Измените метод locationIdUpdate в WeatherLocationSelector, и магазин выполнит действие при нажатии на адрес:
locationIdUpdate(locationId) {
store.dispatch(Actions.updateLocation(locationId))
}
Поскольку существует только одно хранилище, APPDispatcher не нужен, и его можно удалить напрямую. Метод отправки объединяется с хранилищем, а обработка действий определяется редьюсером.
4.3 Компоненты контейнера и фиктивные компоненты
В среде Redux компоненты React в основном отвечают за две функции:
- Взаимодействуйте с Store, читайте данные магазина и отправляйте действия
- Интерфейс рендерится по пропсам и состояниям, поэтому компоненты можно разделить, причем родительский компонент отвечает за деловые отношения с магазином (компонент-контейнер), а дочерний компонент отвечает за рендеринг (компонент-дурак)
Взяв за пример наше приложение, логику отправки действия по обновлению информации о погоде в WeatherLoactionSelect можно извлечь в его родительский компонент WeatherHeader.
При нажатии кнопки в WeatherLocationSeleceter вызывается родительский компонент для отправки действия через свойства, чтобы компонент LocationSeleceter мог сосредоточиться на рендеринге интерфейса:
import React, { Component } from 'react';
class WeatherLocationSelecter extends Component {
render() {
const {LocationGroup, locationIdUpdate, selectedId} = this.props
return (
<div className="weather-selecter">
{
LocationGroup.map((locationObj) => {
return <button className={locationObj.id === selectedId ? 'selected' : ''} key={locationObj.id} onClick={() => locationIdUpdate(locationObj.id)}>{locationObj.name}</button>
})
}
</div>
);
}
}
export default WeatherLocationSelecter;
Когда я писал это, я обнаружил, что в предыдущем письме были некоторые неточности...
- У WeatherPanel еще есть состояние — selectedCalender, которое надо хранить в магазине и обновлять календарь как действие:
// store
import {createStore} from 'redux'
import reducer from './Reducer.js'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const store = createStore(reducer, initValues)
export default store
// action
...
export const updateCalender = (calenderId) => {
return {
type: ActionTypes.UPDATECALENDER,
calenderId: calenderId
}
}
// Reducer
...
case ActionTypes.UPDATECALENDER:
return {...state, calenderId: action.calenderId}
...
- Компонент контейнера и компонент дурака я не написал внятно - рендер в WeatherHeader все равно делает логическую обработку отображения заголовка, который можно разделить на компоненты WeatherTitleWrapper + WeatherTitle
CalenderSelector и Selectedstatus в WeatherPanel также могут быть разделены на компонент Wrapper (сосредоточено на сделке с магазином) + UI (сосредоточено на рендеринге)
На данный момент структура нашего приложения показана на рисунке:
Среди них WeatherHeader и WeatherPanel — компоненты-контейнеры, отвечающие за работу с хранилищем; WeatherLocationSelector/WeatherCalender/WeatherSelectedStatus — компоненты-дураки, фокусирующиеся на работе рендеринга пользовательского интерфейса.
Пересмотренная запись отправки кода:
Git log: b8f8e41ed0ee4c1563400ad3032d790c442dfdd8
4.4 Контекст компонента
Компоненты WeatherHeader и WeatherPanel напрямую импортируют хранилище, но во многих случаях мы не можем определить конкретное место хранения хранилища, и этой практики следует избегать.
Однако, если вы импортируете хранилище из верхнего уровня приложения и передаете его дочернему компоненту в качестве реквизита, также будет другая проблема:
Когда компонент, которому нужно хранилище, расположен очень глубоко, нам нужно передать хранилище самому нижнему подкомпоненту через компонент среднего уровня, даже если компоненту среднего уровня хранилище не нужно.
С этой целью React предоставляет функцию Context, которая может предоставить контекстную среду, чтобы все компоненты могли получить доступ к объектам, хранящимся в среде.
Сначала напишите провайдера и позвольте провайдеру предоставить контекст. Провайдер должен предоставитьgetChildContextметод и определить егоchildContextTypes
import {Component} from 'react'
import PropTypes from 'prop-types'
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
}
}
render() {
return this.props.children
}
}
Provider.childContextTypes = {
store: PropTypes.object
}
export default Provider
И заверните Provider в компонент приложения верхнего уровня WeatherApp:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import WeatherApp from './WeatherApp';
import store from './Store.js'
import Provider from './Provider'
ReactDOM.render(
<Provider store={store}>
<WeatherApp />
</Provider>,
document.getElementById('root')
)
Таким образом, когда компоненту нужно использовать хранилище, он может напрямую передатьthis.context.storeспособ получения:
// WeatherHeader
...
class WeatherHeader extends Component {
...
constructor(props, context) {
super(props, context)
...
}
getOwnState() {
return {
selectedId: this.context.store.getState().locationId
}
}
...
}
WeatherHeader.contextTypes = {
store: PropTypes.object
}
export default WeatherHeader;
Здесь следует отметить две вещи:
- Поскольку используется контекст, этот параметр необходимо ввести в конструкторе.
- Как и Provider, contextTypes необходимо определить
4.5 react-redux
react-redux в основном делает две вещи:
- Предоставляется провайдер — просто импортируйте {Provider} непосредственно из «реагировать-редукс».
- В предыдущем коде много компонентов с большим количеством повторяющегося кода (subscribe/onChange/getOwnState), которых можно избежать с помощью соединения react-redux
Основная работа каждого компонента Wrapper включает в себя два пункта: - Получить состояние из хранилища, передать его обернутому фиктивному компоненту
- Инкапсулируйте отправку магазина в хранилище и передайте его обернутому фиктивному компоненту.
Форма использования соединения react-redux:
connect(mapStateToProps, mapDispatchToProps)(UIComponent)
скопировать код
connect принимает две функции в качестве параметров и возвращает функцию, которая принимает в качестве параметра дурацкий компонент, конечным результатом этой функции является класс
Немного запутанно говорить об этом, т.
Функция mapStateToProps заменяет функцию передачи состояния в компоненте-контейнере
mapState(state, ownProps) - state это состояние в хранилище, ownProps это свойства компонента
Функция mapDispatchToProps заменяет функцию передачи диспетчеризации в компоненте-контейнере
mapDispatcher(отправка,
ownProps) — вызов диспетчера (xxx) для отправки действия. Используя соединение, функция компонента контейнера может быть напрямую заменена, и предыдущий избыточный код больше не нужно повторять, что значительно уменьшает объем кода.
Взяв в качестве примера предыдущий WeatherSelectedStatusWrapper + WeatherSelecetedStatus, вы можете напрямую удалить Wrapper и изменить SelectedStatus на:
import React, { Component } from 'react'
import {connect} from 'react-redux'
import './WeatherSelectedStatus.css'
function mapState(state) {
return {
currentDayInfo: state.daily[state.calenderId]
}
}
class WeatherSelectedStatus extends Component {
render() {
const {currentDayInfo} = this.props
const {text_day, code_day, high, low, wind_scale, wind_direction, wind_direction_degree, wind_speed} = currentDayInfo
return (
<div className="selected-status">
<div className="status">{text_day}</div>
<div className="detail">
<div>
<img alt="status-img" src={require('../img/status_icon/' + code_day + '.png')} />
<span>{low} ~ {high}°C</span>
</div>
<div>
<div>风力等级: {wind_scale}</div>
<div>风向角度(0~360): {wind_direction} { wind_direction_degree}</div>
<div>风速(km): {wind_speed}</div>
</div>
</div>
</div>
);
}
}
export default connect(mapState)(WeatherSelectedStatus);
промежуточное ПО
В предыдущей главе после использования redux мы изменили процесс запроса сетевых данных, чтобы возвращать данные локально в режиме реального времени.
Это связано с тем, что Reducer, как чистая функция, должен возвращать состояние сразу после получения действия и не может выполнять асинхронные операции.
Для этого в Redux предусмотрено промежуточное ПО. Так называемое промежуточное ПО — это механизм предварительной обработки действий перед отправкой их в Редьюсер:
Здесь мы можем использовать резервный преобразователь промежуточного программного обеспечения, предоставляемый Redux для асинхронной обработки.
redux-thunk
Как правило, действие должно иметь атрибут Type, который обрабатывается Reducer.
Но когда тип действия — функция, при прохождении через redux-thunk преобразователь перехватит действие и выполнит функцию.
Функция, которую обрабатывает преобразователь, принимает два параметра:
- диспетчеризация: диспетчеризация магазина, когда преобразователь обрабатывается, новое действие может быть отправлено через диспетчеризацию
- getState: вы можете использовать этот метод для получения состояния, хранящегося в хранилище.
Измените Store.js и добавьте промежуточное ПО redux-thunk:
import {createStore, compose, applyMiddleware} from 'redux'
import reducer from './Reducer.js'
import thunkMiddelware from 'redux-thunk'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const middlewares = [thunkMiddelware]
const storeEnhancers = compose(applyMiddleware(...middlewares))
export default createStore(reducer, initValues, storeEnhancers)
Измените содержимое, отправленное в действии, и измените его на функциональную форму:
// 增加用于发送网络请求结果的action
export const fetchDataSuccess = (daily, locationId) => {
return {
type: ActionTypes.FETCHDATASUCCESS,
locationId: locationId,
daily: daily
}
}
export const fetchDataStarted = (locationId) => {
return {
type: ActionTypes.FETCHDATASTARTED,
daily: 'Loading...',
locationId: locationId
}
}
export const fetchDataFailed = (locationId) => {
return {
type: ActionTypes.FETCHDATAFAILED,
daily: 'get data failed!',
locationId: locationId
}
}
// 获取天气信息
export const fetchData = (locationId) => {
return (dispatch, getState) => {
if(getState().locationId === locationId) {
return
}
let requestCode = undefined
LocationGroup.forEach((val) => {
if(val.id === locationId) {
requestCode = val.code
}
})
if(!requestCode) {
dispatch(fetchDataFailed(locationId))
return
}
dispatch(fetchDataStarted(locationId))
const requestURL = `/v3/weather/daily.json?key=${CustomConfig.key}&location=${requestCode}&language=zh-Hans&unit=c&start=0&days=3`
fetch(requestURL)
.then((response) => {
if(response.status !== 200) {
dispatch(fetchDataFailed(locationId))
return
}
response.json().then((responseJSON) => {
dispatch(fetchDataSuccess(responseJSON.results[0].daily, locationId))
}).catch((error) => {
dispatch(fetchDataFailed(locationId))
})
})
}
}
В этот момент, в это время, redux-thunk может зафиксировать действиеActions.fetchData(locationId), и отправить соответствующее действие в процессе получения данных, чтобы уведомить Reducer о возврате соответствующего состояния:
// Reducer.js
import {ActionTypes} from './action'
export default (state, action) => {
switch(action.type) {
case ActionTypes.UPDATECALENDER:
return {...state, calenderId: action.calenderId}
case ActionTypes.FETCHDATASTARTED:
return {...state, daily: action.daily, locationId: action.locationId}
case ActionTypes.FETCHDATASUCCESS:
return {...state, daily: action.daily, locationId: action.locationId}
case ActionTypes.FETCHDATAFAILED:
return {...state, daily: action.daily, locationId: action.locationId}
default:
return state
}
}
пользовательское промежуточное ПО
При настройке промежуточного программного обеспечения вам необходимо сделать это в следующем формате:
function doNothyingMiddleware({dispatch, getState}) {
return function(next) {
return function(action) {
return next(action)
}
}
}
dispath & getState: методы в хранилище для отправки действий и получения состояния
next: функция, выполнить next(action), чтобы передать текущее обрабатываемое действие следующему промежуточному программному обеспечению
действие: объект действия, отправленный в систему
Видно, что функции вложены во многие слои — Redux разработан в соответствии с идеей функционального программирования, Важный момент — сделать функцию каждой функции как можно меньше, а сложные функции реализовать через вложенную комбинацию функции
Исходя из этого, мы можем настроить промежуточное программное обеспечение thunk в предыдущем разделе: когда тип действия является функцией, вызовите функцию, в противном случае обработка не выполняется, и действие передается прямо в обратном направлении.
// customMiddlewares
let customThunkMiddleware = ({dispatch, getState}) => {
return function(next) {
return function(action) {
if(typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
}
}
export {customThunkMiddleware}
Мы также можем определить несколько промежуточных программ для обработки действия, и порядок обработки будет соответствовать порядку, в котором промежуточные программы определены в Store.js. Например, мы определяем промежуточное программное обеспечение, которое печатает конкретную информацию о действии:
// customMiddlewares
...
let customLogMiddleware = ({dispatch, getState}) => {
return (next) => {
return (action) => {
console.log("action type is: " + action.type)
next(action)
}
}
}
export {customThunkMiddleware, customLogMiddleware}
и добавьте его в массив промежуточных программ:
import {createStore, compose, applyMiddleware} from 'redux'
import reducer from './Reducer.js'
import {customThunkMiddleware, customLogMiddleware} from './customThunkMiddleware'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const middlewares = [customThunkMiddleware, customLogMiddleware]
const storeEnhancers = compose(applyMiddleware(...middlewares))
export default createStore(reducer, initValues, storeEnhancers)
После запуска через точку останова вы можете видеть, что каждое действие, отправленное в систему, будет проходить последовательно.customThunkMiddlewareа такжеcustomLogMiddleware