Это поможет любому, кто пытается научиться создавать полное изоморфное приложение React-Redux.

Вы, наверное, уже знаете, как список TO-DO стал похож на программу Hello World, когда дело доходит до изучения или тестирования нового фреймворка Javascript. И поэтому я буду использовать список дел, чтобы показать, как работает Isomorphic React-Redux.

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

Полный код можно найти в GitHub.

Так в конечном итоге будет выглядеть наша страница.

Как вы, возможно, уже заметили, я использую бутстрап для создания стиля и навигации.

Что все вы можете делать в этом приложении?

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

С чего мне начать?

Возможно, вы думаете, откуда начать изоморфное приложение. Что нужно начинать проектировать и кодировать - со стороны сервера или клиента?

Что ж, я предлагаю создать обычное клиентское приложение, как вы обычно делаете, а затем мы просто сделаем несколько последних настроек, чтобы подключиться к серверу и обслуживать его оттуда.

Базовая структура папок

Ниже представлена ​​структура папок, которую я использовал.

dist - будет целевой библиотекой, из которой мы ссылаемся на CSS и связанный JS.

node_modules - здесь будут присутствовать все модули узлов, которые мы устанавливаем с npm, а также зависимости. Эта папка создается автоматически.

src - исходная папка. Здесь происходит все действие

.babelrc - конфигурации для Babel. например. пресеты или плагины

index.html - будет использоваться для рендеринга на стороне клиента.

package.json - пакеты и скрипты зависимостей npm.

webpack.config.js - базовая конфигурация, используемая webpack.

В разделе src вы можете увидеть подпапки css, js и views.

  • css - Здесь ничего особенного не происходит. Bootstrap покрывает лишь некоторые базовые стилистики.
  • js - Мы делим его на три подпапки в зависимости от того, где он предназначен для использования.
  • просмотры - мы будем использовать ejs для создания шаблонов HTML. Это не требуется, когда вы используете только клиент, а мы используем index.html для клиента, но когда дело доходит до первоначального рендеринга с сервера, я объясню, почему мы его используем.

Более подробное изучение структуры папок в разделе src / js

  • client - имеет client.js, который является точкой входа, когда мы запускаем приложение только как клиент
  • server - имеет server.js, который будет точкой входа, когда мы запустим приложение как изоморфное.
  • shared - содержит компоненты React, действия и редукторы Redux. Компоненты реакции здесь не разделяются на интеллектуальные и глупые компоненты. Но если вам интересно узнать, какие компоненты являются умными, то лучше всего использовать декоратор подключения.

@соединять()

Давайте начнем сейчас, не так ли?

Для начала вам необходимо установить все зависимости.

npm i -S ‹имя-пакета

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

Создание клиентского приложения

Если вы уже создали клиентское приложение и хотите преобразовать его в изоморфное, пропустите этот раздел и перейдите к последнему разделу.

Давайте сначала начнем с создания только на стороне клиента списка задач React-Redux.

Если вы не знакомы с React или Redux, я бы порекомендовал вам щелкнуть соответствующие ссылки, чтобы узнать больше, прежде чем продолжить.

  1. Как упоминалось ранее, точка входа - с client.js
const app = document.getElementById('app');
const initialState = window.__REDUX_STATE__
const store = createStore(allReducer,initialState);
ReactDOM.render(
 <Provider store={store}>
     <Router history={browserHistory} routes={routes} />
   </Provider>
 , app);

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

const store = createStore(allReducer);

Как видите, client.js устанавливает некоторые очень важные вещи.

  • Он создает магазин Redux.
  • И он передает его в React, делая его доступным для наших интеллектуальных компонентов. Это делается с помощью пакета npm response-redux.
<Provider store={store}>
  • Также, как вы видите, мы настроили маршрутизацию для нашего одностраничного приложения (SPA). Это делается пакетом npm response-router.
<Router history={browserHistory} routes={routes} />

Опора routes, которую вы видите выше, - это маршруты, которые мы импортируем из routes.js

Теперь, когда вы понимаете точку входа. Посмотрим, что будет дальше, посмотрев на маршрут. Когда мы попадаем на путь по умолчанию /

React отобразит компоненты Layout и Index, как IndexRoute.

А компонент Индекс отобразит компонент ToDoList.

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

2. А теперь давайте подробнее рассмотрим TodoList.js

return(
   <div>
     <AddTodo/>
     <ListToDoItems/>
   </div>
  );

Он отобразит AddTodo и ListToDoItems.

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

3. ToDoActions, ToDoReducers и AllReducers

  • Когда когда-либо реагирует на обновление состояния, он должен делать это через хранилище redux. Для этого наш компонент реакции должен будет отправлять действия, которые присутствуют вToDoActions.js

Здесь следует отметить важный момент. Действие всегда должно иметь свойство Тип. Остальные свойства могут быть любыми в зависимости от того, какие данные вы хотите передать.

  • Наш редуктор подхватит действие и создаст новый объект состояния. Все редукторы, относящиеся к to-do, можно найти в ToDoReducers.js
const initialState = { items:[]}
let id = 0;
const todoReducer = (state=initialState, action) => { 
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items:     state.items.concat({id:id++,text:action.item,editItem:false,completed:false})
      }
......
....
...

1. Очень важный момент: состояние никогда не должно изменяться. то есть наше состояние всегда должно быть неизменным.

И поэтому вы можете видеть, что в нашем редукторе мы используем оператор распространения (…), чтобы каждый раз создавать новый объект состояния, а затем обновлять в нем соответствующую информацию. Оператор спреда является частью ES7.

2. Также обратите внимание, что мы передаем начальное состояние редуктору? В нашем случае нам нужно, чтобы в нашем начальном состоянии был пустой массив items.

  • Если у нас есть несколько Reducer, каждый из которых обслуживает разные функции или части приложения, то лучше хранить их в отдельных файлах и объединять их, используя combineReducers, как показано на AllReducers.js

Ниже вы можете найти все js файлы, относящиеся к действиям и редукторам для приложения со списком дел.

Если вы видели приведенные выше фрагменты и хорошо понимаете, как выглядят действия и редукторы, давайте вернемся к нашим компонентам и попробуем отправить некоторые действия.

4. Компонент AddTodo

Код компонента AddTodo: -

Как вы видите выше, когда пользователь вводит что-то в поле ввода и когда он нажимает кнопку Добавить элемент, мы отправляем действие AddItem с новый список дел.

this.props.dispatch(AddItem(newItem))

Действие запускается, которое, в свою очередь, выбирается редуктором и обновляет состояние в хранилище Redux, добавляя новый элемент в массив items.

Итак, теперь вам может быть интересно, как это обновит мой компонент ListToDoItems и отобразит новый элемент?

Ну, это просто, ListToDoItems также является интеллектуальным компонентом, поэтому он отслеживает любые изменения в нашем магазине. Поэтому, когда AddToDo обновляет состояние, ListToDoItems извлекает последнее состояние из магазина.

4. Компонент ListToDoItems

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

Как запустить созданный клиентский код?

Здесь вам понадобятся webpack и webpack-dev-server

Webpack - это сборщик модулей, который мы будем использовать для объединения наших файлов js и помещения его в папку dist/js.

Если вы отметите webpack.config.js, вы увидите, что для объединения мы упоминаем точку входа и выход, как показано ниже: -

module.exports = {
  entry: path.join(__dirname, 'src/js/client', 'client.js'),
  output: {
    path: path.join(__dirname, 'dist', 'js'),
    filename: 'bundled.js'
  },
  debug:true,
  devtool: 'source-map',  
  module: {
    loaders: [{
      test: path.join(__dirname, 'src/js'),
      loader: ['babel-loader'],
      query: {
        //cacheDirectory: 'babel_cache',
        presets: ['react', 'es2015','stage-0'],
        plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
      }
    },
    { test: /\.css$/, loader: "style-loader!css-loader" },
    { test: /\.png$/, loader: "url-loader?limit=100000" },
    { test: /\.jpg$/, loader: "file-loader" },
    {
        test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/,
        loader: 'url-loader'
    }
    ]
  },

Также вы можете заметить несколько вещей: мы используем babel-loader для создания наших js-файлов и преобразования ES6 в ES5 во время сборки. Пресеты и плагины, упомянутые в файле конфигурации, важны для правильного переноса с ES6 на ES5, а также для понимания декораторов и т. Д.

Если вы заметили сценарий в package.json, у нас есть

“scripts”: {
“dev”: “webpack-dev-server — content-base — inline — hot”,

мы используем webpack-dev-server для клиента, поэтому вы можете запустить

npm запустить dev

Если ваша сборка прошла успешно, перейдите по адресу http: // localhost: 8080 /, чтобы увидеть клиентское приложение в действии :)

Преобразование его в изоморфный

Если вы добрались сюда, я считаю, что ваше клиентское приложение работает идеально, как ожидалось?

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

Для начала нам нужно создать простой экспресс-сервер.

Если вы еще не установили express, ejs, babel-cli, path ранее через npm, установите их, используя npm i -S <package-name>, прежде чем мы продолжим.

После установки всех зависимостей создайте файл с именем server.js

И добавьте приведенный ниже код

var path = require('path');
var express = require('express');
var app = express();
// start the server
var port = process.env.PORT || 4000;
var env = process.env.NODE_ENV || 'development';
app.listen(port,  (err) => {
    if (err) {
        return console.error(err);
    }
    console.info('+++Server running on http://localhost:' + port + ' [' + env + ']');
});

И проверьте с узла, используя следующую команду:

babel-node src/js/server/server.js

перейдите по адресу http: // localhost: 4000 / и посмотрите, запускается ли сервер.

Здорово! у вас работает простой экспресс-сервер. Теперь давайте добавим код, связанный с реакцией и редукцией, в server.js и снова запустим его.

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../../views'));
app.use(express.static(path.join(__dirname, '../../../dist')));
app.get('*',  (req, res) =>  {
match({ routes: routes, location: req.url },  (err, redirectLocation, renderProps) => {
// in case of error display the error message
        if (err) {
            return res.status(500).send(err.message);
        }
// in case of redirect propagate the redirect to the browser
        if (redirectLocation) {
            return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
        }
var markup,
            store,
            initialState = {todoReducer:
                               {
                                   items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
                               }
                           }
            
            store = createStore(allReducers,initialState)
            initialState = store.getState() //JSON.stringify(store.getState())
if (renderProps) {
            markup = renderToString(
                <Provider store={store}>
                   { <RouterContext {...renderProps} />}
                </Provider>
            )
        }
return res.render('index', { markup: markup, initialState: initialState });
    });
});

Теперь добавьте приведенный выше код сразу после var app = express();

Прежде чем продолжить, мы постараемся понять приведенный выше код.

  • Сначала мы упоминаем механизм шаблонов, который мы собираемся использовать, и откуда его нужно выбирать. В нашем случае это index.ejs из папки views
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../../views'));
  • Далее мы упоминаем, откуда сервер должен выбирать статические файлы.

app.use(express.static(path.join(__dirname, ‘../../../dist’)));

На этом этапе, если вы снова запустите сервер, вы увидите сообщение Cannot GET /

Это потому, что мы ничего не отправляем клиенту с сервера.

Далее мы добавим оставшуюся часть кода.

app.get('*',  (req, res) =>  {
match({ routes: routes, location: req.url },  (err, redirectLocation, renderProps) => {
// in case of error display the error message
        if (err) {
            return res.status(500).send(err.message);
        }
// in case of redirect propagate the redirect to the browser
        if (redirectLocation) {
            return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
        }
var markup,
            store,
            initialState = {todoReducer:
                               {
                                   items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
                               }
                           }
            
            store = createStore(allReducers,initialState)
            initialState = store.getState() //JSON.stringify(store.getState())
if (renderProps) {
            markup = renderToString(
                <Provider store={store}>
                   { <RouterContext {...renderProps} />}
                </Provider>
            )
        }
return res.render('index', { markup: markup, initialState: initialState });
    });
});

Здесь происходит следующее: мы сообщаем серверу, что когда сервер получает запрос GET, нам нужно сопоставить request.url с адресом from routes.js.

Для этого мы будем использовать функцию match из response-router, и если она совпадает, мы создаем начальное состояние и хранилище, используя начальное состояние и редуктор.

initialState = {todoReducer:
                               {
                                   items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
                               }
                           }
            
store = createStore(allReducers,initialState)

Мы создаем фиктивное начальное состояние с делом под названием «Начальное состояние, чтобы сделать элемент».

Мы передаем это начальное состояние вместе с редуктором, который мы создали AllReducer для создания магазина.

Затем нам нужно заполнить разметку, которую ожидает наш index.ejs файл.

<div id="app">
        <%- markup -%>
</div>

Для этого мы создадим в server.js следующее:

if (renderProps) {
            markup = renderToString(
                <Provider store={store}>
                   { <RouterContext {...renderProps} />}
                </Provider>
            )
        }

renderToString используется для рендеринга элемента React в его исходный HTML.

Если вы вызовете ReactDOM.render() на узле, на котором уже есть эта разметка, отрисованная сервером, React сохранит ее и подключит только обработчики событий, что позволит вам получить очень эффективную работу с первой загрузкой.

Если вы заметили нашу разметку, она похожа на то, что мы создали в client.js, но мы используем ‹RouterContext› здесь, на сервере, вместо ‹Router›. RouterContext сохранит историю, поэтому вам не нужно добавлять ее, как в Router.

Наконец, мы передаем разметку, начальное состояние вместе с нашим index.ejs в качестве ответа на полученный запрос.

return res.render(‘index’, { markup: markup, initialState: initialState });

Итак, наконец, мы закончили создание нашего изоморфного приложения.

Чего же ты ждешь? Снова запустите сервер.

Я добавил этот скрипт в package.json

“server”: “webpack && babel-node src/js/server/server.js”

Это соберет и объединит наши файлы, а также запустит сервер.

Совет здесь,

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

Просто запустите npm run server, чтобы запустить сервер и посмотреть свое первое изоморфное приложение.

Когда вы перейдете на http: // localhost: 4000 /, вы должны увидеть следующее: -

Но видите ли вы «Начальное состояние для выполнения элемента»?

Что ж, вы этого не видите, потому что нам еще предстоит сделать одну крошечную вещь. то есть нам нужно поместить наше Начальное состояние в объект окна, чтобы он также мог использоваться клиентом.

Добавьте указанный ниже блок <script> в свой index.ejs файл.

<script type="text/javascript" charset="utf-8">
      window.__REDUX_STATE__ = JSON.parse('<%- JSON.stringify(initialState) %>');
      
</script>

Это начальное состояние, которое вы передаете в res.render в server.js

И измените client.js, чтобы получить исходное состояние и использовать его также для создания своего магазина в клиенте.

const initialState = window.__REDUX_STATE__
const store = createStore(allReducer,initialState);

Запустите сервер в последний раз, и на этот раз вы должны увидеть начальную задачу, которую мы передаем с сервера :)

Заключить

Надеюсь, это поможет вам, ребята, в создании изоморфного приложения с использованием React и Redux.

Удачного кодирования !!