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

    Универсальный Koa

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

    В ходе разработки в нашей команде получился неплохой бойлер-плейт и я решил им поделиться. Код можно посмотреть здесь.

    Я не хочу лить воду, так как на эту тему есть очень хороший туториал:


    По поводу, что такое Kоа:


    Запуск проекта для разработки:

           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 и раздачу скриптов по отдельнных проектах во избежание казусов и надежности. Буду рад услышать ваши комментарии и замечания
    • +5
    • 13,7k
    • 6
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 6
    • 0
      Смотрю на весь этот дикий ад и чувствую себя старым.
      • 0
        А почему ад? Предложите решение по лучше.
      • +1
        В мое время сайты писали в нотепаде! Иш развели тяхнологий.
        • –1
          )))
          • +1
            Сайты и сейчас можно писать в нотепаде, а вот веб-приложения тяжеловато…
          • 0
            А что делать с клиентскими экшенами? Не уверен что superagent можно использовать изоморфно. Может и можно, но без кэша

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.