Универсальный (Изоморфный) проект на Koa 2.x + React + Redux + React-Router
Сейчас много споров по поводу универсального (изоморфного) кода, есть свои за и против.
Я считаю, что за универсальным (изоморфним) кодом будущее, так как он позволяет взять лучшее с серверного и клиентского рендеринга.
В ходе разработки в нашей команде получился неплохой бойлер-плейт и я решил им поделиться. Код можно посмотреть здесь.
Я не хочу лить воду, так как на эту тему есть очень хороший туториал:
- 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 (
// если не нужен lazy-loading, тогда импортим компонент и пишем стандартно
//
)
}
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";
console.log(newState)}
/>
...
Данные для 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 и раздачу скриптов по отдельнных проектах во избежание казусов и надежности. Буду рад услышать ваши комментарии и замечания