Pull to refresh

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

Reading time 5 min
Views 19K
Универсальный 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 и раздачу скриптов по отдельнных проектах во избежание казусов и надежности. Буду рад услышать ваши комментарии и замечания
Tags:
Hubs:
+5
Comments 6
Comments Comments 6

Articles