Структура приложения

Привет! В данном посте я хотел бы описать получившуюся структуру исходников блога, а также описать причины, по которым она вышла именно такой.

Общая структура

На данный момент проект имеет следующую структуру:


Давайте пройдемся по порядку.

Директории dist и ssr–dist — это собранные версии блога: в первом случае это клиентская версия, во втором — версия для серверного рендеринга (server-side renderingssr), т.е. для запуска посредством Node.js. Думаю, все понимают почему нужна первая версия, но почему появилась необходимость во второй? При разработке я использовал новые возможности javascript'а из ES-2016, а также object spread operator, который находится пока лишь в виде предложения в стандарт, но сам по себе весьма удобен, а также естественно JSX, который тоже необходимо прекомпилировать. Отсюда и вытекает необходимость такой-же прекомпиляции кода для Node.js.

node_modules — думаю в представлении не нуждается, сюда устанавливаются все сторонние зависимости приложения.

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

.babelrc — это конфиг для компилятора Babel, он представляет из себя следующее:

{
    "presets": [
        "es2015",
        "react"
    ],
    "plugins": ["transform-object-rest-spread"]
}

Т.е. я использую 2 набора плагинов: es2015 — набор для компиляции ES2015 и react — набор для компиляции JSX, а также плагин для упомянутого выше rest-spread. Вообще начинал разработку я без последнего, т.к. хотелось писать код соответствующий стандарту, что теоретически позволило бы отказаться от прекомпиляции после его повсеместного внедрения в браузерах, но слишком уж он удобен, особенно при проксировании части свойств в react-компонентах описанных в JSX. Сравните сами:

// Без rest-spread
const props = Object.assign({}, restProps, { title, text });
return React.createElement(Post, props);

// С rest-spread
return <Post { …restProps, title, text } />

.editorconfig — это INI-конфиг для редактора кода, позволяющий придерживаться единого стандарта форматирования кода в проекте автоматически, удобен при командной разработке. В принципе у меня необходимости в его использовании не было, т.к. я работал один, но это уже скорее дело привычки. Если вы не знакомы с ним, то подробнее о нем можно узнать на сайте проекта, либо же в этой статье на хабре.

.eslintrc. Как нетрудно догадаться это конфигурационный файл, предназначающийся линтеру ESLint. Его я использовал впервые (до этого работал с jshint), и могу с уверенностью рекомендовать его всем. Он очень гибок и позволяет линтить не только JS, но и jsx к примеру. Причем правила линтинга могут представлять из себя достаточно интересные случаи: от обязательного описания propTypes компонента, до сортировки в правильном порядке его методов. Мой конфиг:

{
  "extends": "airbnb",
  "globals": {
    "__DEVELOPMENT__": true,
    "__DEVTOOLS__": true,
    "__BROWSER__": true
  },
  "parserOptions": {
    "ecmaVersion": 6,
    "ecmaFeatures": {
      "jsx": true,
      "experimentalObjectRestSpread": true
    },
    "sourceType": "module"
  },
  "env": {
    "browser": true,
    "node": true
  },
  "rules": {
    "comma-dangle": [2, "never"],
    "no-trailing-spaces": [1, { "skipBlankLines": true }],
    "no-use-before-define": [2, {"functions": false}],
    "no-underscore-dangle": [0],
    "no-shadow": [0],
    "indent": [2, 4, {
      "SwitchCase": 1,
      "VariableDeclarator": 1
    }],
    "max-len": [2, 120],
    "react/jsx-indent": [2, 4],
    "react/jsx-indent-props": [2, 4],
    "react/jsx-curly-spacing": [2, "always"],
    "object-curly-spacing": [2, "always", {
      "objectsInObjects": false,
      "arraysInObjects": false
    }],
    "consistent-return": [0]
  }
}

Как видно я использовал популярный конфиг от Airbnb, но с некоторыми коррективами по своему вкусу.

  • В секции globals перечислены мои глобальные переменные, чтобы ESLint не выдавал ошибок при их использовании.
  • В parserOptions указаны опции для парсера: используем ES2015, JSX и rest-spread.
  • sourceType = module указывает на то, что я использую ES2015 модули
  • env — в каких окружениях будет использоваться наш код, это опять-таки необходимо для корректной работы с глобальными переменными этих сред.

.gitignore содержит:

node_module
.sass-cache
dist
ssr-dist
logs
npm-debug.log

Исключаем из репозитория npm-зависимости, кэш Sass, собранные версии приложения, папку логов (SSR) и npm-debug.log. Также в глобальном .gitignore у меня находится .idea, т.к. я использую Webstorm в разработке, а он создает одноименную папку в проекте.

ecosystem.json — это файл с конфигурацией для развертывания приложения с помощью PM2. Данный механизм я рассмотрю в отдельной статье.

index.js — главная точка входа приложения.

nginx.conf — конфигурация для nginx. Я стараюсь хранить зависимые конфиги прямо в самом репозитории проекта, таким образом их проще найти при необходимости.

npm-shrinkwrap.json — зафиксированные версии установленных зависимостей, необходимо для того чтобы быть уверенным что на production-сервере будут установлены точно такие же версии, как и в dev-окружении. Подробнее тут.

package.json — всем знакомый файл. В нем описаны зависимости приложения, а также dev-зависимости, которые необходимы при сборке. Так же в этом проекте я активно использовал секцию scripts для управления сборкой и развертыванием.

Readme.md. такой файл должен быть в каждом проекте. Это вводная часть как для новичков, так и для самого себя, когда ты возвращаешься к проекту через продолжительное время. В нем как минимум должны быть описаны процессы запуска dev-режима, сборка проекта, и его развертывание.

webpack.development.js и webpack.production.js — конфигурационные файлы для сборки в различных окружениях с помощью Webpack. По их названиям несложно догадаться в каких =).

Фух, с конфигурационными файлами вроде закончил, перейдем к исходному коду приложения:

Структура исходного кода приложения


Т.к. я использовал Redux в качестве flux-реализации, то первой у нас идет папка actions, в которой хранятся все action'ы приложения.


В директории assets находятся favicon'ы приложения (их много, под все актуальные платформы), шрифты, и вспомогательный код стилей.
fonts.css содержит @font-face декларации, по которым с помощью fontfaceobserver асинхронно загружаются сами шрифты.
Как видно по скриншоту для написания стилей я использовал Sass, а точнее Scss-нотацию. Также использовалась BEM-нотация именования компонентов. при этом я намеренно отказался от использования inline-стилей в React, так как не готов отказываться от такого важного для меня аспекта как кеширование.

Компоненты приложения разбиты согласно документации redux и этой статье Дэна Абрамова(создателя Redux) на презентационные и контейнеры и хранятся они соответственно в директориях components и containers. Данное разделение необходимо для уменьшения связности компонентов и возможности их дальнейшего реиспользования в других проектах. В данном случае речь конечно же о презентационных компонентах.


Каждый презентационный компонент состоит из папки с именем соответствующим названию компонента и файлом index.jsx который содержит код этого компонента, таким образом его получается удобно использовать в import'ах. Компонент также обычно содержит файл стилей (он же презентационный) и остальные зависимые файлы, например, спрайт иконок в svg.
Компонент-контейнер выполняет связующую роль – он связывает компонент с нужной частью stor'a (через функцию connect из react-redux модуля), прокидывает необходимые презентационным компонентам action’ы и callback’и.


В constants хранятся константы для action'ов, настройки приложения, например: настройки disqus'а, ID Яндекс-метрики, количество постов на странице, и т.д. Так же в зависимости от текущего окружения в константу ENV записываются значения из env/{окружение}:

const envConfig = __DEVELOPMENT__ ? 'development' : 'production';

export const ENV = require(`./env/${envConfig}`);

В них может хранится к примеру, URL к API приложения, который различен для dev и prod окружений.
Файл models.js содержит декларации моделей для динамической генерации REST-админки.

В lib находится вспомогательный и/или общий код, который не подпал ни под одну другую категорию.

modules содержит в себе модули, которые изначально разрабатывались как независимые и возможно будут вынесены на github как отдельные проекты. Их структура в целом совпадает со структурой папки src. Почему я их не вынес сразу в отдельный проект, и не слинковал их с помощью npm? Потому что для меня так удобнее: нет отдельно висящего процесса сборки, отдельного инстанса редактора для этого модуля, не теряется время при переключении между проектами. Когда же API модуля стабилизировалось — его можно выносить в отдельный проект.

Ну и в последней директории reducers расположены Redux reducer'ы. При этом selector'ы хранятся рядом с reducer'ами для упрощения дальнейшего изменения/рефакторинга приложения. Пример простейшего reducer'а:

import {
    FETCH_SEARCH_RESULTS,
    FETCH_SEARCH_RESULTS_SUCCESS,
    FETCH_SEARCH_RESULTS_FAILURE
} from '../constants/actions';

export default function search(searchState = {
    searchRequest: '',
    offset: 0,
    total: 0,
    results: [],
    isFetching: false
}, action) {
    switch (action.type) {
        case FETCH_SEARCH_RESULTS:
            return {
                ...searchState,
                searchRequest: action.searchRequest,
                offset: action.offset,
                isFetching: true
            };
        case FETCH_SEARCH_RESULTS_SUCCESS:
            return {
                ...searchState,
                results: action.results,
                total: action.total,
                isFetching: false
            };
        case FETCH_SEARCH_RESULTS_FAILURE:
            return {
                ...searchState,
                isFetching: false,
                error: action.error
            };
        default:
            return searchState;
    }
}

export function getSearch(state) {
    return state.search;
}

Как Вы можете видеть тут используется принцип неизменяемости данных: при каждом вызове action'a возвращается новый объект, это позволяется использовать так называемое поверхностное сравнение props'ов для выявления изменений в shouldComponentUpdate, что ускоряет работу приложения в целом. Помните об этом!

index.html содержит начальный html-код страницы. Используется как при серверном рендеринге, так и при клиентском. Для этого в нем есть html-комментарии: <!-- title -->, <!-- meta -->, <!-- appState -->, <!-- svgIcons -->, <!-- appHtml --> в нужных местах, которые игнорируются при клиентском рендеринге, но используются для подстановки подготовленных данных при серверном.

В routes.jsx находится конфигурацию роутинга приложения. Для роутинга используется react-router и react-router-redux. Последний занимается тем что пропускает все изменения URL'а через свой action, что отображается в Redux stor'е приложения.

index.js и server.js представляют собой соответственно клиентскую и серверную инициализации приложения. При этом клиентский код естественно более простой. В server.js я использую express.js для перехвата всех пользовательских запросов, подготовки необходимой разметки и данных, и отдачи готовой страницы пользователю. Но об этом в одной из следующих статей.

Заключение

Надеюсь вам удалось уследить за моей мыслью, и понять чем я руководствовался, разбивая свое приложение на вышеперечисленную структуру.
В следующем же посте я рассмотрю процесс сборки приложения с помощью Webpack и npm-scripts.