Блог а-ля Хабр, выбор платформы

  • Tutorial

В предыдущей серии (Как слямзить Хабр по-быстрому) запустил проект на базе Create React App (CRA). Но это SPA, что не очень подходит, когда требуется индексация в поисковиках. Нужен Server Side Rendering (SSR). И желательно из коробки, а не на коленке. Крайне расточительно тратить ресурсы на самостоятельную разработку базовых технологий. Как выбирать платформу с поддержкой SSR? На практике, конечно, POC. Попробую реализовать CRUD с формой ввода на Material-UI, рассматривая кандидатов: React Starter Kit (RSK), NEXT.js и Electrode (не путать с Electron).


Исходники на GitHub.


Роутинг


RSK имеет собственный велосипед — universal-router. Загружать внешние данные в Redux при серверном рендеринге предлагается через конфигурацию express (или koa, или hapi).


NEXT.js подошёл к вопросу роутинга оригинально, предложив использовать имена файлов в папке pages. Внутри обычные React-компоненты со статическим методом getInitialProps, в котором предлагается выполнять загрузку внешних данных при серверном рендеринге. Если же нужно кастомизировать обработку запросов, то это делается самым естественным способом — через конфигурацию express (или koa, или hapi).


Electrode, в отличии от других рассмотренных платформ, не заставляет отказываться от redux-router. А это значит, что на время реализации фронтенда, можно вернуться к разработке на CRA. Вся магия серверного рендеринга внутри electrode-redux-router-engine.


Производительность


Очень интересно наблюдать, как загружается сайт zeit.co, который работает на NEXT.js. Когда открываешь страничку списка постов, приходит 15 MB траффика, но дальше переходы на страницы постов происходят мгновенно — всё кешируется в браузере. Однако это безобразие можно отключить через свойство prefetch компонента Link. Offline-first обещан в NEXT.js (с оговорками), но в Electrode уже реализован. Кеширование SSR: в NEXT.js есть пример, но в Electrode решение в продакшене.


В RSK замечено не менее интересное поведение. Когда переключаю пункты списка истории строки ввода URL в Хроме, то наблюдаю выполнение роутов на сервере, хотя ещё не подтвердил свой выбор. А в результате готовая страница выдаётся с сервера быстрее.


Настройка сборки


NEXT.js и Electrode скрывают свою реализацию, но позволяют конфигурировать сборку. CRA запрещает конфигурировать сборку, только через eject (народ бился полгода за право прикручивать SCSS), но выход есть. В RSK только форк, нужно следить за обновлениями проекта и мержить их руками — плата за полный доступ к исходному коду платформы.


Сборка и деплой


RSK (как и CRA) убивает загрузкой новой страницы браузера при начальной сборке, NEXT.js и Electrode не имеют этого недостатка.


Electrode очень медленно выполняет начальную сборку.


NEXT.js предлагает замечательный сервис для деплоя now, и он не привязан к конкретной платформе.


Работа над ошибками


В RSK ужасный вывод ошибок, кроме того неправильно работает дебаг по Source Maps, курсор смещается на пару строк ниже. В Electrode просто невозможно понять, что пошло не так, если ошибка на стороне сервера. В NEXT.js ситуация значительно лучше, и обещают доработать ещё в ближайшем будущем. CRA тут эталон, и хочется сказать отдельное спасибо за вывод в консоль сообщений ESLint в процессе работы над проектом. Кстати, RSK превратил проверки ESLint в форменную пытку, я выпилил pre-commit.


Поддержка


NEXT.js подкупает разнообразием рецептов, хотя некоторые важные вопросы пока без ответа, но динамика развития проекта обнадёживает. RSK и Electrode хорошо дополняют общий набор. На примере CSRF: вообще пока ничего в NEXT.js, в Electrode праздник, в RSK нашёл отсылку на csurf.


Вчера, 27 марта вышел NEXT.js 2.0, спустя полгода после старта. Движуха вокруг NEXT.js намного больше, в сравнении с RSK и Electrode. Если судить по звездочкам на GitHub, то NEXT.js догоняет CRA, учитывая разницу в возрасте.


За технологиями стоят конкретные персоны. Это тоже важный момент выбора. NEXT.js развивает ZEIT — основатель Guillermo Rauch был техническим директором и соучредителем LearnBoost, помните TJ Holowaychuk?


P.S. По пути выяснил


Что разделение на components и containers - это лишнее

Многие проекты руководствуются рекомендациями Presentational and Container Components, но уважаемый автор признаётся в сносках, что концепция разделения спорная, и компоненты можно смешивать. А если это так, то зачем тащить чемодан без ручки? Все компоненты проекта удобнее хранить в одной общей папке. Какие плюсы:


  • Простота навигации по файловой системе.
  • Уникальные имена компонентов проекта.
  • Импорт без боли ('../../../../../..').

Когда проект вырастет, следует дробить его на приватные npm-пакеты, инкапсулируя реализацию. Но не выращивать дерево подпапок внутри папки компонентов — развивать и поддерживать такое ощутимо сложнее. Проверено.


Дальше-больше. Попробовал CSS-Modules — отказался от BEM, но после styled-jsx не могу больше смотреть на CSS-Modules. Зачем связывать каждый рутовый элемент компонента через className, ради чего переключать контекст внимания между файлами — непонятно.


Что можно объединить actions и reducers

Достигнув просветления с общей папкой компонентов, применил ducks-pattern. Но и это ещё не всё, позвонив прямо сейчас, вы получите совершенно бесплатно возможность отказаться от констант для связывания actions-reducers, используя пакет redux-act:


// Create an action creator (description is optional)
const add = createAction('add some stuff');
const increment = createAction('increment the state');
const decrement = createAction('decrement the state');

// Create a reducer
const counterReducer = createReducer({
  [increment]: (state) => state + 1,
  [decrement]: (state) => state - 1,
  [add]: (state, payload) => state + payload,
}, 0); // <-- This is the default state

Как проще всего настроить абсолютные пути для импорта модулей в Atom

Добавить плагин js-hyperclick.


Добавить пакет в проект:


$ yarn add babel-plugin-module-resolver -D

.babelrc


{
  "presets": [
    "next/babel"
  ],
  "plugins": [
    ["module-resolver", {
      "root": [""]
    }]
  ]
}

package.json


{
  "moduleRoots": [
    ""
}

пример:


import MyComponent from 'components/MyComponent'
import MyPage from 'pages/MyPage'

Кстати, NEXT.js избавляет от бессмысленной папки src в проекте. Просто не обращал внимания раньше, но как же без неё хорошо!


Что пакет redux-form не нужен

А хотелось просто валидацию полей формы. Но 462 открытых issues как бы намекают. Я не смог себя заставить. Кода для обслуживания получается больше, чем без redux-form. И нужно думать не только о поведении формы, но как ее заставить работать с помощью этой прекрасной обертки. В морг. Невозможно угодить всем и вся. Это нужно предложить решение на каждый случай применения. Был у меня подобный печальный опыт с Meteor-овскими велосипедом AutoForm. На первый взгляд — замечательно. Описываешь конфиг и оно само тебе формы выдает! Для двух полей это работает. Но когда формы большие, да со связанными полями. Божечки. Тормозит жутко. Глючит. И опять же вынуждает тебя лезть под капот с кувалдометром. Автор забил на пулл-реквесты. Остаётся форк — вешаешь на саппорт большую кучу "универсального" кода. Оно надо?


Про Material-UI

Это давний спор с техлидом маленькой но гордой веб-студии, что никакие UI-фреймворки нам не заменят ручной труд. Понятно, что разработка интерфейсов — это хлеб с маслом. Но я убеждённый сторонник применения готовых решений. Хорошо себе представляю, какая может быть проработка деталей, имея опыт реализации проекта с чистого листа по взрослому ТЗ от гуру UI/UX-дизайна. Гайдлайн Material Design — вторая космическая скорость. Но где же его реализация? Что видел — шлак. Кроме Material-UI. Повертел-прикрутил, доработал напильником. Ну да, есть несколько недочётов. Точнее 647 открытых issues. Но пациент скорее жив чем мёртв. Демо — форма добавления/редактирования поста блога.


UPDATE


Совершил забег по кругу, и теперь могу с уверенностью сказать, что предложенный способ опционального прикручивания SSR к SPA — самый правильный (хотелка сформулирована давно).


Я же был счастлив, когда сидел на CRA. До той поры, пока не прочитал "Что взять за основу React приложения". А дальше беспрерывная борьба со сложностью окружения. Начитался issues — голова пухнет.


Все исследованные решения предлагают свои велосипеды для роутинга, которые уже можно выбросить — последняя версия React-Router реализует функционал для SSR.

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 44

    0
    что разделение на components и containers — это лишнее

    Все-таки контейнеры — это view-controllers. Они определяют что именно читать из стора, какие именно действия использовать. Это главное и важное различие.

    уважаемый автор признаётся в сносках, что концепция разделения спорная, и компоненты можно смешивать

    Мне кажется, это больше касается использования state в компонентах, и элементов, стилей в контейнерах.
      0
      RSK убивает загрузкой новой страницы браузера при начальной сборке

      это можно отключить в настройках browserSync, добавь open: false в start.js после proxy: {...}

        0

        в CRA это тоже можно отключить, package.json:


        { 
          "scripts": {
            "dev": "BROWSER=none npm start",
            "start": "react-scripts start",
            "build": "react-scripts build",
            "test": "react-scripts test --env=jsdom",
            "eject": "react-scripts eject"
          }
        }

        $ yarn dev
        +1

        Отличное продолжение, интересно почитать!


        А почему отказались от идеи повторить дизайн Хабра? На Material-UI получилась очередная поделка, которых в интернете уже куча и до них особо дела никому нет.

          0
          Отличное продолжение, интересно почитать!

          Спасибо! Очень приятно. 3 недели усердного труда.


          А почему отказались от идеи повторить дизайн Хабра?

          Чтобы прикрутить Material-UI. :) Хотелось бы получить из коробки более проработанные элементы формы. Планируется стилевая мимикрия, в зависимости от родительского сайта, к которому будет подключаться блог. По этому вопросу всё будет хорошо.

          0
          По-моему, MDL (material-design-light) и более зрелый. Не знаю, обернуты ли компоненты для react, например для angular2 они есть точно. А вот к Electrode надо присмотреться, похоже может выручать.
            0

            MDL переродился в Material Components for the web. Компонентов меньше, чем в Material-UI. Печалит BEM внутри. Обертка для React-а слабенькая — "Doesn't use MDC JS sources". В морг.


            Потенциально интересен react-polymer, но пока не вижу в своём проекте.

              +1
              А вот к Electrode надо присмотреться, похоже может выручать.

              Сначала он мне тоже понравился. Больше того, NEXT.js произвел отрицательное впечатление. Но я уже переобулся, пощупав обе платформы. Можно попробовать перетащить:


                0
                А чем в итоге electrode не понравился, и почему самый лучший в итоге это CRA? Может вообще SSR вручную каждый раз из бойлерплейта лучше всего делать?
              0
              Как верно сказали выше — сайт на material design выглядит как мертвая поделка. Но плюсов много.

              Material-UI позволяет получить ужасный, но сразу кликабельный и рабочий прототип. Часто дизайнеру работать с прототипом быстрее и проще, т.к. понятен UX, есть и данные, и состояния.

              Думаю с нуля руками без ui фреймворка проще делать что-то совсем простое.
                +1
                Все компоненты проекта удобнее хранить в одной общей папке

                Как представлю — ужасаюсь. Я храню в одной папке всё, что связано с отдельным модулем приложения: его компоненты (как контейнеры, таки презентационные), его сторы, его гейты к апи, его стили и т. п. По сути получается, что каждый модуль такое мини-приложение, которое с остальными слабо связано.

                  0
                  Когда проект вырастит, следует дробить его на приватные npm-пакеты, инкапсулируя реализацию. Но не выращивать дерево подпапок внутри папки компонентов — развивать и поддерживать такое ощутимо сложнее. Проверено.
                    +3

                    Я вижу смысл в выделение в npm (или любой иной) пакет только того, что используется в нескольких проектах, иначе усилия по поддержке пакета не стоят выделки — плавали, знаем. И у меня нет папки компонентов, есть папки модулей приложения, сгруппированные по, прежде всего, предметной области.

                      +1

                      Глянуть бы одним глазком на конкретный пример.

                        +3

                        Примерно так:
                        src/Person — папка модуля "физлица"
                        src/Person/person.js — модель физлица
                        src/Person/PersonCard.js — карточка физлица (контейнер)
                        src/Person/PersonCardView.js — карточка физлица (тупой компонент)
                        src/Person/PersonPhoto.js — фото физлица (контйнер)
                        src/Person/PersonPhotoView.js — фото физлица (тупой компонент)
                        src/Person/person.sccc — стили
                        src/Person/tests/… тесты


                        Таких модулей несколько десятков, подавляющее большинство задач в рамках одного модуля

                          +2

                          Это называется "организация проекта по фичам".
                          И у этого подхода немало сторонников, вот для примера:
                          https://medium.com/@alexmngn/how-to-better-organize-your-react-applications-2fd3ea1920f1

                            +1

                            Обособить по модулям != устраивать иерархии из компонентов. Это ад. Проходили.

                            0

                            Если положить туда еще package.json, то получится приватный пакет, который можно дергать только через его API (по соглашению). Идея в полной изоляции внутренней реализации. А то я видел примеры, когда из одного пакета объявляют импорт напрямую в другой.


                            Пример package.json, весь фокус в опции main:


                            {
                              "name": "Person",
                              "version": "0.0.1",
                              "private": true,
                              "main": "./Person.js"
                            }
                              0

                              А разве не надо будет его переносить из src/ в node_modules, прописывать в package.json и выделять в отдельный репозиторий (хотя бы гит)?


                              Ну и не вижу особой разницы между import PersonCard from '../Person/PersonCard' и import {PersonCard} from 'person';

                                0
                                А разве не надо будет его переносить из src/ в node_modules, прописывать в package.json и выделять в отдельный репозиторий (хотя бы гит)?

                                Просто соглашение внутри проекта. Модули складываются в отдельную папку modules, но в папке модуля не нужно объявлять index.js, в котором экспортируется API, а вместо index.js — файл совпадающий с именем модуля, благодаря опции main в package.json (это в частности нужно для Find Usage в WebStorm).


                                Далее у меня настроены абсолютные пути, по соглашению можно обращаться только к имени модуля: import { PersonCard } from 'modules/person'


                                Но импорт компонента только для внутреннего использования:
                                import PersonCardView from 'modules/Person/PersonCardView' — приведёт к анархии, модуль вовсе не модуль (в смысле сокрытия реализации).

                  0
                  Что пакет redux-form не нужен

                  Только что закончил проект с реально монструозными и очень динамическими формами (на сотни полей), и просто не представляю насколько сложнее это было бы без redux-form, вообще не имею претензий к этой библиотеке и её производительности. Какие у вас были проблемы? Часто забывают сбилдиться в релиз, отключить redux-devtools и
                  immutable state invariant, эти вещи отлично помогают в разработке, но легко могут просадить производительность в сотню раз.
                    0
                    Форма
                    const PostForm = ({
                      id, flow, title, content, hubs, isTranslation, sourceAuthor, sourceLink,
                      isTutorial, searchHub, sourceFlows, sourceHubs, errors, isLoading, mainError,
                      input, save
                    }) => (
                      <div className={s.root}>
                        <div className={s.container}>
                          <h1>{!!id ? 'Редактирование публикации' : 'Хочу разместить публикацию'}</h1>
                          <form onSubmit={handleSubmit(isLoading, save)} autoComplete="off">
                            <PostFormIsTutorial {...{ isTutorial, input }} />
                            <PostFormFlow {...{ flowId: flow.id, sourceFlows, input, error: errors.flow }} />
                            <PostFormTitle {...{ title, input, error: errors.title }} />
                            <PostFormContent {...{ content, input, error: errors.content }} />
                            <PostFormSearchHub {...{ searchHub, sourceHubs, hubs, input, error: errors.searchHub }} />
                            <PostFormHubs {...{ hubs, input, error: errors.searchHub }} />
                            <PostFormIsTranslation {...{ isTranslation, input }} />
                            <PostFormSourceAuthor {...{ sourceAuthor, isTranslation, input, error: errors.sourceAuthor }} />
                            <PostFormSourceLink {...{ sourceLink, isTranslation, input, error: errors.sourceLink }} />
                            <PostFormSubmit {...{ isLoading }} />
                          </form>
                          <br/>
                          {!!mainError && <div>{mainError}</div>}
                        </div>
                      </div>
                    )

                    Валидация с зависимыми полями
                    const required = value => value ? null : 'Required'
                    const maxLength = max => value =>
                      value && value.length > max ? `Must be ${max} characters or less` : null
                    const range = (value, min, max) =>
                      value && value.length >= min && value.length <= max ? null : `Must be from ${min} to ${max} elements`
                    
                    const validators = {
                      flow: (value) => required(value.id),
                      title: (value) => required(value) || maxLength(POST_FORM_TITLE_MAX)(value),
                      content: (value) => required(value),
                      searchHub: (value, state) => {
                        const hubs = state.postForm.hubs
                        return required(!isEmpty(hubs)) || range(hubs, 1, POST_FORM_HUBS_MAX)
                      },
                      sourceAuthor: (value, state) => {
                        return state.postForm.isTranslation && required(value)
                      },
                      sourceLink: (value, state) => {
                        return state.postForm.isTranslation && required(value)
                      },
                    }

                    Обработки onInupt
                      input: ({ key, value, isValidate = false }) => (dispatch, getState) => {
                        if (isValidate) {
                          const validate = validators[key]
                          if (!!validate) {
                            const state = getState()
                            const error = validate(value, state)
                            if (state.postForm[key] !== error) {
                              dispatch(setError({ key, error }))
                            }
                          }
                        }
                        if (value !== void 0) {
                          dispatch(setField({ key, value }))
                        }
                      },

                    Обработка onSubmit
                      save: () => (dispatch, getState) => {
                        const state = getState()
                        const errors = {}
                        Object.keys(validators).forEach(key => {
                          const validate = validators[key]
                          const error = validate(state.postForm[key], state)
                          if (!!error) {
                            errors[key] = error
                          }
                        })
                        if (!isEmpty(errors)) {
                          dispatch(setErrors(errors))
                          dispatch(appActions.setMainError('Исправьте ошибки в форме'))
                          return
                        }
                        dispatch(appActions.setMainError())
                        dispatch(appActions.setLoading(true))
                        //...
                      },

                    Скрипач не нужен. :)

                      0
                      Ну так у вас простая форма, и при этом одна, тут и вправду не сильно нужен redux-form. Он нужнее там где форм много, и все они сложные.
                        +1

                        на самом деле нет.


                        Чем больше форма и проект, тем больше выскакивает косяков из-за необновления каких-то полей в модели. Вот два примера из моей практики:



                        По итогам всего этого могу сказать, что лучше держать логику обработки форм у себя в проекте. Если он действительно большой, то это будет меньше 10% вашего кода и много сил на поддержку не отнимет. Гораздо проще будет, чем подкладывать костыли под redux-form.

                          0
                          Несмотря на большую сложность, вложенность и динамичность — обошлось без костылей, и всё работает именно так как задуманно.
                            +2

                            Вам повезло, что ваши требованиями по фичам совпали с возможностями redux-form, рад за вас.


                            Однако приведенные мной ссылки — это открытые баги, а значит они есть, и когда вы с ними столкнетесь, вам придется либо форкать redux-form либо съезжать на другое решение для форм.

                              0
                              Либо делать пулл реквест, что всяко быстрее написания всего функционала с нуля. Мои PR-ы проходили довольно быстро. Правда и правки были минимальными.
                    +2

                    Material-UI ощущается как какой-то страшный монстр, не смог себя заставить с ним подружиться. Взял React-MD за основу и доволен. Вот даже демо сделал для Next.JS — https://github.com/zeit/next.js/tree/master/examples/with-react-md

                      0

                      React-MD не видел, очень даже. Компонентов больше. Но стилевая мимикрия будет сложнее.

                        0
                        React Toolbox ещё вполне себе, но требует поддержки CSS Modules в проекте, к сожалению.
                          +1

                          Есть react-toolbox-themr.
                          Добавляем себе в проект, вешаем его на postinstall и он будет собирать стили для toolbox сразу после его установки. В своем конфиге вебпака ничего хачить не надо.


                          Приятным бонусом будет еще и то, что теперь можно называть свои css-классы как угодно и не бояться что они перекроют что-то из библиотеки.

                        0
                        Не совсем понял смысл next.js. Как мне кажется один из плюсов приложений на js — было то что рендеринг отдается на откуп клиенту, и серверу нужно только через апи отдать нужные данные.
                          +2

                          Серверный рендеринг для двух вещей нужен:


                          • отдать клиенту отрендеренную страницу с нужными ему данными, а уж потом он будет рендерить полученные дополнительно через апи данные — сочетаем плюсы серверного рендеринга (быстрый показ при первом открытии приложения) и клиентского (при дальнейшей работе с приложением рендерит уже клиент, а сервер только данные по апи отдаёт/принимает)
                          • отдавать поисковикам и прочим ботам отрендеренные страницы для SEO, превьшек на ссылки и т. п.
                          0

                          Смысл серчать на redux-froms тыкая на ~450 issues в то время как bug-labeled всего лишь 23 ?!
                          Вас же реакт со своими 572 issues не остановил, а там то bug-labeled побольше будет — аж целых 37(!)


                          С таким же успехом можно было бы сравнивать по количеству звёзд на гитхабе, детский сад какой-то.


                          P.S. С redux-froms не работал

                            +1

                            Абсолютными числами оперировать некорректно. Лучше взять процент.


                            В react 572 из 3669 (15%)
                            В redux-form 465 из 1732 (26%)


                            Иными словами, четверть запросов пользователей остается без ответа. Учитывать только баги тоже некорректно, так как отсутствующие фичи это тоже проблема

                              0

                              А вообще, вы правы.


                              Осознание, что redux-form не торт пришло после месяца работы с ним. Как это предвидеть заранее — непонятно.

                                0
                                А чем не торт? Я после проекта законченного за три месяца понял что без redux-form делал бы этот проект на три месяца дольше.
                                  0

                                  Мы уже общались в этой ветке и я рассказал о незакрытых issues, под которые пришлось искать костыли.

                                    0
                                    Упс, я подумал что вы новый человек. Ну всё-же незакрытые issues под довольно специфичные действия — не такой уж большой показатель. Вы всегда можете заинвестигейтить в чём дело и сделать свой pull request, библиотека всё-такие бесплатная и с открытым исходным кодом.
                              0
                              Добавил опровержение. :)

                              Only users with full accounts can post comments. Log in, please.