Создание приложений React с нуля (2) — Архитектура приложения React

Архитектура JavaScript React.js Redux
Создание приложений React с нуля (2) — Архитектура приложения React

Предыдущая статья——Создание приложений React с нуля (1) — базовая конструкцияОписывает, как использовать webpack для создания очень простой среды разработки React. В этой статье будет подробно описана архитектура построения приложения React.

Адрес склада:GitHub.com/г-н Чжан123/…

redux

В нашем процессе разработки много раз нам нужно позволить компонентам совместно использовать некоторые данные.Хотя совместное использование данных может быть достигнуто с помощью компонентов, если между компонентами нет отношений родитель-потомок, передача данных очень проблематична, и легко сделать код Читабельность снижена, в это время нам нужен инструмент управления состоянием (state). Общие инструменты управления состоянием включают redux и mobx, здесь Redux выбран для управления состоянием. Стоит отметить, что React 16.3 привносит совершенно новыйContext API, мы также можем использовать новый Context API для управления состоянием.

Redux — это контейнер состояния JavaScript, обеспечивающий предсказуемое управление состоянием. Позволяет создавать согласованные приложения, которые работают в разных средах (клиент, сервер, собственные приложения) и легко тестируются. Мало того, он также обеспечивает очень хороший опыт разработки, например, отладчик путешествий во времени, который может предварительно просмотреть в реальном времени после редактирования.

Поток данных redux показан на следующем рисунке:

Три принципа REDUX:

  1. Все приложениеstateВсе они хранятся в дереве объектов, и это дерево объектов существует только в одном хранилище, но это не означает, что использование избыточности требует, чтобы все состояние сохранялось в избыточности.
  2. состояние только для чтения, единственный способ изменить состояние — запуститьaction,actionэто обычный объект, используемый для описания произошедшего события.
  3. Используйте чистые функции для выполнения модификаций, чтобы описать, как действия изменяют дерево состояний, необходимо написать редюсеры.

Промежуточное ПО (промежуточное ПО Redux)

Redux middleware Предоставляет точку расширения после запуска действия и до достижения редуктора. Действия, инициированные диспетчером, по очереди проходят через промежуточное ПО и, наконец, достигают редуктора. Мы можем использовать промежуточное ПО Redux для ведения журнала, создания отчетов о сбоях, вызова асинхронных интерфейсов или маршрутизации и т. д.По сути, промежуточное ПО просто расширяетstore.dispatchметод.

Улучшитель магазина

Усилитель магазина используется для улучшения функциональности магазина. Усилитель магазина на самом деле представляет собой функцию более высокого порядка, которая возвращает новый расширенный создатель магазина.

const logEnhancer = createStore => (reducer, initialState, enhancer) => {
  const store = createStore(reducer, initialState, enhancer)
  function dispatch(action) {
    console.log(`dispatch an action: ${JSON.stringify(action)}`)
    const res = store.dispatch(action)
    const newState = store.getState()
    console.log(`current state: ${JSON.stringify(newState)}`)
    return res
  }
  return { ...store, dispatch }
}

можно увидетьlogEnhancerИзменено поведение магазина по умолчанию, каждый разdispatchДо и после будет выводиться журнал.

react-redux

Redux сам по себе является государственной библиотекой state JS, которую можно использовать в сочетании с приложениями react, vue, angular и даже с родными JS.Чтобы redux помогал нам управлять состоянием реагирующих приложений, нам нужно связать redux с реакцией. Официально предоставленоreact-reduxбиблиотека.

реакция-редукс обеспечиваетProviderКомпонент внедряет хранилище в приложение через контекст, а затем компонент используетconnectМетод более высокого порядка получает и отслеживает хранилище, затем вычисляет новые реквизиты на основе состояния хранилища и собственных реквизитов компонента, вводит их в компонент и сравнивает рассчитанные новые реквизиты, наблюдая за хранилищем, чтобы определить, нужно ли компоненту быть обновлено.

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)

Интегрировать избыточность в реагирующее приложение

Объединить редукторы

В реагирующем приложении есть только одно хранилище, компоненты передают данные редьюсеру, вызывая функцию действия, редуктор изменяет соответствующее состояние в соответствии с данными. Однако по мере увеличения сложности приложения редюсер будет становиться все больше и больше.В настоящее время вы можете рассмотреть возможность разделения редьюсера на несколько отдельных функций, каждая из которых отвечает за независимое управление частью состояния.

редукс обеспечиваетcombineReducersВспомогательная функция объединяет рассредоточенный редюсер в конечную функцию редуктора, а затем использует ее при вызове CreateStore.

Интеграция промежуточного ПО

Иногда нам нужно объединить несколько промежуточных программ вместе, чтобы сформировать цепочку промежуточных программ для улучшенияstore.dispatch, при создании магазина нам необходимо интегрировать в магазин цепочку промежуточного программного обеспечения, которая официально предоставляетсяapplyMiddleware(...middleware)Связывайте промежуточное ПО вместе.

Интеграция усилителей магазина

Усилитель магазина используется для улучшения магазина. Если нам нужно интегрировать несколько усилителей магазина, когда у нас есть несколько усилителей магазина, мы будем использовать их в это время.compose(...functions).

использоватьcomposeПри объединении нескольких функций каждая функция принимает один параметр, ее возвращаемое значение будет предоставлено в качестве параметра функции слева и т. д. Самая правая функция может принимать несколько параметров.compose(funA,funB,funC)можно понимать какcompose(funA(funB(funC()))), который, наконец, возвращает итоговую функцию после объединения функций, полученных справа налево.

Создать магазин

редукс черезcreateStoreСоздайте магазин REDUX для хранения всех приложенийstate,createStoreПараметры формы следующие:

createStore(reducer, [preloadedState], enhancer)

Итак, наш код для создания магазина выглядит следующим образом:

import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'

import reducers from '../reducers'

const initialState = {}

const store = createStore(reducers, initialState, applyMiddleware(thunk))

export default store

Магазин, который будет создан позже, передаетсяProviderИнтегрируйте Redux с реагирующими приложениями, внедряя компоненты в реагирующие приложения.

Примечание: в приложении должен быть и только один магазин.

React Router

React Router — это комплексное решение для маршрутизации для React, которое поддерживает синхронизацию пользовательского интерфейса с URL-адресом. В проект мы интегрируем последнюю версию React Router v4.

В react-router v4 react-router разделен на три пакета: react-router, react-router-dom и react-router-native, отличия заключаются в следующем:

  • react-router: Предоставляет основные компоненты и функции маршрутизации.
  • react-router-dom: реагирующий маршрутизатор для браузеров
  • react-router-native: реагирующий маршрутизатор для реагирующего нативного

редукс и реактивный маршрутизатор

React Router в основном отлично работает при использовании с Redux, но иногда есть обновления маршрутов, но нет обновлений дочерних маршрутов или активных навигационных ссылок. Это происходит, когда:

  1. Компонентconnect()(Comp)Подключиться к редуксу.
  2. Компонент не является «компонентом маршрутизации», т.е. компонент не похож на<Route component={SomeConnectedThing} />Рендерить вот так.

Причина этой проблемы в том, что Redux реализуетshouldComponentUpdate, компонент не получает обновления реквизита при изменении маршрута.

Решение этой проблемы простое, найтиconnectи использовать егоwithRouterпакет:

// before
export default connect(mapStateToProps)(Something)

// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))

Глубокая интеграция редукции с реактивным маршрутизатором

Иногда мы можем захотеть более глубоко интегрировать Redux с React Router для достижения:

  • Синхронизируйте данные роутера с магазином и получите к ним доступ из магазина
  • Навигация через диспетчерские действия
  • Поддержка отладки изменений маршрута во времени в Redux devtools

Этого можно достичь за счет глубокой интеграции react-router с redux через Connected-React-Router и библиотеки истории.

В официальной документации упоминается react-router-redux, и он был интегрирован в react-router v4, но, согласно документации react-router-redux, репозиторий больше не поддерживается, и рекомендуется connect-react-router.

Сначала установите Connected-React-Router и библиотеки истории:

$ npm install --save connected-react-router
$ npm install --save history

Затем добавьте в хранилище следующую конфигурацию:

  • Создайтеhistoryобъект, потому что наше приложение работает на стороне браузера, поэтому используйтеcreateBrowserHistoryСоздайте
  • использоватьconnectRouterобертывает корневой редуктор и обеспечиваетhistoryобъект, получить новый корневой редуктор
  • использоватьrouterMiddleware(history)Реализуйте действия с использованием истории отправки, чтобы вы могли использоватьpush('/path/to/somewhere')изменить маршрут (нажатие здесь от connect-react-router)
import thunk from 'redux-thunk'
import { createBrowserHistory } from 'history'

import { createStore, applyMiddleware } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'

import reducers from '../reducers'

export const history = createBrowserHistory()
const initialState = {}

const store = createStore(
  connectRouter(history)(reducers),
  initialState,
  applyMiddleware(thunk, routerMiddleware(history))
)

export default store

В корневом компоненте добавляем следующую конфигурацию:

  • использоватьConnectedRouterОбернуть маршрут и разместить созданный в магазинеhistoryВведение объекта, переданного в приложение в качестве реквизита
  • ConnectedRouterкомпонент, который будетProviderподкомпонент
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'

import App from './App'
import store from './redux/store'
import { history } from './redux/store'

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)

Таким образом, мы интегрировали Redux с react-router.

Используйте диспетчеризацию для переключения маршрутов

После завершения вышеуказанной конфигурации вы можете использоватьdispatchПереключение маршрутов:

import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))

react-router-config

До реакции-маршрутизатора v4 - статическая маршрутизация

В версиях до react-router v4 мы могли настраивать маршруты приложения напрямую, используя статические маршруты, что позволяло проверять и сопоставлять маршруты перед рендерингом.

В router.js обычно есть такой код:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)
export default routes

Затем импортируйте маршрут во время инициализации, а затем выполните рендеринг:

import ReactDOM from 'react-dom'
import routes from './config/routes'

ReactDOM.render(routes, document.getElementById('app'))

Динамический маршрутизатор React-Router V4--

Начиная с версии V4, React-Router использует динамические компоненты вместо конфигурации пути, т.е.react-router является общим компонентом реагирующего приложения., напишите так, как вы его используете, вам не нужно отделять маршрут от компонента, как раньше. Таким образом, приложение реакции добавляет react-router, чтобы представить то, что нам нужно.

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

Здесь мы будемBrowserRouterПредставлен и переименован вRouter,BrowserRouterРазрешить react-router передавать информацию о маршрутизации приложения черезcontextПередается любому из необходимых компонентов. Таким образом, для правильной работы react-router необходимо, чтобы он отображался в корневом узле приложения.BrowserRouter.

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <Router>
        <div>
          <div>
            <Link to="/">Home</Link>
          </div>
          <hr />
          <Route exact path="/" component={Home} />
        </div>
      </Router>
    )
  }
}

также используетсяRoute, когда местоположение приложения совпадает с маршрутом,RouteОтобразит указанный компонент, иначе отобразитnull.

Добавить больше маршрутов, добавитьRouteКомпоненты можно использовать, но такой способ написания может показаться немного сумбурным, ведь маршруты разбросаны по всем компонентам, сложно увидеть маршруты всего приложения так же легко, как раньше, а если в проекте использовались версия react before -Router до v4, то обновление v4 тоже очень дорого.Чтобы решить эту проблему, официальный предоставляет библиотеку, специально используемую для работы со статической конфигурацией маршрутизации - react-router-config.

Добавьте react-router-config для использования статической маршрутизации

После добавления react-router-config мы можем написать наши знакомые статические маршруты. В то же время с его помощью конфигурацию маршрутизации можно рассредоточить по различным компонентам, и в итоге использоватьrenderRoutesСлейте разрозненные фрагменты роутинга в корневом компоненте и отрендерите его.

Настройте статические маршруты:

import Home from './views/Home'
import About from './views/About'

const routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
export default routes

Затем слейте корневой компонент, визуализируйте:

import { renderRoutes } from 'react-router-config'

import HomeRoute from './views/Home/router'
import AboutRoute from './views/About/router'
// 合并路由
const routes = [...HomeRoute, ...AboutRoute]

class App extends Component {
  render() {
    return (
      <Router>
        <div className="screen">{renderRoutes(routes)}</div>
      </Router>
    )
  }
}

renderRoutesЭто на самом деле помогло нам сделать что-то подобное:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)

Это добавляет статическую маршрутизацию в приложение React.

Добавить замену горячего модуля

Функция горячей замены модулей (HMR) заменяет, добавляет или удаляет модули во время работы приложения без перезагрузки всей страницы. В основном следующими способами:

  • Сохранить состояние приложения, потерянное при полной перезагрузке страницы
  • Обновляйте только измененный контент, чтобы сэкономить время разработки
  • Изменение стилей не требует обновления страницы

В режиме разработки HMR может заменить LiveReload, поддерживаемый webpack-dev-server.hotмодальный, прежде чем пытаться перезагрузить всю страницу,hotСхема пытается обновиться с помощью HMR.

Включить HMR

Добавьте плагин HMR в файл конфигурации webpack:

plugins: [new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin()]

добавлено сюдаNamedModulesPluginплагин,

Установите сервер webpack-dev наhotмодель:

const server = new WebpackDevServer(compiler, {
+  hot: true,
  // noInfo: true,
  quiet: true,
  historyApiFallback: true,
  filename: config.output.filename,
  publicPath: config.output.publicPath,
  stats: {
    colors: true
  }
});

Таким образом, при изменении кода реакции страница будет автоматически обновляться, изменять файл css, страница не будет обновляться, а стиль будет представлен напрямую.

Но будет проблема.Автоматическое обновление страницы приведет к потере состояния нашего компонента реакции.Можем ли мы изменить компонент реакции, например, изменить файл css, не обновляя страницу (сохраняя состояние страницы) и заменить это напрямую? Ответ — да, вы можете использовать react-hot-loader.

добавить реактивный горячий загрузчик

Добавить react-hot-loader очень просто, просто добавьте метод более высокого порядка при экспорте корневого компонента.hotТолько что:

import { hot } from "react-hot-loader";

class App extends Component {
	...
}

export default hot(module)(App);

Таким образом, все приложение может изменять компонент реакции, сохраняя при этом состояние во время разработки.

Примечание:

В процессе разработки я читал какие-то статьи и говорил, что для взаимодействия с редуксом нужно добавить в store.js следующий код:

if (process.env.NODE_ENV === 'development') {
  if (module.hot) {
    module.hot.accept('../reducers/index.js', () => {
      // const nextReducer = combineReducers(require('../reducers'))
      // store.replaceReducer(nextReducer)
      store.replaceReducer(require('../reducers/index.js').default)
    })
  }
}

Однако в react-hot-loader v4 он не нужен, добавьте его напрямуюhotВот и все.

Загружать компоненты асинхронно (разделение кода)

После завершения вышеуказанной конфигурации наше основное тело было построено почти, но когда вы открываете инструмент разработчика, вы обнаружите, что когда приложение начинает загружать, все JS всей приложения загружается напрямую, но мы ожидаем, что Страница для загрузки кода какой страницы, так как реализовать кодовое расщепление приложения?

Фактически, для достижения реагированного кода разделения библиотеки там много, таких как:

Вы можете выбрать один из них, в моем проекте используется react-loadable.

Мы настроили статическую маршрутизацию в проекте ранее, и компоненты импортируются напрямую, нам нужно обработать только напрямую импортированные компоненты, код выглядит следующим образом:

import loadable from 'react-loadable'
import Loading from '../../components/Loading'

export const Home = loadable({
  loader: () => import('./Home'),
  loading: Loading
})
export const About = loadable({
  loader: () => import('./About'),
  loading: Loading
})

const routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
export default routes

Асинхронное управление потоком задач

Идеи для реализации асинхронных операций

В большинстве случаев наши приложения являются синхронными операциями, то есть при отправке действия состояние будет обновляться немедленно, но иногда нам нужно выполнять асинхронные операции. Синхронные операции должны выполнять только один тип действий, а асинхронные операции должны выполнять три типа действий.

  • Действие, когда действие инициировано
  • Действия при успешном выполнении операции
  • Действия при сбое операции

Чтобы различать эти три действия, можно добавить специальныйstatusПоля как биты флага:

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

Или определить для них разные типы:

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

Итак, если вы хотите добиться асинхронной работы, вам нужно сделать:

  • Когда операция начинается, выдается действие, состояние обновляется до «работа», а представление повторно отображается.
  • После завершения операции выдается другое действие, которое запускает обновление состояния до «Конец операции», и представление снова перерисовывается.

redux-thunk

Асинхронная операция отправляет как минимум два действия. Первое действие такое же, как и синхронная операция, которую можно отправить напрямую. Итак, как отправить второе действие?

Мы можем отправить функцию Action Creator при отправке первого действия, чтобы второе действие могло быть отправлено автоматически после завершения асинхронного выполнения.

componentDidMount() {
   store.dispatch(fetchPosts())
}

После успешной загрузки компонента отправьте действие для запроса данных, здесьfetchPostsЭто создатель действий. Код fetchPosts выглядит следующим образом:

export const SET_DEMO_DATA = createActionSet('SET_DEMO_DATA')

export const fetchPosts = () => async (dispatch, getState) => {
  store.dispatch({ type: SET_DEMO_DATA.PENDING })
  await axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response => store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }))
    .catch(err => store.dispatch({ type: SET_DEMO_DATA.ERROR, payload: err }))
}

fetchPostsэто создатель действий, который возвращает функцию при выполненииdispatchДействие, указывающее, что будет выполнена асинхронная операция; после завершения асинхронного выполнения, в соответствии с различными результатами запроса,dispatchРазличные действия возвращают результат асинхронной операции.

Несколько замечаний:

  1. fetchPostsВозвращается функция, тогда как обычный Action Creator по умолчанию возвращает объект.
  2. Параметры возвращаемой функции:dispatchа такжеgetStateДля этих двух методов Redux параметром общего Action Creator является содержимое Action.
  3. В возвращаемой функции сначала введитеstore.dispatch({type: SET_DEMO_DATA.PENDING}), указывающий на начало асинхронной операции.
  4. После завершения асинхронной операции выполните еще однуstore.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }), означающий окончание операции.

Но существует проблема,store.dispatchПри нормальных обстоятельствах можно отправлять только объекты, а мы хотим отправлять функции, чтобы позволитьstore.dispatchФункции можно отправлять, мы используем middleware — redux-thunk.

Внедрить redux-thunk легко, просто используйте его при создании магазина.applyMiddleware(thunk)Импортируйте его.

Инструменты разработки и отладки

Отладка неизбежна в процессе разработки.Существует множество часто используемых инструментов отладки, таких как redux-devtools-extension, redux-devtools, storybook и т. д.

redux-devtools-extension

redux-devtools-extension — инструмент для отладки редукса, которым очень удобно следить за действиями.

Первый по версии браузераChrome Web StoreилиMozilla Add-onsчтобы скачать плагин.

Затем при создании магазина добавьте его в конфигурацию Enhancer Store можно:

import thunk from "redux-thunk";
import { createBrowserHistory } from "history";

import { createStore, applyMiddleware } from "redux";
+ import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
import { connectRouter, routerMiddleware } from "connected-react-router";

import reducers from "../reducers";

export const history = createBrowserHistory();
const initialState = {};

+  const composeEnhancers = composeWithDevTools({
+   // options like actionSanitizer, stateSanitizer
+ });

const store = createStore(
  connectRouter(history)(reducers),
  initialState,
+  composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
);

напиши в конце

В этой статье выясняется мое понимание архитектуры приложения React и конкретной конфигурации связанных библиотек, а также углубляется понимание архитектуры приложения React.Однако в этой статье не рассматриваются такие аспекты, как неизменяемость данных, сохраняемость, оптимизация веб-пакетов и т. д. , и в будущем продолжит изучение смежных тем, а также постарается построить более полноценное React-приложение.

Кроме того, после обновления последней версии Babel в процессе сборки проекта было обнаружено, что @babel/preset-stage-0 скоро устаревает.Рекомендуется использовать другие альтернативы.Подробнее см. здесь:

прикрепил

Ключевые слова:

  • redux
  • react-router
  • react-router-config
  • Асинхронная загрузка (разделение кода)
  • Горячее обновление
  • Асинхронное управление задачами — redux-thunk
  • react-redux
  • redux-devtools-extension

Некоторые используемые библиотеки

Ссылаться на