
Сейчас много споров по поводу универсального (изоморфного) кода, есть свои за и против.
Я считаю, что за универсальным (изоморфним) кодом будущее, так как он позволяет взять лучшее с серверного и клиентского рендеринга.
В ходе разработки в нашей команде получился неплохой бойлер-плейт и я решил им поделиться. Код можно посмотреть здесь.
Я не хочу лить воду, так как на эту тему есть очень хороший туториал:
- React.js: собираем с нуля изоморфное / универсальное приложение. Часть 1
- React.js: собираем с нуля изоморфное / универсальное приложение. Часть 2
- React.js: собираем с нуля изоморфное / универсальное приложение. Часть 3
По поводу, что такое Kоа:
- Koajs 2.0: новое поколение фреймворка нового поколения
- Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность
- Пишем микросервис на KoaJS 2 в стиле ES2017. Часть II: Минималистичный REST
Запуск проекта для разработки:
git clone https://github.com/BoryaMogila/koa_react_redux.git; npm install; npm run-script run-with-build;
Посмотреть тестовый запуск можно на url: localhost(127.0.0.1):4000/app/
Запуск проекта для продакшена:
// сборка скриптов npm run-script build-production; // сборка серверных скриптов и запуск ноды npm run-script run-production;
Серверные скрипты собираются, а не используется babel-register потому, что при использовании lazy-loading при первом запросе роута время отдачи около двух секунд из-за транспиляции кода.
Клиентские скрипты собираются для продакшн сборки также и в gzip формате. Для раздачи скриптов настоятельно рекомендую использовать nginx вместо koa-serve-static (реально удобно). Серверный код лежит в папке app, изоморфный и клиентский в папке src.
Контроллеры для api пишем в папке koa_react_redux/app/controllers/:
//koa_react_redux/app/controllers/getPostsController.js export default async function(ctx) { // ваш код по обработке данных и формированию ответа. //................ // ответ в виде json ctx.body = [ { title: 'React', text: 'React is a good framework' }, { title: 'React + Redux', text: 'React + Redux is a cool thing for isomorphic apps' }, { title: 'React + Redux + React-router', text: 'React + Redux + React-router is a cool thing for isomorphic flexible apps' } ] }
Серверные роуты прописываем в файле koa_react_redux/app/routes/index.js по типу:
import getPosts from '../controllers/getPostsController'; router.get('/getPosts/', getPosts);
Универсальные роуты пишем в файле koa_react_redux/src/routes.js:
import React from 'react'; import Route from 'react-router/lib/Route' import IndexRoute from 'react-router/lib/IndexRoute' import App from './components/App'; // для lazy-loading const getPosts = (nextState, callback) => require.ensure( [], (require) => { callback(null, require("./containers/Posts").default) } ), getPost = (nextState, callback) => require.ensure( [], (require) => { callback(null, require("./containers/Post").default) } ); function createRoutes() { return ( <Route path="/app/" component={App}> // если не нужен lazy-loading, тогда импортим компонент и пишем стандартно // <IndexRoute сomponent={/*компонент*/}/> <IndexRoute getComponent={getPosts}/> <Route path='post/:id' getComponent={getPost}/> </Route> ) } export default createRoutes
Общие middleware для redux подключаем стандартно в файле koa_react_redux/src/storeCinfigurator.js
import { createStore, combineReducers, compose, applyMiddleware } from 'redux' import promiseErrorLogger from './middlewares/promiseErrorLogger' createStore( combineReducers({ ...reducers }), initialState, compose( applyMiddleware( promiseErrorLogger, ) ) )
Клиентские middleware в файле koa_react_redux/src/index.js:
import promiseErrorLogger from './middlewares/promiseErrorLogger' import { configureStore} from './storeCinfigurator' configureStore(browserHistory, window.init, [promiseErrorLogger]);
Серверные по аналогии в файле koa_react_redux/app/controllers/reactAppController.js.
Асинхронные экшены:
import {GET_POSTS} from './actionsTypes' import superagentFactory from '../helpers/superagentFactory' const superagent = superagentFactory(); export function getPosts(){ return { type: GET_POSTS, payload: superagent .get('/getPosts/') .then(res => res.body) } }
Для асинхронных экшенов редюсери:
import {GET_POSTS, PENDING, SUCCESS, ERROR} from '../actions/actionsTypes'; export default function(state = [], action = {}){ switch (action.type){ case GET_POSTS + SUCCESS: return action.payload; case GET_POSTS + PENDING: return state; case GET_POSTS + ERROR: return state; default: return state; } }
Редюсеры для redux подключаем в файле koa_react_redux/src/reducers/index.js:
import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux' import posts from './postsReducer' const rootReducer = { posts, routing: routerReducer }; export default rootReducer;
Общую конфигурацию пишем по аналогии config js в папке koa_react_redux/config/, была сделана своя обёртка для изоморфного использования.
Серверную конфигурацию пишем так:
const config = { //общая конфигурация }; // for cut server-side config if (typeof cutCode === 'undefined') { Object.assign(config, { // серверная конфигурация }); } module.exports = config;
Для SEO наша команда использует библиотеку «шлем»))) (react-helmet)
Работает так:
// код пишем в компоненте import Helmet from "react-helmet"; <div className="application"> <Helmet htmlAttributes={{"lang": "en", "amp": undefined}} // amp takes no value title="My Title" titleTemplate="MySite.com - %s" defaultTitle="My Default Title" base={{"target": "_blank", "href": "http://mysite.com/"}} meta={[ {"name": "description", "content": "Helmet application"}, {"property": "og:type", "content": "article"} ]} link={[ {"rel": "canonical", "href": "http://mysite.com/example"}, {"rel": "apple-touch-icon", "href": "http://mysite.com/img/apple-touch-icon-57x57.png"}, {"rel": "apple-touch-icon", "sizes": "72x72", "href": "http://mysite.com/img/apple-touch-icon-72x72.png"} ]} script={[ {"src": "http://include.com/pathtojs.js", "type": "text/javascript"}, {"type": "application/ld+json", innerHTML: `{ "@context": "http://schema.org" }`} ]} onChangeClientState={(newState) => console.log(newState)} /> ... </div>
Данные для server-rendering пишем в контейнере, который подключаем в роутах:
import {getPosts} from '../actions' class Posts extends Component { constructor(props) { super(props); } // эта функция выполняется на сервере для получения данных static fetchData(dispatch, uriParams, allProps = {}) { const promiseArr = [ dispatch(getPosts()), ]; // массив асинхронных экшенов для получения серверных данных return Promise.all(promiseArr); } render(){ return ( //ваша разметка ); } }
P.S. Советую разнести api и раздачу скриптов по отдельнных проектах во избежание казусов и надежности. Буду рад услышать ваши комментарии и замечания