Single page apps in depth 1 (перевод)

Почему мы так хотим писать одностроничные приложения? Основная причина в том, что они позволяют нам предоставить пользователю более близкий к привычному интерфейс.

Этого сложно добиться другими способами. Поддерживать сложное взаимодействие между многими компонентами страницы значит, что эти компоненты имеют множество промежуточных состояний (например, меню открыто, элемент Y выбран, элемент X выбран, элемент кликнут). Отрисовывать эти состояния на сервере очень затратно --- малые изменения представления сложно уместить в url.

Одностраничные приложения (в дальнейшем SPA) известны своей возможностью изменить любую часть интерфейса без необходимости отправлять сервер в кругосветное путешествие, чтобы выдать HTML. Это достигается через разделение данных от их представления путем создания слоя Модели для работы с данными и слоя Представления, который считывает данные.

Большинство проектов начинаются с высоких амбиций и слабого понимания насущных проблем. Реализация, как правило, опережает наше понимание. Можно написать код без полного понимания проблемы; просто этот код более сложен, чем если бы мы понимали проблему.

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

Архитектуры, которые могут быть использованы в SPA, являются результатом подобного процесса --- там, где вы раньше использовали jquery, теперь главенствует код, который использует стандартные механизмы.

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

Чтобы писать поддерживаемый код, нам нужно держать сущности простыми. Это константа: проще добавить сложности чтобы решить проблему, которая того не стоит, но еще проще решить проблему способом, который не уменьшает сложности. Пространства имен --- пример последнего.

Держа вышесказанное в голове, рассмотрим структуру современного веб-приложения с точки зрения трех различных перспектив:

  • Архитектуры: какие (концептуальные) части составляют наше приложение? Как различные части общаются между собой? Как они зависят друг от друга?

  • Упаковка asset'ов (наборов css и js файлов): как наше приложение разбито на файлы, а файлы на логические модули? Как эти модули собираются и загружаются в браузер? Как эти модули могут быть загружены для юнит-тестирования?

  • Состояние в момент работы: что находится в памяти на момент загрузки приложения? Как мы производим изменения между состояниями и обеспечиваем прозрачность текущего состояния для решения проблем?

Архитектура современного веб-приложения

Современное веб-приложение в основном выглядит так:

Более конкретно:

DOM только для записи. Ни состояния, ни данные не читается из DOM. Приложение выводит HTML и оперирует элементами, но ничего не читает из DOM.Хранить и быстро управлять разрозненными данными в DOM становится все сложнее, гораздо проще иметь одно место, где живут данные, и отрисовывать UI на их основе. В частности это полезно когда одни и те же данные отражены в разных участках UI.

Модели - единственный источник истины. Вместо хранения данных в DOM, или случайном объекте, у нас есть набор Моделей в оперативной памяти, которые отражают все состояния/данные приложения.

Представления наблюдают за изменениями модели. Нам нужно чтобы Представления отражали содержимое Моделей. Когда несколько Представлений зависят от одной Модели (к примеру, нам нужно перерисовать Предсавления при изменении в Модели), мы не хотим в ручную следить за каждым зависимым Представлением. Вместо этого есть система событий, через которую Представления получают оповещение об изменении от Модели и они перерисовываются сами.

Разделенные модули, которые представляют небольшие внешние интерфейсы. Вместо того, чтобы делать все глобальным, мы должны постараться сделать небольшие подсистемы, которые невзаимозависимы. Зависимости делают код сложным и нетестируемым. Небольшие внешние интерфейсы делают рефакторинг проще, ибо при изменении приватного кода, интерфейсы остануться теми же.

Минимизировать DOM-зависимый код. Почему? Любой код, который зависит от DOM, должен быть протестирован на кроссбраузерность. Путем написания кода, который изолирует эти грязные части, гораздо меньшая часть кода должна будет протестирована на кроссбраузерность. Все несовместимости таким образом будут более управляемыми. Несовместимости находятся в реализациях DOM, а не Javascript, поэтому имеет смысл минимизировать и изолировать DOM-зависимый код.

Смерть всем Контроллерам

Есть причина тому, что я не употребил слово "контроллер" в диаграмме выше. Мне не нравится это слово, поэтому вы больше не увидите его в книге. Моя причина проста: это просто слово-заглушка, которое мы принесли в мир SPA после написания огромного количества серверного "MVC".

Многие фреймворки для SPA используют термин "контроллер", но я рассматриваю это как "поместите связующий код сюда". Как видно из определения:

"Контроллеры имеют дело с добавлением и реакцией на события DOM, парсят шаблоны и содержат Представления и Модели синхронизированными. ".

Как так? Может нам стоит рассматривать эти проблемы раздельно?

SPA требуется лучшее слово, потому что у них более сложная система промежуточных состояний, чем в серверных приложениях:

  • здесь и события DOM, которые слегка меняют Представления
  • здесь и события Модели, которые запускаются когда меняется значение
  • здесь и состояние приложения, которое меняет местами Представления
  • здесь и изменения глобальных состояний, вроде ухода в оффлайн в приложении реального времени
  • здесь и отложенные результаты AJAX, которые могут вернуться от бэкенда в любое время

Все эти вещи должны быть как-то склеены вместе и слово "контроллер" слабо описывает координатор для всей этой деятельности.

Ясно, что нам нужна Модель для хранения данных и Представление, чтобы иметь дело с интерфейсом, но уровень, который их склеивает содержит в себе несколько независимых проблем. Знание того, что у фреймворка есть контроллер ничего не говорит о том, как он решает эти проблемы. Поэтому я прошу людей использовать более специфичные термины.

Поэтому в этой книге нет главы про контроллеры --- как бы то ни было, я охватываю каждую их этих проблем когда говорю о Представлении и Модели. Каждое решение имеет свое название, например, связывание событий, события изменений, инициализаторы и тд.

Пакетирование (или, если подробнее, упаковка кода для браузера)

Упаковка кода --- это когда вы берете JS код приложения и создаете один или несколько файлов (пакетов), которые могут быть загружены в браузер через тег script.

Никто не подчеркивает насколько важно делать это правильно. Это не о том, чтобы ускорить время загрузки, это о том, чтобы сделать ваше приложение модульным и быть уверенным, что оно не превратится в нетестируемое месиво. Люди также заботятся о производительности, но это опционально.

То, как хорошо вы разбили код на модули, сильно влияет на то, насколько тестируем и поддерживаем ваш код будет в будущем. Пакетирование как раз об этом. Сравните два подхода:

Все вперемешку (без модулей)

  • Каждый кусок кода глобален по умолчанию
  • Имена глобальны
  • Полностью доступное пространство имен
  • Порядок загрузки имеет значение
  • Скрытые зависимости на что-то глобальное
  • Файлы и модули не имеют важных соединений
  • Запускается только в браузере, потому что зависимости не изолированны

Пакеты и модули

  • Пакеты представляют единый публичный интерфейс
  • Имена локальны в рамках пакета
  • Детали реализации скрыты от внешнего мира
  • Порядок загрузки не имеет значения
  • Явные зависимости
  • Каждый файл представляет один модуль
  • Запускается из командной строки, не нужен браузер

По умолчанию ("брось каждый JS файл в глобальную область видимости и надейся что результат сработает") ужасен, потому что делает юнит-тестирование и, как следствие, рефакторинг, сложными. Вот почему плохая модульность ведет к зависимостям от глобального состояния и глобальных имен и делает тестирование невыносимым.

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

Состояние во время работы

Третья перспектива --- SPA в момент загрузки. Это о том, как приложение выглядит когда оно загружено в ваш браузер --- о том, что содержат переменные и какие шаги необходимы между сменами Представлений.

Здесь есть три интересных взаимосвязи:

URL <-> состояние У SPA шизофренические отношения с URL. С одной стороны, SPA предоставляет богатый набор функций, что подразумевает большой набор состояний Представления, который не поместится в URL. Но с другой стороны, мы бы хотели поместить URL в закладки чтобы вернуться к тому же месту.

Для реализации закладок, нам каким-то образом нужно уменьшить уровень детализации в URL. Если у страницы есть одна основная задача, отразим только эту необходимую часть в URL. Второстепенная активность (чат в интерфейсе почты) при перезагрузке вернется в состояние по умолчанию.

Определение <-> Инициализация Некоторые люди их все еще совмещают, а это плохая идея. Компоненты, готовые к повторному использованию, должны быть определены без инициализации или активации. Но как же мы тогда обеспечим инициализацию различных состояний приложения?

На мой взгляд, здесь три основных подхода:

  • небольшая функция в каждом модуле, которая принимает, к примеру, ID, и возвращает необходимое Представление или объект;
  • глобальный загрузочный файл с роутером, который загружает правильное состояние из глобального набора;
  • обернуть все в синтаксический сахар, который сделает инстанцирование невидимым.

Мне нравится первый; второй можно найти в приложениях, где все изначально было запутано, а потом выросло. Третий можно найти в некоторых фреймворках в реализации уровня Представлений.

Мне нравится первый, потому что я нахожу инстанцирование отвратительной и хочу спрятать ее в один файл (по подсистемам, инстанцирование должно быть локальным, а не глобальным, об этом позже). Голые данные просты, как и определения. Именно тогда, когда у нас есть много взаимозависимых и/или сложно локализируемых инстанцирований, работать становится сложнее; трудно объяснить и, в общем, неприятно.

Вторая выгода первого подхода --- нет необходимости грузить все приложение при перезагрузке страницы, вы можете тестировать отдельные части без загрузки всего приложения. Так же у вас будет гибкость в загрузке приложения после загрузки начального Представления. Это значит, что время загрузки приложения не будет расти пропорционально количеству модулей.

Элементы HTML <-> объекты Представлений и События HTML <-> изменения Представлений Наконец, есть вопрос --- насколько прозрачно мы можем видеть загрузку фреймворка, который используем. Я не видел фреймворков, которые отвечают на него открыто (хотя всегда есть разные трюки): когда я запускаю приложение, как я могу сказать что происходит когда я выделяю отдельный HTML элемент? Или когда я смотрю на HTML-элемент, как я могу сказать что произойдет когда я кликну на нем или что-то другое?

Простая реализация в основном лучше именно потому, что расстояние между элементом/событием до объекта/обработчика события гораздо короче. Я надеюсь, что фреймворки обратят больше внимания подниманию этой информации на поверхность.

Это только начало

Итак, у нас есть это: три перспективы --- одна с точки зрения архитектуры, другая с точки зрения файловой системы и, наконец, с точки зрения браузера.

Яндекс.Метрика