Одностраничные веб-приложения (SPA) с библиотекой KnockoutJS. Одностраничные приложения Примеры известных single page application

16.02.2024

Термин «одностраничное приложение» (или SPA) обычно используется для описания приложений, которые были созданы для Интернета. Эти приложения доступны через веб-браузер, как и другие веб-сайты, но предлагают более динамичные взаимодействия, напоминающие родные мобильные и настольные приложения.

Самая заметная разница между обычным сайтом и SPA – это уменьшение количества обновлений страницы. У СПА более тяжелое использование AJAX – способ общения с серверными серверами без полного обновления страницы – для загрузки данных в наше приложение. В результате процесс рендеринга (построения) страниц происходит в основном на стороне клиента с помощью JavaScript.

SPA, Single Page Application, Одностраничное приложение

В то время как строительство SPA-приложений является модным и считается современной практикой развития, важно знать о его недостатках, в том числе:

  • Браузер делает большую часть тяжелого подъема, что означает, что производительность может быть проблемой – особенно на менее способных мобильных устройствах.
  • Сложность поисковой оптимизации (SEO), сложность предоставления вашего контента поисковым системам и сайтам социальных сетей, которые предоставляют предварительный просмотр ссылок.
Смягчение недостатков с помощью рендеринга на стороне сервера

Большинство современных фреймворков JavaScript работают над тем, как обрабатывать серверный рендеринг SPA, т.е. пользователь получит полностью заполненную страницу, когда SPA будет загружен впервые, вместо того, чтобы, например, увидеть индикатор загрузки.

Рендеринг на стороне сервера может облегчить некоторые из блогов, которые браузеры должны выполнять при рендеринге страниц, а также поможет решить проблему SEO и доступности контента.

Популярные JavaScript-фреймворки и библиотеки для создания SPA

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

Существует множество открытых исходных JavaScript-фреймворков, которые помогают создавать SPA-приложения, такие как.

Продукты и технологии:

Single-Page Applications (SPA), ASP.NET Web API, Knockout.js, Ember.js, AJAX и HTML5

В статье рассматриваются:

  • создание уровня сервисов и веб-клиента AJAX для приложения-примера;
  • шаблоны MVC и MVVM;
  • связывание с данными;
  • создание веб-клиента с применением Knockout.js;
  • создание веб-клиента с применением Ember.js.

Одностраничные приложения (Single-Page Applications, SPA) - это веб-приложения, которые загружают одну HTML-страницу и динамически обновляют ее при взаимодействии с пользователем.

SPA используют AJAX и HTML5 для создания гибких и адаптивных веб-приложений без постоянных перезагрузок страницы. Однако это означает, что большая часть работы возлагается на клиентскую сторону, а именно на JavaScript-код. Разработчику для традиционной ASP.NET может быть трудно совершить такой кульбит. К счастью, существует множество JavaScript-инфраструктур с открытым исходным кодом, которые облегчают создание SPA.

В этой статье я пошагово пройду процесс создания простого SPA-приложения. Попутно вы ознакомитесь с некоторыми фундаментальными концепциями создания SPA, в том числе с шаблонами Model-View-Controller (MVC) и Model-View-ViewModel (MVVM), связыванием с данными и маршрутизацией (routing).

О приложении-примере

Я создал приложение-пример для операций с простой базой данных по фильмам (рис. 1 ). В крайнем слева столбце страницы отображается список жанров. Выбор жанра приводит к появлению списка соответствующих фильмов. Кнопка Edit рядом с записью позволяет изменять эту запись. После редактирования можно щелкнуть кнопку Save для передачи обновления на сервер или кнопку Cancel для отмены изменений.

Рис. 1. SPA-приложение для базы данных по фильмам

Я создал две версии этого приложения: одна из них использует библиотеку Knockout.js, а другая - библиотеку Ember.js. Эти две библиотеки основаны на разных подходах, поэтому будет весьма поучительно сравнить их. В обоих случаях клиентское приложение не требовало более 150 строк JavaScript-кода. На серверной стороне я задействовал ASP.NET Web API, чтобы обслуживать JSON для клиента. Исходный код обеих версий вы найдете на github.com/MikeWasson/MoviesSPA .

(Примечание Я создавал приложение, используя RC-версию Visual Studio 2013. В RTM-версии некоторые вещи могли измениться, но они не должны повлиять на код.)

Обзор

В традиционном веб-приложении при каждом вызове сервера тот осуществляет рендеринг новой HTML-страницы. Это вызывает обновление страницы в браузере. Если вы когда-нибудь писали приложение Web Forms или PHP, этот жизненный цикл страниц должен быть знаком вам.

В SPA после загрузки первой страницы все взаимодействие с сервером происходит через AJAX-вызовы. Эти AJAX-вызовы возвращают данные (не разметку) - обычно в формате JSON. Приложение использует JSON-данные для динамического обновления страницы без ее перезагрузки. Рис. 2 иллюстрирует разницу между этими двумя подходами.


Рис. 2. Сравнение традиционного жизненного цикла страницы с жизненным циклом в SPA

Одно из преимуществ SPA очевидно: приложения более гибкие и адаптивные, свободные от рваного эффекта перезагрузки страницы и ее рендеринга заново. Другое преимущество может оказаться менее очевидным и касается того, как вы проектируете архитектуру веб-приложения. Отправка данных приложения как JSON обеспечивает разделение между презентационной частью (HTML-разметкой) и прикладной логикой (AJAX-запросы плюс JSON-ответы).

Это разделение упрощает проектирование и развитие каждого уровня. В SPA-приложении с тщательно продуманной архитектурой можно изменять HTML-разметку, не касаясь кода, который реализует прикладную логику (по крайней мере, в идеале). Вы увидите это на практике, когда мы будем обсуждать связывание с данными.

В чистом SPA все UI-взаимодействие происходит на клиентской стороне через JavaScript и CSS. После начальной загрузки страницы сервер действует исключительно как уровень сервисов. Клиенту нужно просто знать, какие HTTP-запросы он должен посылать. Ему не важно, как сервер реализует свою часть.

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

Создание проекта в Visual Studio

В Visual Studio 2013 есть один тип проекта ASP.NET Web Application. Мастер этого проекта позволяет выбрать ASP.NET-компоненты, которые будут включены в проект. Я начал с шаблона Empty, а затем добавил в проект ASP.NET Web API, установив флажок Web API в разделе Add folders and core references for, как показано на рис. 3 .


Рис. 3. Создание нового ASP.NET-проекта в Visual Studio 2013

В новом проекте есть все библиотеки, необходимые для Web API, а также кое-какой конфигурационный код Web API. Я не вводил никаких зависимостей от Web Forms или ASP.NET MVC.

Обратите внимание на рис. 3 , что Visual Studio 2013 включает шаблон Single Page Application. Этот шаблон устанавливает скелет SPA-приложения, основанный на Knockout.js. Он поддерживает вход с применением базы данных с информацией о членстве в группах или с помощью внешнего провайдера аутентификации. Я не стал использовать этот шаблон в своем приложении, потому что хотел показать более простой пример с нуля. Шаблон SPA - отличный ресурс, особенно если вам нужно добавить аутентификацию в приложение.

Создание уровня сервисов

Я использовал ASP.NET Web API, чтобы создать простой REST API для приложения. Не буду здесь вдаваться в детали Web API - подробности вы можете прочитать по ссылке asp.net/web-api.

Сначала я создал класс Movie, представляющий фильм. Этот класс делает две вещи:

  • сообщает Entity Framework (EF), как создавать таблицы базы данных для хранения информации о фильмах;
  • сообщает Web API, как форматировать полезные данные JSON.

Вы не обязаны использовать одну модель для обеих целей. Например, вам может понадобиться, чтобы схема базы данных отличалась от полезных данных JSON. В этом приложении я ничего не усложняю:

  • namespace MoviesSPA.Models
  • public class Movie
  • public int ID { get; set; }
  • public string Title { get; set; }
  • public int Year { get; set; }
  • public string Genre { get; set; }
  • public string Rating { get; set; }
  • Затем я воспользовался технологией scaffolding в Visual Studio для создания контроллера Web API, который задействует EF в качестве уровня данных. Чтобы применить эту технологию, щелкните правой кнопкой мыши папку Controllers в Solution Explorer и выберите Add / New Scaffolded Item. В мастере Add Scaffold укажите Web API 2 Controller with actions, using Entity Framework, как показано на рис. 4 .


    Рис. 4. Добавление контроллера Web API

    На рис. 5 приведен мастер Add Controller. Я присвоил контроллеру имя MoviesController. Имя имеет значение, так как URI для REST API основываются на имени контроллера. Я также установил флажок Use async controller actions, чтобы задействовать преимущества новой функциональности async в EF 6. Я выбрал класс Movie в качестве модели и указал New data context, чтобы создать новый контекст данных EF.


    Рис. 5. Мастер Add Controller

    Мастер добавляет два файла:

    • MoviesController.cs - определяет контроллер Web API, который реализует REST API для приложения;
    • MovieSPAContext.cs - это в основном склеивающий слой EF, который предоставляет методы для запроса нижележащей базы данных.

    В табл. 1 показан REST API по умолчанию, создаваемый технологией scaffolding.

    Табл. 1. REST API по умолчанию, созданный технологией scaffolding из Web API

    Значения в фигурных скобках являются заменителями для подстановки. Например, чтобы получить фильм с идентификатором, равным 5, URI должен выглядеть так: /api/movies/5.

    Я расширил этот API, добавив метод, который находит все фильмы указанного жанра:

    Клиент указывает жанр в строке запроса URI. Например, чтобы получить все фильмы жанра Drama, клиент посылает GET-запрос на /api/movies?genre=drama. Web API автоматически связывает параметр запроса с параметром genre в методе GetMoviesByGenre.

    Создание веб-клиента

    До сих пор я просто создавал REST API. Если вы отправите GET-запрос на /api/movies?genre=drama, исходный HTTP-ответ будет выглядеть так:

    HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Date: Tue, 10 Sep 2013 15:20:59 GMT Content-Length: 240 [{"ID":5,"Title":"Forgotten Doors","Year":2009,"Genre":"Drama","Rating":"R"}, {"ID":6,"Title":"Blue Moon June","Year":1998,"Genre":"Drama","Rating":"PG-13"},{"ID":7,"Title":"The Edge of the Sun","Year":1977,"Genre":"Drama","Rating":"PG-13"}]

    Теперь мне нужно написать клиентское приложение, которое делает с этим что-то осмысленное. Базовый рабочий процесс такой:

    • UI инициирует AJAX-запрос;
    • обновляем HTML для отображения полезных данных ответа;
    • обрабатываем AJAX-ошибки.

    Вы могли закодировать все это вручную. Например, вот некоторый jQuery-код, который создает список названий фильмов:

  • $.getJSON(url)
  • .done(function (data) {
  • // При успехе data содержит список фильмов
  • var ul = $("")
  • $.each(data, function (key, item) {
  • // Добавляем элемент в список
  • $("
  • ", { text: item.Title }).appendTo(ul);
  • $("#movies").html(ul);
  • В этом коде есть кое-какие проблемы. Он смешивает прикладную логику с презентационной и тесно связан с вашим HTML. Кроме того, писать его весьма утомительно. Вместо того чтобы сосредоточиться на приложении, вы тратите время на написание обработчиков событий и кода, манипулирующего DOM.

    Решение заключается в том, чтобы использовать JavaScript-инфраструктуру. К счастью, их выбор довольно велик, и эти инфраструктуры имеют открытый исходный код. К некоторым из более популярных инфраструктур относятся Backbone, Angular, Ember, Knockout, Dojo и JavaScriptMVC. Большинство использует вариации шаблонов MVC или MVVM, поэтому будет полезно вкратце рассмотреть эти шаблоны.

    Шаблоны MVC и MVVM

    Корни шаблона MVC уходят в 80-е годы прошлого века и связаны с ранними графическими UI. Цель MVC - разбиение кода на три уровня со своими обязанностями (рис. 6 ). Вот что они делают:

    • модель представляет данные и бизнес-логику предметной области;
    • представление отображает модель;
    • контроллер принимает пользовательский ввод и обновляет модель.

    Рис. 6. Шаблон MVC

    Более современная вариация MVC - шаблон MVVM (рис. 7 ). В шаблоне MVVM:

    • модель по-прежнему представляет данные предметной области;
    • модель представления - это абстрактное отражение представления;
    • представление отображает модель представления и посылает пользовательский ввод модели представления.

    Рис. 7. Шаблон MVVM

    View Model View Model

    В JavaScript-инфраструктуре MVVM представлением является разметка, а моделью представления - код.

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

    Создание веб-клиента с применением Knockout.js

    Для первой версии своего приложения я использовал библиотеку Knockout.js. Knockout следует шаблону MVVM, соединяя представление и модель представления через связывание с данными.

    Чтобы создать привязки данных, вы добавляете к HTML-элементам специальный атрибут data-binding. Например, следующая разметка связывает элемент span со свойством genre в модели представления. Всякий раз, когда изменяется значение genre, Knockout автоматически обновляет HTML:

  • Привязки также могут работать в другом направлении, скажем, если пользователь вводит текст в поле, Knockout обновляет соответствующее свойство в модели представления.

    Удобно, что связывание с данными осуществляется декларативно. Вам не требуется подключать модель представления к элементам HTML-страницы. Просто добавьте атрибут data-binding, и Knockout сделает остальное.

    Я начал с создания HTML-страницы с базовой разметкой без связывания с данными, как показано на рис. 8 .

    Рис. 8. Начальная HTML-разметка

  • Movies SPA
  • TitleYearRating
  • No records found.

  • (Примечание Я использовал библиотеку Bootstrap для оформления внешнего вида приложения, поэтому в настоящем приложении уйма дополнительных элементов и CSS-классов, управляющих форматированием. Для ясности я убрал все это из кода.)

    Создание модели представления

    Наблюдаемые объекты (observables) занимают центральное место в системе связывания с данными в Knockout. Наблюдаемым является объект, который хранит какое-то значение и может уведомлять подписчиков об изменении этого значения. Следующий код преобразует JSON-представление фильма в эквивалентный объект с наблюдаемыми полями:

  • function movie(data) {
  • var self = this;
  • data = data // {};
  • // Данные из модели
  • self.ID = data.ID;
  • self.Title = ko.observable(data.Title);
  • self.Year = ko.observable(data.Year);
  • self.Rating = ko.observable(data.Rating);
  • self.Genre = ko.observable(data.Genre);
  • На рис. 9 показана начальная реализация модели представления. Эта версия поддерживает только получение списка фильмов. Средства редактирования я добавлю позже. Модель представления содержит наблюдаемые свойства для списка фильмов, строку ошибки и текущий жанр.

    Рис. 9. Модель представления

  • var ViewModel = function () {
  • var self = this;
  • // Наблюдаемые свойства модели представления
  • self.movies = ko.observableArray();
  • self.error = ko.observable();
  • // Жанр, просматриваемый пользователем в данный момент
  • self.genre = ko.observable();
  • // Доступные жанры
  • self.genres = ["Action", "Drama", "Fantasy", "Horror", "Romantic Comedy"];
  • // Добавляем JSON-массив объектов movie
  • // в модель представления
  • function addMovies(data) {
  • var mapped = ko.utils.arrayMap(data, function (item) {
  • return new movie(item);
  • self.movies(mapped);
  • // Обратный вызов для получения ошибок от сервера
  • function onError(error) {
  • self.error("Error: " + error.status + " " + error.statusText);
  • // Получаем список фильмов по жанру
  • // и обновляем модель представления
  • self.getByGenre = function (genre) {
  • self.error(""); // очистка ошибки
  • self.genre(genre);
  • // Инициализируем приложение, получая первый жанр
  • self.getByGenre(self.genres);
  • // Создаем экземпляр модели представления и передаем в Knockout
  • ko.applyBindings(new ViewModel());
  • Заметьте, что фильмы находятся в observableArray. Как и подразумевает его имя, observableArray действует как массив, уведомляющий подписчиков об изменении своего содержимого.

    Функция getByGenre выдает AJAX-запрос серверу на получение списка фильмов, а затем заполняет результатами массив self.movies.

    При использовании REST API одна из самых хитрых частей - обработка асинхронной природы HTTP. jQuery-функция ajax возвращает объект, реализующий Promises API. Вы можете задействовать метод then объекта Promise, чтобы установить обратный вызов, инициируемый, когда AJAX-вызов завершается успешно, и еще один обратный вызов, запускаемый при неудачном AJAX-вызове:

  • app.service.byGenre(genre).then(addMovies, onError);
  • Привязки данных

    Теперь, когда у меня есть модель представления, я могу связать ее с HTML через привязки данных. Для полного списка жанров, который появляется на левой стороне экрана, я использую следующие привязки данных:

  • Атрибут data-bind содержит одно или более объявлений привязок, где каждая привязка имеет форму "привязка: выражение". В этом примере привязка foreach сообщает Knockout перебирать в цикле содержимое массива genres в модели представления. Для каждого элемента в массиве Knockout создает новый элемент

  • . Привязка text в присваивает text в span значение элемента массива, каковой в данном случае является названием жанра.

    На данный момент щелчок названия жанра ни к чему не приводит, поэтому я добавляю привязку click для обработки событий щелчка:

  • Это связывает событие щелчка с функцией getByGenre в модели представления. Здесь нужно было использовать $parent, так как эта привязка осуществляется в контексте foreach. По умолчанию привязки в foreach ссылаются на текущий элемент в цикле.

    Рис. 10. Добавление привязок в таблицу для отображения списка фильмов

    На рис. 10 привязка foreach перебирает в цикле массив объектов movie. Внутри foreach привязки text ссылаются на свойства текущего объекта.

    Привязка visible в элементе

    контролирует, визуализируется ли таблица. Таблица будет скрыта, если массив movies пуст.

    Наконец, вот привязки для сообщения об ошибке и сообщения "No records found" (заметьте, что вы можете помещать в привязку сложные выражения):

    No records found.

    Редактирование записей

    Последняя часть этого приложения дает возможность пользователю редактировать записи в таблице. Для этого необходима следующая функциональность:

    • переключение между режимами просмотра (только текст) и редактирования (элементы управления вводом);
    • передача обновлений на сервер;
    • поддержка отмены изменений и восстановление исходных данных.

    Чтобы отслеживать режим просмотра/редактирования, я добавил булев флаг в объект movie как наблюдаемое свойство:

  • function movie(data) {
  • // Другие свойства опущены
  • self.editing = ko.observable(false);
  • Мне нужно было, чтобы таблица фильмов отображала текст, когда свойство editing равно false, но переключалась на элементы управления вводом, когда оно - true. Для этого я использовал Knockout-привязки if и ifnot, как показано на рис. 11 . Синтаксис "" позволяет включать привязки if и ifnot без их размещения внутри элемента HTML-контейнера.

    Рис. 11. Поддержка редактирования записей о фильмах

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

    Я связал обработчики щелчка кнопок с функциями save, cancel и edit в модели представления.

    Функция edit проста. Достаточно установить флаг editing в true:

  • self.edit = function (item) {
  • item.editing(true);
  • Функции save и cancel немного посложнее. Для поддержки отмены мне нужен был какой-то способ кеширования исходного значения при редактировании. К счастью, Knockout упрощает расширение поведения наблюдаемых объектов. В коде на рис. 12 добавляется функция store в класс observable. Вызов функции store из observable придает этому классу две новые функции: revert и commit.

    Рис. 12. Расширение ko.observable функциями revert и commit

  • var self = this;
  • var oldValue = self();
  • read: function () {
  • return self();
  • write: function (value) {
  • oldValue = self();
  • self(value);
  • this.revert = function () {
  • self(oldValue);
  • this.commit = function () {
  • oldValue = self();
  • return this;
  • Теперь я могу вызвать функцию store, чтобы добавить эту функциональность в модель:

  • function movie(data) {
  • // ...
  • // Новый код:
  • self.Title = ko.observable(data.Title).store();
  • self.Year = ko.observable(data.Year).store();
  • self.Rating = ko.observable(data.Rating).store();
  • self.Genre = ko.observable(data.Genre).store();
  • Рис. 13 демонстрирует функции save и cancel в модели представления.

    Рис. 13. Добавление функций save и cancel

  • self.cancel = function (item) {
  • revertChanges(item);
  • item.editing(false);
  • self.save = function (item) {
  • app.service.update(item).then(
  • function () {
  • commitChanges(item);
  • function (error) {
  • onError(error);
  • revertChanges(item);
  • }).always(function () {
  • item.editing(false);
  • function commitChanges(item) {
  • for (var prop in item) {
  • if (item.hasOwnProperty(prop) && item.commit) {
  • item.commit();
  • function revertChanges(item) {
  • for (var prop in item) {
  • if (item.hasOwnProperty(prop) && item.revert) {
  • item.revert();
  • Создание веб-клиента с применением Ember

    Для сравнения я написал другую версию своего приложения, используя библиотеку Ember.js.

    Ember-приложение начинает с таблицы маршрутизации (routing table), которая определяет навигацию пользователя в рамках приложения:

  • window.App = Ember.Application.create();
  • App.Router.map(function () {
  • this.route("about");
  • this.resource("genres", function () {
  • this.route("movies", { path: "/:genre_name" });
  • Первая строка кода создает Ember-приложение. Вызов Router.map создает три маршрута. Каждый маршрут соответствует URI или шаблону URI:

    /#/about /#/genres /#/genres/genre_name

    Для каждого маршрута вы создаете HTML-шаблон, используя библиотеку шаблонов Handlebars.

    В Ember имеется шаблон верхнего уровня для всего приложения. Этот шаблон подвергается рендерингу для каждого маршрута. На рис. 14 показан шаблон application для моего приложения. Как видите, этот шаблон в основном является HTML-кодом, размещаемым в теге script с type="text/x-handlebars". Шаблон содержит специальную разметку Handlebars в двойных фигурных скобках: {{ }}. Эта разметка служит той же цели, что и атрибут data-bind в Knockout. Например, {{#linkTo}} создает ссылку на маршрут.

    Рис. 14. Шаблон Handlebars уровня приложения

  • ko.observable.fn.store = function () {
  • var self = this;
  • var oldValue = self();
  • var observable = ko.computed({
  • read: function () {
  • return self();
  • write: function (value) {
  • oldValue = self();
  • self(value);
  • this.revert = function () {
  • self(oldValue);
  • this.commit = function () {
  • oldValue = self();
  • return this;
  • Movies
  • {{outlet}}
  • 2013 Mike Wasson

  • Теперь допустим, что пользователь переходит к /#/about. Это активирует маршрут about. Ember сначала осуществляет рендеринг шаблона application верхнего уровня, затем шаблона about в {{outlet}} шаблона application. Вот шаблон about:

  • Movies App
  • About this app...
  • На рис. 15 показано, как выполняется рендеринг шаблона about в шаблоне application.


    Рис. 15. Рендеринг шаблона about

    Поскольку у каждого маршрута свой URI, история браузера сохраняется. Пользователь может осуществлять навигацию кнопкой Back, а также обновлять страницу, не теряя контекст или закладку, и перезагружать ту же страницу.

    Контроллеры и модели в Ember

    В Ember каждый маршрут имеет модель и контроллер. Модель содержит данные предметной области. Контроллер действует как прокси для модели и хранит все данные состояния приложения для представления. (Это не совпадает с классическим определением MVC. В некоторых отношениях контроллер больше похож на модель представления.)

    Вот как я определил модель movie:

  • App.Movie = DS.Model.extend({
  • Title: DS.attr(),
  • Genre: DS.attr(),
  • Year: DS.attr(),
  • Rating: DS.attr(),
  • Контроллер наследует от Ember.ObjectController (рис. 16 ).

    Рис. 16. Контроллер Movie наследует от Ember.ObjectController

  • App.MovieController = Ember.ObjectController.extend({
  • isEditing: false,
  • actions: {
  • edit: function () {
  • this.set("isEditing", true);
  • save: function () {
  • this.content.save();
  • this.set("isEditing", false);
  • cancel: function () {
  • this.set("isEditing", false);
  • this.content.rollback();
  • Здесь происходит кое-что интересное. Во-первых, я не указывал модель в классе контроллера. По умолчанию маршрут автоматически устанавливает модель в контроллере. Во-вторых, функции save и cancel используют средства транзакций, встроенные в класс DS.Model. Для отмены изменений просто вызовите функцию rollback модели.

    Ember использует массу соглашений по именованию для подключения различных компонентов. Маршрут genres взаимодействует с GenresController, который выполняет рендеринг шаблона genres. По сути, Ember будет автоматически создавать объект GenresController, если вы его не определили. Однако вы можете переопределять все, что предлагается по умолчанию.

    В своем приложении я сконфигурировал маршрут genres/movies на использование другого контроллера, реализовав точку подключения (hook) renderTemplate. Тем самым несколько маршрутов может использовать один и тот же контроллер (рис. 17 ).

    Рис. 17. Несколько маршрутов могут иметь общий контроллер

  • App.GenresMoviesRoute = Ember.Route.extend({
  • serialize: function (model) {
  • return { genre_name: model.get("name") };
  • renderTemplate: function () {
  • this.render({ controller: "movies" });
  • afterModel: function (genre) {
  • var controller = this.controllerFor("movies");
  • var store = controller.store;
  • return store.findQuery("movie", { genre: genre.get("name") })
  • .then(function (data) {
  • controller.set("model", data);
  • Одна из приятных особенностей Ember в том, что многое можно делать с помощью минимума кода. Мое приложение-пример состоит примерно из 110 строк кода на JavaScript. Эта версия короче, чем версия на основе Knockout, и вдобавок я безо всяких усилий получил поддержку истории браузера. С другой стороны, Ember также является весьма "своенравной" инфраструктурой. Если вы не пишете код в стиле Ember, то скорее всего попадете в неприятности. Так что при выборе инфраструктуры следует принимать во внимание набор функциональности, стиль кодирования и то, насколько общая архитектура инфраструктуры подходит под ваши требования.

    Где узнать больше

    В этой статье я показал, как JavaScript-инфраструктуры упрощают создание SPA. Попутно я рассказал о некоторых общих средствах этих библиотек, в том числе о связывании с данными, маршрутизации и шаблонах MVC и MVVM. Узнать больше о создании SPA с помощью ASP.NET можно по ссылке asp.net/single-page-application .

    Майк Уоссон (Mike Wasson) - программист и технический писатель в Microsoft. Многие годы занимался документированием мультимедийной части Win32 API. В настоящее время пишет о ASP.NET с основным акцентом на Web API. С ним можно связаться по адресу [email protected] .

    Выражаю благодарность за рецензирование статьи эксперту Microsoft Хиньяну Чу (Xinyang Qiu).

    май 21 , 2017

    Одностраничные сайты или Single Page Applications (SPA) - это круто. Главный их профит в том, что SPA быстрее и отзывчивее на действия пользователей. Достигается это за счет переноса логики работы на клиентскую сторону и активного взаимодействия с сервером посредством ajax.

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

    Зачем это надо и как это сделать, приложив немного усилий? Об этом ниже. Поехали.

    Итак, зачем это баловство?

    Самое главное - это скорость работы.

    Конечно, при разработке одностраничного сайта-визитки мы столкнемся с некоторыми проблемами:

    • 1. Как подступиться, с чего начать?
    • 2. Как разобраться с историей браузера, с History API?
    • 3. Какой фреймворк или библиотеку изучить: ангуляр, реакт? А мы ни одного не знаем...
    • 4. Как заставить поисковики индексировать одностраничный сайт?

    Ответы на эти вопросы:

    • 1. Разберемся в этой же статье, на примере простого сайта
    • 2. Тоже расскажу ниже, это десяток строк кода
    • 3. Никакой, обойдемся нативным javascript-ом и jQuery в качестве помощника
    • 4. Про поисковики будет следующая статья из этой серии

    Почему без ангуляра-реакта?
    Конечно же, это очень нужные темы, рекомендую их изучать. Но для нашего примера они не понадобятся, нам достаточно обладать минимальными знаниями javascript-a.

    Одностраничники - это тема не одной статьи. Это будет целый проект, серия статей минимум из трех штук. И затрагиваться в нем будут самые разные темы. Но уточню. Мы будем строить не сложную админку со взаимодействием с сервером посредством REST API (по крайней мере, в самом начале). В первых уроках наш одностраничный сайт будет обычной визиткой из десятка страниц. Но сайт будет работать без перезагрузки страниц и радовать наших пользователей скоростью и отзывчивостью интерфейса.

    Идея сайта, как он устроен

    Возьмем самый обычный корпоративный сайт: главная страница, раздел "О проекте", контакты и блог. В разделе "Блог" будет несколько ссылок на внутренние страницы. На каждой странице забьем какой-нибудь контент и вставим немного перекрестных ссылок.

    На каждой странице сайта, как правило, есть повторяющийся контент. У нас это будет шапка и меню. И есть основное содержимое страницы, которое меняется. Мы сделаем так: загрузим страницу всего один раз, а потом кликая по ссылкам, будем динамически подгружать нужное содержимое аяксом. При этом мы будем менять заголовок страницы во вкладке браузера, url в адресной строке и запоминать историю в браузере, чтобы работала навигация через кнопки Назад/Вперед браузера.

    Контент для каждой отдельной странице будет храниться в отдельном html-файле.

    Можете сразу посмотреть, что у нас в итоге получится -

    Структура сайта и подготовительные работы

    Структура файлов-папок такова. В корне проекта файл index.php и.htaccess. Почему именно php, а не html, расскажу позже. В папке css лежат стили в файле main.css. В папке js - библиотека jquery.js и главный файл приложения main.js. В папке pages лежат html-файлы с содержимым сайта - на каждую страницу по одному файлу.

    Готовим контент

    Я сделаю демо-сайт на примере своего проекта webdevkin. Набор страниц будет таким:

    • — main - Главная
    • — about - О проекте
    • — blog - Блог
      • — shop - Интернет-магазины
      • — frontend - Фронтенд
      • — mysql - База данных MySql
      • — widgets - Встраиваемые виджеты
    • — simpple - Проект Simpple
    • — contacts - Контакты

    Соответственно, в папке pages будут лежать 9 html-файлов. Всю разметку для контента найдете в . Для примера приведу содержимое только одного файла - simpple.html

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

    Как видно, никаких head, body, html, script здесь нет - только разметка, относящаяся к конкретной странице.

    index.php и.htaccess

    Почему не index.html?
    Дело в том, что на нашем сайте будет одна-единственная физическая страница - index. Но нас интересуют и такие адреса, как site.ru/about, site.ru/contacts и прочее. Но страниц about и contacts в корне нашего сайта нет. Папка pages с набором html-файлов - это не полноценные страницы, а просто куски html-кода, которые встраиваются внутрь общего каркаса.

    Поэтому, чтобы при обращении к site.ru/about не посыпались 500, 403 и еще бог знает какие ошибки, мы должны все входящие запросы на сайт перенаправлять на index.php, который уже и будет эти запросы разруливать. Впрочем, пока что index.php у нас - это обычная html-разметка без единой строчки php-кода (но это только пока). А в.htaccess мы пропишем следующее. Возвращаться к нему придется редко.

    RewriteEngine On Options +SymLinksIfOwnerMatch RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-l RewriteRule ^(.+)$ index.php?q=$1

    В тему
    Однажды я писал статью про Простой RESTful-сервис на нативном PHP . Там Вы найдете немного больше информации про такой способ перенаправления запросов.

    html-код будет у нас очень простым. В head подключаются стили css/main.css. В подвале 2 js-файла: js/jquery.js и js/main.js. А в body будет следующее:

    Сначала выводим меню. Дальше идет заголовок страницы. И ниже пустой div с id=content (не обращайте внимания на style="" - парсер когда-нибудь выбесит и я его заменю). В #content будут подгружаться динамически содержимое страниц из файлов pages/*.html. Пока ничего интересного.

    Только обратите внимание на атрибуты data-menu и data-link="ajax" у ссылок. Они введены для того, чтобы отличать ссылки-навигации от обычных внешних ссылок, которые на нашем сайте тоже будут. data-link="ajax" означает, что при клике по этой ссылке мы перехватим стандартное поведение браузера и возьмем работу со ссылкой в свои руки. А data-menu означает, какой пункт главного меню будет выделен при клике на оную ссылку. Здесь data-menu дублируется с атрибутом href, но возможны и другие варианты. Например, когда мы залезем в раздел frontend блога, то мы укажем data-menu="blog".

    2 примера для наглядности:

    simpple.ru Главная страница simpple.ru Стили main.css

    Быстро пролистаем и скопипастим самое скучное - файл css/main.css.

    Body { position: relative; font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; font-size: 1em; font-weight: 400; color: #333; } a, a:visited { color: steelblue; } a:hover { color: navy; } .wrapper { width: 80%; margin: 0 10%; } .spa-title { font-size: 1.2em; text-align: center; } menu { margin-top: 2em; padding: 0; text-align: center; } menu a { display: inline-block; margin-right: 10px; padding: 5px 15px; text-decoration: none; } menu a:hover, menu a.active { background-color: steelblue; color: white; } .page-title { text-align: center; } ul li { list-style-type: circle; }

    А вот теперь самое интересное - javascript-код, который превратит наш набор отдельных файлов в одностраничный сайт.

    javascript. Общий код и конфиги

    Зададим каркас js-кода.

    Var app = (function() { var config = {}; var ui = {}; // Привязка событий function _bindHandlers() { // ... } // Инициализация приложения function init() { // ... _bindHandlers(); } // Возвращаем наружу return { init: init } })(); // Запуск приложения $(document).ready(app.init);

    Мы имеем отдельный модуль app, который при загрузке страницы запускает свою функцию init. В ней навешиваем обработчики событий и выполняем еще некоторый код. Также видим 2 объекта: config и ui. В ui будут закэшированы все dom-элементы, которые понадобятся нам в работе.

    Var ui = { $body: $("body"), $menu: $("#menu"), $pageTitle: $("#page-title"), $content: $("#content") };

    $menu нам нужно, чтобы выделять отдельные пункты меню, $pageTitle будем менять динамически при переходе между страницами, а в $content будет подгружаться содержимое файлов pages/*.html

    А вот config выглядит интереснее.

    Var config = { siteTitle: "Webdevkin SPA", mainPage: "main", pages: { main: { title: "Главная", menu: "main" }, about: { title: "О проекте", menu: "about" }, blog: { title: "Блог Webdevkin-a", menu: "blog" }, simpple: { title: "Проект Simpple", menu: "simpple" }, contacts: { title: "Контакты", menu: "contacts" }, shop: { title: "Интернет-магазины", menu: "blog" }, frontend: { title: "Статьи о фронтенде", menu: "blog" }, mysql: { title: "База данных Mysql", menu: "blog" }, widgets: { title: "Встраиваемые javascipt-виджеты", menu: "blog" } } };

    siteTitle: "Webdevkin SPA" - заголовок сайта, используется в нескольких местах.
    mainPage: "main" - указываем стартовую страницу сайта, ту, которая откроется при заходе на site.ru. Сейчас это главная страница main, но Вам запросто может прийти в голову поставить стартовую, например, "О проекте" - about.

    Самое важное во всем конфиге - это объект pages. Каждое поле объекта описывает одну страницу сайта. Сейчас нам понадобятся только 2 пункта: заголовок страницы и меню, к которому оная страница относится. Ключи объекта, то есть страницы, совпадают с названиями файлов в pages/*.html и атрибутами href во внутренних ссылках.

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

    Подгрузка контента с помощью ajax. History API

    Пойдем по порядку. Займемся функцией init, в которой есть привязывание нужных событий _bindHandlers

    // Привязка событий function _bindHandlers() { ui.$body.on("click", "a", _navigate); window.onpopstate = _popState; }

    В первой строке мы отлавливаем клики на внутренние ссылки a и отправляем их в функцию _navigate. Теперь мы окончательно убедились, что нужны отдельные атрибуты data-link="ajax" :-)

    Далее window.onpopstate = _popState;
    Из документации.
    Событие popstate отсылается объекту window каждый раз, когда активная запись истории меняется между двумя записями истории для одного и того же документа.
    Проще говоря, это срабатывание кнопок Назад/Вперед в браузере. Как мы видим, нативное поведение браузера тоже нужно перехватывать. Поэтому мы отдадим управление функции _popState.

    Для лучшего восприятия приведу код сразу для обеих функций

    // Клик по ссылке function _navigate(e) { e.stopPropagation(); e.preventDefault(); var page = $(e.target).attr("href"); _loadPage(page); history.pushState({page: page}, "", page); } // Кнопки Назад/Вперед function _popState(e) { var page = (e.state && e.state.page) || config.mainPage; _loadPage(page); }

    При явном клике по ссылке _navigate мы останавливаем всплытие события клика и отменяем дефолтное поведение браузера (переход по ссылке). Затем мы определяем страницу, которую мы хотим загрузить (понимаем по атрибуту href), и вызываем новую функцию _loadPage. Она и сделает всю основную работу по загрузке контента, изменению заголовка и прочее-прочее. И в конце через history.pushState добавляем новую запись в истории браузера. Да, мы сами, явным образом создаем историю браузера. Тогда, когда считаем нужным. И сохраняем данные о загружаемой странице в объект {page: page}. Эти данные нам пригодятся в следующей функции _popState.

    В _popState идея аналогична: ищем нужную страницу и запускаем ту же _loadPage.

    Var page = (e.state && e.state.page) || config.mainPage;

    e.state && e.state.page - вытаскивает нам страницу из объекта истории, которую мы предусмотрительно записали в _navigate. Если же объект e.state недоступен (например, когда мы первый раз зашли на сайт site.ru и еще не успели побродить по нему), то берем страницу, указанную главной в нашем конфиге - config.mainPage.
    По совести говоря, функция history.pushState и тот факт, что в window.onpopstate доступен объект e.state с данными, записанными в pushState, - это все, что нам достаточно знать о History API. Для более любопытных товарищей, не сомневаюсь, что гугление поможет найти и другие хорошие способы работы с историей браузера.

    Мы же глубокими изысканиями заниматься не будем, а напишем код главной функции _loadPage

    // Загрузка контента по странице function _loadPage(page) { var url = "pages/" + page + ".html", pageTitle = config.pages.title, menu = config.pages.menu; $.get(url, function(html) { document.title = pageTitle + " | " + config.siteTitle; ui.$menu.find("a").removeClass("active"); ui.$menu.find("a").addClass("active"); ui.$pageTitle.html(pageTitle); ui.$content.html(html); }); }

    Выглядит он вполне заурядно. Сначала формируем url, то есть путь, по которому мы загрузим html для страницы. Потом вытаскиваем из конфига заголовок страницы и пункт меню, подлежащий выделению. Далее получаем html-содержимое страницы через банальный jQuery.get и выполняем еще ряд нехитрых действий.

    а) Обновляем заголовок страницы
    б) В 2 строки выделяем нужный пункт меню
    в) Меняем заголовок уже на самой странице, в html-коде
    г) Загружаем собственно html-содержимое страницы, полученное из url

    Почти все! Остался маленький момент. Если мы сейчас перейдем, например, на страницу site.ru/blog и обновим ее, то загрузится не блог, а пустая страница. Потому что при инициализации мы _loadPage не вызываем. Чтобы это исправить, нам нужно дописать пару строк в функцию init, которая в итоге будет выглядеть так

    // Инициализация приложения function init() { var page = document.location.pathname.substr(1) || config.mainPage; _loadPage(page); _bindHandlers(); }

    Вот теперь точно все!

    Подведем итоги и напомним ссылки

    Итак, мы написали простой, но вполне рабочий одностраничный сайт, а также немного узнали, как работает History API. Честно говоря, чувствую себя режиссером плохого фильма, когда 80% времени ведут к какой-то грандиозной развязке, а в конце все оказывается намного проще, чем ожидалось.

    С одной стороны, почти весь код - это просто подготовка, обвязка, написание конфигов и прочих скучных вещей. А действительно полезный код, выполняющий непосредственную работу, занимает 2 десятка строк.

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

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

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

    И небольшой опрос

    Роман Липский

    Мир программного обеспечения сейчас эволюционирует с огромной скоростью. Всего пару лет назад настольные компьютеры и ноутбуки являлись основными устройствами, под которые была заточена веб-разработка. Сегодня дела обстоят совсем не так.

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

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

    Если вы задумываетесь о разработке веб-приложения для своей компании, вы наверняка уже знаете о том, что существуют два основных подхода к разработке: можно создавать как одностраничные веб-приложения (SPA), так и многостраничные приложения (MPA).

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

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

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

    Как мы уже упоминали выше, и у SPA, и у MPA есть свои плюсы и минусы. Давайте же разберемся в разнице между двумя типами приложений и постараемся найти правильное решение в области веб-разработки для вашей компании.

    Чтобы сделать это, мы попросим директора веб-направления BLAKIT Виталия Озерского дать свои экспертные комментарии по теме. Надеемся, что вместе мы сможем облегчить для вас этот выбор.

    Одностраничные приложения

    Одностраничные приложения работают в рамках браузера и не требуют перезагрузки страницы или загрузки дополнительных страниц во время использования. Подобные приложения ежедневно используют миллионы юзеров, даже не замечая этого. Самые популярные примеры: GitHub, Gmail, Google Maps и даже Facebook.

    Виталий Озерский: одностраничные приложения, как правило, максимально интерактивны, причём настолько, что у пользователя складывается ощущение, будто он работает с десктопным приложением: реакция приложения на пользовательские действия моментальная в большинстве случаев. Этим SPA выгодно отличаются от многостраничных сайтов, где при каждом действии пользователю необходимо дожидаться загрузки новой страницы.

    Одностраничные приложения созданы специально для того, чтобы предоставлять пользователям отличный UX, напоминающий «естественную» среду браузера без перезагрузок страниц, а значит, без задержек при совершении действий.

    Типовое одностраничное приложение выглядит как веб-страница, подгружающая и обновляющая контент без перезагрузки с помощью JavaScript. SPA запрашивает разметку страницы и её контент, а затем создает конечный вид страницы непосредственно в браузере. Такого эффекта можно достигнуть благодаря продвинутым фреймворкам JavaScript, таким как AngularJS, Ember.js, Meteor.js, Knockout.js.

    ВО: Помимо основных популярных фреймворков разработчики BLAKIT способны также разрабатывать одностраничные приложения при помощи ReactJS.

    Основное преимущество React – невысокий порог входа. React довольно прост в использовании; практически любой разработчик, знакомый с HTML, может создавать React-приложения.

    Еще одно преимущество – возможность писать приложения под веб и под мобильные платформы, используя единый стек технологий.

    Мы используем React вместе с библиотекой Redux, которая позволяет заложить правильную архитектуру и создавать сложные веб-приложения, легко поддающиеся масштабированию.

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

    Преимущества одностраничных приложений:
    • SPA характеризуются отличным быстродействием, так как большинство ресурсов, которые они используют (HTML+CSS+Скрипты), загружаются лишь однажды в течение сессии использования приложения. После совершения действий на странице меняются лишь данные.
    • Разработка веб-приложений обычно быстрее и эффективнее. Нет необходимости писать отдельный код для рендера страницы на стороне сервера. Также гораздо легче запустить процесс разработки подобных приложений, потому что писать код можно начинать с файла file://URI, не используя при этом никакой сервер.
    • SPA оптимизированы для Chrome debugging, разработчики могут отслеживать сетевые действия, изучать элементы страниц и данные, с ними ассоциируемые.
    • Если у вас уже есть SPA, будет возможность с тем же бэкендом создать и мобильное приложение.
    • SPA более эффективны в кэшировании данных на локальных носителях. Приложение высылает один запрос, собирает все необходимые данные, и с этого момента способно работать даже в режиме оффлайн.
    Недостатки одностраничных приложений:
    • SEO-оптимизация одностраничных приложений, по очевидным причинам, не очень проста. Контент приложений загружается при помощи AJAX (Asynchronous JavaScript and XML) - метода обмена данными и обновления приложения без перезагрузки страницы, в то время как SEO-оптимизация основана на устойчивости контента в каждой отдельно взятой странице. При этом продвижение вашего сайта в поисковиках не невозможно. Многие изменения в SEO можно провести на стороне сервера, а компании вроде Google продолжают придумывать новые решения для того, чтобы облегчить жизнь как владельцам SPA, так и их пользователям.
    • Они довольно долго загружаются, поскольку тяжелые клиентские фреймворки должны сперва загрузиться в браузер.
    • SPA требуют JavaScript в активном режиме в браузерах пользователей. Если кто-то из ваших клиентов вручную отключит использование JavaScript, они не смогут в полной мере воспользоваться вашим приложением. Даже если вы будете кэшировать и обрабатывать ваши страницы на стороне сервера (а это сейчас тоже возможно), вы всё ещё рискуете не доставить пользователям без JS все функции одностраничного приложения в правильном виде.
    • По сравнению с традиционными приложениями, SPA чуть хуже защищены. Благодаря межсайтовому скриптингу (XSS), злоумышленники имеют возможность внедрять дополнительные скрипты на стороне клиента.
    • Некоторые утечки памяти в JavaScript могут привести к падению производительности даже в мощных системах

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

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

    Многостраничные приложения

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

    Сложность и издержки при разработке MPA выше, также для них требуется многоуровневый дизайн UI. К счастью, сейчас это уже не такая большая проблема, поскольку AJAX позволяет обновлять только определенные части приложения, а не перебрасывать кучу данных между серверами и браузерами.

    Преимущества многостраничных приложений:
    • Это наиболее подходящий вариант для пользователей, которым нужен более визуально понятный интерфейс и привычная навигация по приложению. MPA обычно создаются с многоуровневыми меню и другими инструментами навигации.
    • Значительное упрощение SEO. Вы сможете оптимизировать каждую отдельно взятую страницу приложения под нужные вам ключевые слова.
    Недостатки многостраничных приложений:
    • Frontend и backend разработка в данном случае объединены очень тесно.
    • Разработка МPA довольно сложна, так как она требует использования фреймворков как на клиентской, так и на серверной стороне. Сроки и издержки разработки, соответственно, не так приятны для многих компаний.
    SPA или MPA?

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

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

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

    Технически, гибридное приложение – всё ещё SPA, но оно использует якоря URL как синтетические страницы, благодаря чему расширяются возможности встроенной навигации в браузере и функциональность настроек.

    Дайте нам знать, если хотите узнать больше о гибридных приложениях, мы напишем о них отдельный текст.

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

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

    ВО: полностью согласен с утверждением про растущую популярность одностраничных приложений. Сейчас SPA-приложения пользуются большим спросом, так как компании постепенно переводят свой софт с десктоп-приложений на приложения, доступные с любого браузера, а не только на ПК с Windows.

    В последнее время в работе специалистов Netpeak Agency много сайтов, использующих AJAX-технологии , а также сайтов написанных на JavaScript фреймворках. Без дополнительной оптимизации они не готовы к продвижению в поисковых системах. Поэтому я детально опишу, что такое SPA-сайты, как они работают, и как можно сделать Single Page Application SEO-Friendly.

    Статья будет полезна SEO-специалистам и владельцам сайтов, которые хотят перейти на Single Page Application, но не решаются, потому что SPA могут «поссориться» с поисковыми системами.

    Что такое SPA

    SPA (Single Page Application ) — одностраничное JavaScript приложение, которое запускается и работает в браузере. В отличии от «традиционного» сайта, архитектура на SPA-сайтах построена так, что рендеринг страницы полностью происходит на стороне клиента, а не на стороне сервера.

    В браузере пользователя запускается JavaScript-приложение, а все необходимое содержимое страниц динамически загружается с помощью AJAX. Навигация по сайту происходит без перезагрузки страниц. За счет такой архитектуры, SPA-сайты работают быстрее, чем «традиционные» сайты.

    Рассмотрим детальнее, как происходит загрузка и рендеринг содержимого на SPA-сайтах:

  • Пользователь запрашивает HTML содержимое сайта.
  • В ответ приходит JavaScript-приложение.
  • Приложение определяет, на какой странице находится пользователь, и какое содержимое ему нужно отобразить.
  • С помощью AJAX пользователь получает необходимый контент: CSS, JS, HTML и текстовый контент.
  • JavaScript-приложение обрабатывает полученные данные и отображает их в браузере.
  • При навигации по сайту обновляется не вся страница, а только необходимое содержимое.
  • Плюсы SPA-сайтов:

    • высокая скорость работы;
    • быстрая разработка;
    • создание версий для разных платформ на основе готового кода (desktop и mobile приложения) .

    Минусы SPA-сайтов:

    • JavaScript не обрабатывается большинством поисковых систем;
    • SPA-сайты не работают без включенного JS в браузере;
    • их нельзя анализировать на предмет ошибок популярными программами и инструментами (например, Netpeak Spider).

    Популярные JavaScript фреймворки:

    • Angular ;
    • Meteor ;
    • React ;
    • Backbone ;
    • Ember ;
    • Vue ;
    • Polymer ;
    • Knockout .
    Какие поисковые системы индексируют SPA-сайты

    На сегодняшний день только поисковый робот Google умеет рендерить содержимое SPA-сайтов, так как использует для рендеринга инструменты на базе Chrome 41. ASK.com использует выдачу Google. Для остальных поисковых систем необходимо наличие контента в коде в формате HTML.


    Оптимизация индексации SPA-сайтов

    Роботы Google и Yandex могут проиндексировать Single Page Application, если структура сайта соответствует определенным правилам. При этом, для Яндекса необходимо обязательное наличие полной HTML-копии страницы.

    Для Google нужно использовать только правильный формат URL. Летом 2018 Google перестанет индексировать HTML-копии страниц , а будет использовать только рендеринг.

    (!) Нельзя запрещать индексирование JS и CSS файлов для поисковых роботов Google. Ограничив их индексацию, Google не сможет проиндексировать содержимое SPA-сайтов.

    Существует два способа «заставить» поисковых роботов индексировать AJAX-страницы:

  • Использовать «?_escaped_fragment_=» .
  • Отдавать роботу HTML-копии определяя его по user-agent .
  • Использование «?_escaped_fragment_=»

    Этот способ подразумевает генерацию HTML-копий страниц (снимков) по отдельному URL с использованием параметра «?_escaped_fragment_» .

    Использование URL c «#!»

    Если адреса AJAX страниц формируются с помощью «#» (хеш) , значит нужно заменить их на «#!» (хешбенг) . Например, с https://example.com/#url на https://example.com/#!url .

    Google просканирует содержимое по основному URL, а робот Яндекса, обнаружив в URL «#!», запросит снимок страницы. Он заменит «#!» на «?_escaped_fragment_=» и просканирует ее по адресу https://example.com/?_escaped_fragment_=url .

    Примеры адресов c «#!» и HTML-копий страниц:

    • https://example.com/home#!page → https://example.com/home?_escaped_fragment_=page ;
    • https://example.com/index/#!main → https://example.com/index/?_escaped_fragment_=main ;
    • https://example.com/#!home/index → https://example.com/?_escaped_fragment_=home/index .
    Использование «традиционных» URL

    Если на сайте используются «традиционные» URL (https://example.com/url), необходимо указать на всех страницах мета-тег:

    Google просканирует содержимое по основному URL, а робот Яндекса, обнаружив на странице мета-тег , запросит HTML-копию страницы. Также добавит в URL параметр «?_escaped_fragment_=» и просканирует ее по адресу https://example.com/url?_escaped_fragment_=

    Для популярных фреймворков есть готовые решения, которые заменяют «#!» на «традиционные» URL, например, HTML5 mode для Angular .

    (!) В HTML-копии документа мета-тег размещать не следует: в этом случае страница не будет проиндексирована.

    (!) На страницах HTML-копий canonical должен либо отсутствовать, либо вести сам на себя.

    Например, на странице https://example.com/home?_escaped_fragment_= должен быть указан следующий canonical:

    Отдавать HTML-копии страницы по основному URL

    Этот способ подходит для SPA-сайта с «традиционными» URL. В чем суть: поисковый робот, запрашивая страницу по основному URL, вместо динамической версии получает HTML-копию страницы.

    Определить поискового робота можно по User-Agent. К примеру, по списку роботов Яндекса .

    Рендеринг HTML-копий

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

    HTML-копии — это отрендеренные версии страниц SPA-сайта. Например, содержимое исходного кода страницы SPA-сайта выглядит так:

    Single Page Application

    А вот отрендеренная HTML-копия этой страницы:

    TITLE страницы СОДЕРЖИМОЕ СТРАНИЦЫ


    Для создания HTML-копий страниц можно:

  • Использовать рендеринг на своих серверах.
  • Использовать рендеринг на аутсорсе.
  • Использовать изоморфный JavaScript.
  • Существуют много готовых решений для рендеринга HTML-копий, поэтому, если вы используете популярный фреймворк (например, Angular) , на внедрение HTML-копий страниц должно уйти всего несколько часов разработчика.

    Рендеринг на своих серверах

    Подходит для крупных проектов. Для рендеринга HTML-копий на своих серверах можно использовать следующие инструменты:

    • PhantomJS ;
    • Angular Universal ;
    • Angular-SEO .
    Рендеринг на аутсорсе

    Подходит для небольших проектов. Для рендеринга HTML-копий на сторонних серверах можно использовать такие инструменты:

    • BromBone ;
    • Prerender.io ;
    • RenderJS.io .
    Изоморфный JavaScript

    Изоморфный JavaScript позволяет выполнять рендеринг страницы как на сервере, так и в браузере пользователя. Таким образом, при первой загрузке страницы пользователь получает обычную HTML-страницу и JavaScript-приложение. Дальнейшая навигация по сайту для пользователя будет динамической, в то время как поисковые системы каждый раз будут получать HTML-версию страницы.

    Такой подход к разработке сайта позволяет убить сразу двух зайцев: сайт получает все преимущества SPA-сайта и возможность продвижения в поисковых системах без дополнительных внедрений.

    Рендеринг страницы на SPA-сайтах происходит на стороне клиента, поэтому без дополнительных доработок код ответа несуществующей страницы будет 200 ОК.

    SPA-сайты должны корректно обрабатывать несуществующие страницы и отдавать в качестве заголовка страницы 404 ошибку.

    Настройка Google Analytics

    Стандартный код Universal Analytics выполняется при загрузке каждой новой страницы, а SPA-сайты подгружают контент динамически, «имитируя» переход между страницами. Для того, чтобы Google Analytics корректно обрабатывал переходы на страницах, необходимо сделать так, чтобы счетчик Universal Analytics активировался каждый раз, когда на сайте меняется URL.

    Настроить Google Analytics для SPA-сайтов можно, используя Tag Manager и триггер «History» или — передавая данные вручную.

    С помощью Tag Manager и триггера «History» Выводы

    SPA-сайты — это настоящее и будущее, поэтому не нужно бояться брать в работу такие проекты. Single Page Application можно «подружить» с поисковыми системами. Чтобы ваш SPA-сайт был дружелюбен как к SEO, так и к пользователям я рекомендую:

  • Использовать «традиционные» URL.
  • Использовать изоморфные приложения или рендеринг HTML-копий.
  • Настроить правильную отдачу заголовка «404 Not Found».
  • Настроить корректную работу Google Analytics.


  • Save
  • Cancel
  • Edit