Универсальный (Изоморфный) проект на Koa 2.x + React + Redux + React-Router

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

В ходе разработки в нашей команде получился неплохой бойлер-плейт и я решил им поделиться. Код можно посмотреть здесь.
Я не хочу лить воду, так как на эту тему есть очень хороший туториал:
  • 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 и раздачу скриптов по отдельнных проектах во избежание казусов и надежности. Буду рад услышать ваши комментарии и замечания

Комментарии (0)

© Habrahabr.ru