Пишем одностраничный клиент на javascript

    Данная статья является вольным переводом. Оригинал тут.

    Введение


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

    Предлагаю ознакомиться с решением на базе backbone.js, underscore.js и jQuery, которое поможет решить эту проблему.

    Постановка задачи


    Каким бы мы хотели видеть наше приложение? Вот основные моменты, которые мне кажутся важными:
    1. Должен быть удобный способ описать модели нашей предметной области.
    2. Любые изменения в модели должны немедленно отражаться в пользовательском интерфейсе, если модель в нем представлена каким-либо образом.
    3. Понятная и легко-поддерживаемая структуризация кода в стиле MVC.


    Попробуем решить эти задачи на примере простого приложения «Каталог фильмов».


    Инструменты


    Нам потребуется:


    Взгляд на backbone.js


    Задача данного фреймворка не в том чтобы дать вам кучу виджетов, и даже не в том, чтобы обеспечить уровень представления (view). Его задача дать вам несколько ключевых объектов, которые помогут структурировать код.
    Нам понадобятся объекты Model, Collection, View и Controller.

    Модель


    Для получения полнофункциональной модели достаточно написать всего одну строчку кода:
    var Movie = Backbone.Model.extend({});
    

    Теперь мы можем получить экземпляры объекта, задавать и получать произвольные атрибуты:
    matrix = new Movie();
    
    matrix.set({
        title: "The Matrix",
        format: "dvd'
    });
    
    matrix.get('title');
    

    Также можно передавать атрибуты напрямую в конструктор при создании объекта:
    matrix = new Movie({
        title: "The Matrix",
        format: "dvd'
    });
    

    Выполнить какие-то проверки или иные действия при создании объекта, можно расширив модель функцией
    initialize(). При создании объекта она будет вызвана с параметром, который вы передали в конструктор.
    var Movie = Backbone.Model.extend({
        initialize: function (spec) {
            if (!spec || !spec.title || !spec.format) {
                throw "InvalidConstructArgs";
            }
        }
    });
    

    Также можно определить метод validate(), он будет вызываться каждый раз, когда вы задаете атрибуты и используется для валидации атрибутов. В случае если этот метод что-либо возвращает, атрибут не устанавливается:
    var Movie = Backbone.Model.extend({
        validate: function (attrs) {
            if (attrs.title) {
                if (!_.isString(attrs.title) || attrs.title.length === 0 ) {
                    return "Название должно быть непустой строкой";
                }
            }
        }
    });
    

    Для более полного ознакомления с возможностями backbone предлагаю ознакомиться с документацией.

    Коллекции


    Коллекция в backbone представляет из себя упорядоченный список моделей некоторого типа. В отличие от обычного массива, коллекции обеспечивают гораздо больше функционала, такого как, например, установка правил сортировки с помощью метода comparator().
    После того как определен тип модели в коллекции, добавление туда объекта выглядит чрезвычайно просто:
    var MovieList = Backbone.Collection.extend({
        model: Movie
    });
    
    var library = new MovieList();
    
    library.add({
        title: "The Big Lebowski",
        format: "VHS"
    });
    


    Представления


    В общих чертах, представления backbone определяют правила отображения изменений модели в браузере.
    Здесь начинаются манипуляции с DOM и в игру вступает jQuery. Для изначальной загрузки моделей в DOM нам потребуется шаблонизатор, мы воспользуемся связкой ICanHaz.js + mustache.js
    Вот пример представления для нашего приложения:
    var MovieView = Backbone.View.extend({
        render: function() {
            var context = _.extend(this.model.toJSON(), {cid: this.model.cid});
            this.el = ich.movie(context);
         
            return this;
        }
    });
    


    Соберем все вместе


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

    Контроллер


    В контроллере мы свяжем все части приложения, а также определим пути для манипуляций с объектами и связанные с ними методы.
    var MovieAppController = Backbone.Controller.extend({
        initialize: function(params) {
            this.model = new MovieAppModel();
            this.view = new MovieAppView({ model: this.model });
            params.append_at.append(this.view.render().el);
        },
        
        routes: {
            "movie/add": "add",
            "movie/remove/:number": "remove",
        },
    
        add: function() {
            app.model.movies.add(new Movie({
                title: 'The Martix' + Math.floor(Math.random()*11),
                format: 'dvd'
            }));
            // сбросим путь чтобы метод можно было вызвать еще раз
            this.saveLocation(); 
      
        },
        
        remove: function(cid) {
            app.model.movies.remove(app.model.movies.getByCid(cid));
            this.saveLocation();
        }
    });
    


    Здесь мы видим, что в контроллере сохраняется модель приложения, которая будет хранить все остальные модели и коллекции, а также представление приложения.
    Модель приложения в нашем случае будет хранить коллекцию фильмов:
    var MovieAppModel = Backbone.Model.extend({
        initialize: function() {
            this.movies = new MovieList();
        }
    });
    

    Представление приложения будет выглядеть так:
    var MovieAppView = Backbone.View.extend({
        initialize: function() {
            _.bindAll(this, "addMovie", "removeMovie");
            this.model.movies.bind('add', this.addMovie);
            this.model.movies.bind('remove', this.removeMovie);
        },
        
        render: function() {
            $(this.el).html(ich.app(this.model.toJSON()));
            this.movieList = this.$('#movieList');
        
            return this; 
        },
      
        addMovie: function(movie) {
            var view = new MovieView({model: movie});
            this.movieList.append(view.render().el);
        },
      
        removeMovie: function(movie) {
            this.$('#movie_' + movie.cid).remove();
        }
    });
    


    Ну и собственно индексный файл со всеми зависимостями и шаблонами:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
      <title>Movie App</title>
      
      <!-- libs -->
      <script src="js/lib/jquery.js"></script>
      <script src="js/lib/underscore.js"></script>
      <script src="js/lib/backbone.js"></script>
    
      <!-- templating -->
      <script src="js/lib/mustache.js"></script>
      <script src="js/lib/ICanHaz.js"></script>
    
      <!-- app -->
      <script src="js/app/Movie.js"></script>
      <script src="js/app/MovieCollection.js"></script>
      <script src="js/app/MovieView.js"></script>
      <script src="js/app/MovieAppModel.js"></script>
      <script src="js/app/MovieAppView.js"></script>
      <script src="js/app/MovieAppController.js"></script>
      
      <script type="text/javascript">
        $(function() {
          var movieApp = new MovieAppController({append_at: $('body')});
          window.app = movieApp;
          Backbone.history.start();
        });
      </script>
      
      <!-- ich templates -->
    
      
      <script id="app" type="text/html">
        <h1>Movie App</h1>
        <a href="#movie/add">add new movie</a>
        <ul id="movieList"></ul>
      </script>
    
      <script id="movie" type="text/html">
        <li id="movie_{{ cid }}"><span class="title">{{ title }}</span> <span>{{ format }}</span> <a href="#movie/remove/{{ cid }}">x</a></li>
      </script>
      
    </head>
    <body>
    </body>
    </html>
    


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

    Подробнее
    Реклама

    Комментарии 51

      +1
      Основная проблема одностраничных сайтов — поисковики, которые будут видеть только одну страницу. Опять же проблемы с прямыми ссылками. А вот для различных админок и прочих инструментов такой подход просто идеален. Думаю, стоит учесть наличие сего инструментария при разработке админки следующего проекта.
        +2
        Думаю, такой подход скорее удобен для реализации различных сервисов, а не сайтов в привычном понимании. В таком случае индексация поисковиком не играет никакой роли.
          –13
          Помойму индексация поисковиком в любом случае играет не последнюю роль…
            +6
            Если вы предоставляете СЕРВИС для пользователя, то как его можно проиндексировать?
              +4
              XML карты сайта отменили?

              — ввести урлы вида domain.com/#/some/path/here для возможности доступа к любому контенту по прямым ссылкам (JS читает хеш (то что после #) и выполяет необходимые действия)
              — на стороне сервера стоит проверка на user-agent. если к нам стучится бот, то вместо JS интерфейса выдаем статический html контент для индексации
              — создаем динамическую XML карту для наших урлов
              — ???????
              — PROFIT!

              юзер ckald ниже привел вроде как даже ссылку на либу готовую… но я ее не смотрел, да и руками такое закодить не составляет труда
                +1
                забыл добавить, что в xml-карте постим урлы без /#/, чтобы параметры передались на сервер, а не зависли в адрес-хеше на стороне браузера… если постучавшийся по урлу не бот, а простой юзер, то его редиректят на аналогичый адрес но уже с /#/

                пример такого редиректа есть у твиттера. в гугле все заиндексировано ссылками вида twitter.com/lol при попытке зайти с браузера нас редиректят на twitter.com/#!/lol где нас радостно встречает их новый JS интерфейс
                  0
                  Этот хак мы придумали раньше твитера (см. inthecity.ru). Почему хак? Потому что возникают проблемы с традиционными для рунета системами статистики вроде liveinternet.ru (на который обращают внимание часть рекламодателей). Но для li.ru был придуман свой костыль.
                    0
                    Какой? Поделитесь опытом?
                      +2
                      Допустим текущий урл сайта site.ru/#/index.html если подгрузить стандартный код Li.Ru на такой странице получим засчитанный хит для страницы site.ru/. Что бы не терять то что после решетки, то при загрузки такой страницы подгружаем счетчик в iframe'е задавая SRC параметр равный: www.site.ru/index.html по этому адресу тем кто пришел к нам напрямую отдаем перенаправление на аналогичный адрес без www и с #, а для случая когда страничка подгружается через iframe (это можно например определить через доп., параметр или через refferer) генерируем код счетчика LI.ru.
                        0
                        Класс! спасибо.
                  0
                  Отличие той библиотеки, ссылку на которую я привел, в том, что она упраздняет хеши для таких целей. Адрес совершенно бото-понятен и индексируем. И сами классические хеши на странице работают (хотя, как заметил мой друг — да кто их сейчас использует?!).

                  И карта не нужна. Но для браузеров без поддержки pushState сайт выглядит, как и для ботов, статично. Я не говорю, что js не в принципе — но чудеса навигации с поддержкой history устроить не выйдет. На этот случай следует дописать поддержку хеш-навигации с адаптацией, в зависимости от возможностей браузера.

                  То есть, чтобы человек, пришедший на /#!register в google chrome сразу же оказывался на /register. И наоборот.

                  Лично я это использовал как раз для плеера музыки. Вспомнил еще пример — prostopleer.com

                  А насчет комментария Nc_Soft — так я думаю, что он имел в виду, что индексировать сайт вроде prostopleer.com — бессмысленно. Как и какой-нибудь сервис выбора авиабилетов.
                    0
                    А как же авторизация, если сервис персонально-личный?
                      0
                      На самом деле подготавливать две версии очень трудоемко. Более того ПС вправе счесть разную выдачу за клоакинг, поэтому мы пошли другим путем — специально для роботов мы интерпретируем яваскрипт на стороне сервера и отдаем им уже готовый для отображения HTML. Вы можете увидеть это если постучитесь к нашему вебсерверу с юзерагентом яндекс или гугл бота.
                        0
                        Спасибо
                  +3
                  Для поисковиков есть следующее решение: github.com/defunkt/jquery-pjax. Использует pushState для подмены url. Правда, его не ест, как минимум, ИЕ8 — но я надеюсь дописать поддержку аякса с форматом хеша #!url.

                  А поисковик, поскольку он при запросе не дает заголовка HTTP_PJAX — получает целиком сформированную страницу, как и любой человек, зашедший по ссылке.

                  Опробовано на последнем проекте — лично я в восторге.

                  Ну, еще можно глянуть, как работает плеер Вконтакте — он ведь играет, когда мы по страницам лазим.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      +1
                      https://github.com/defunkt/jquery-pjax, а то там точка прилипла :)
                        0
                        Подмена url, что на гитхабе, работает схожим образом?
                        0
                        Расскажите, для чего в твиттере и других подобных сервисах (prostopleer вроде тоже) после хеша перед урлом ставится восклицательный знак? Просто постоянно бросается в глаза, хотя, казалось бы, можно и без него.
                          –1
                          Для красоты. Насколько я слышал, такой формат предложил google. Чтобы отличить от классических хешей на странице.
                            +3
                      +2
                      Сегодня в RSS ленте наткнулся на запись о socketstream:
                      SocketStream is a new full stack web framework built around the Single-page Application paradigm. It embraces websockets, in-memory datastores (Redis), and client-side rendering to provide an ultra-responsive experience that will amaze your users.

                      Думаю, стоит ознакомиться.
                        0
                        Не могу сообразить, как извернуться, чтобы в Backbone валидацию делать на стороне сервера? Или Backbone для этого не подходит?
                          0
                          А в чем проблема? Бэкбоун ведь посылает JSON массив на сервер, значения которого можно обработать на сервере стандартными средствами.
                            0
                            Не совсем проблема, когда разбиралась с Backnone.js, остановилась на этом вопросе.

                            Значит надо писать надстройку к Backbone, чтобы автоматически собирать серверный адрес для валидации, а потом автоматически парсить полученный ответ со списком ошибок.
                              +1
                              Никаких надстроек не надо. Для ajax запросов backbone использует jQuery. При вызове метода модели save() создается RESTfull запрос к серверу по адресу указанному в url модели. При желании можно реализовать свою логику переопределив Backbone.sync(). А метод validate() нужен чтобы отсеять модели с некорректными данными еще до отправки на сервер. Вообще, в документации все доступно расписано.
                                0
                                jQuery или Zepto ;)
                          0
                          а зачем вам mustache.js? в underscore имхо хорошая потдержка темплейтов
                            0
                            mustache используется как шаблонизатор в ICanHaz.js а ICanHaz в свою очередь предоставляет удобный способ описания шаблонов. Хотя конечно, это дело вкуса, как рендерить страницу.
                            0
                            Спасибо за статью, как раз сейчас был озадачен поиском подобных библиотек… а тут как по заказу, статья со ссылочками.
                              +1
                              Для построения «правильной архитектуры» на клиенте, еще Knockout.JS вполне достоин внимания.
                                0
                                Для информации — это весьма интересная реализация паттерна MVVM на JS.
                                  +1
                                  Knockout отличная штука. Но его способ связывание модели с видом — прямо в html-разметке, мне не нравиться. Грязновато получается:
                                  <textarea data-bind="value: message, valueUpdate: 'afterkeydown'"></textarea>

                                  <ul data-bind="template: { name: 'itemTmpl', foreach: items, templateOptions: { selected: selectedItem } }">
                                  </ul>


                                  * This source code was highlighted with Source Code Highlighter.
                                    0
                                    Когда как, тут зависит от подхода и задачи — иногда это удобно, иногда нет. С одной стороны, подобное использование атрибутов «data-» похоже на хак и over-use, но свое применение Knockout имеет.
                                      0
                                      А по-моему очень компактно и очевидно, без лишнего кода. data- аттрибуты очень удобны для этого (имхо это вообще одна из основных задач data- аттрибутов). Зачем громоздить что-то где-то, ведь самое очевидное — указать это прямо в элементе.
                                    –2
                                    Если честно, то я бы всё это заменил Мутулзом. Плюс добавил бы ещё одну свою либу, которая делает что-то вроде тех же моделей, но не абстрактных, а связанных с конкретными элемантами DOM.

                                    Скажите, а использование JS-шаблонизаторов это требование? Просто мне ничто никогда не представлялось настолько бессмысленным как шаблонизация на JS.
                                      0
                                      Думаю, это дань подходу MVC
                                      0
                                      Интересно, как обстоит вопрос быстродействия, по сравнению с jQuery/чистым js?

                                      Вообще принципиальных сложностей с реализацией MVC самому не вижу (просто не вижу); как минимум, это доверие к своему коду в плане оптимальности быстродействия.
                                        0
                                        В backbone.js всего около 1000 строк, так что в чужом коде не погрязнешь.
                                        Ну и как минимум можно подсмотреть удачные решения для своего велосипеда :)
                                          0
                                          Эта да) Но это явно не прямое назначение backbone, и не предлагаемое автором))
                                          А мой велосипед пока что сам в 1000 строк умещается (что за странный критерий, не могу никак понять: пробельные строки/строки с одной скобкой/с комментами считаются?)
                                            0
                                            А Вы можете предложить критерий взамен этого? Чтоб так же просто вычислялся (как тут — взглядом в статус редактора) и столько же говорил о навороченности кода?
                                              0
                                              Просто — вряд ли. Косвенно оценить объём работы можно было бы по такому критерию — сколько строк (а лучше символов) было написано, а не присутствует в коде.
                                              К примеру, мой проект рефакторился раз пять, поэтому код всего в тысячу строк. Но написано было тысячи две-три строк.
                                              Можно также измерить объём файла минимизированного кода — это точнее даст представление об объёме, но не более.
                                              Можно измерять объём уникального кода.
                                              Но это конечно выдуманные критерии, профессиональный подход, вероятно, другие оценки использует. Но с юзабельной точки зрения мне было бы удобнее эти критерии использовать, и они не намного сложнее.
                                                0
                                                Сколько строк в коде сейчас — посмотреть может каждый, а сколько их было написано — придётся верить туманным прикидкам автора, и не одного. Собирать мнения, суммировать… Минимайзер крда тоже далеко не всегда под рукой. Так что — ИМХО, имеющийся критерий крив, но уникален по доступности и достаточно информативен во многих ситуациях. Другого такого подручного — нет.
                                                  0
                                                  >>уникален по доступности — что это значит?
                                                  Число написанных строк можно подсчитывать напрямую, если работа ведётся в одном редакторе, либо вычислять на основе системы версий.
                                                  В общем случае я уверен также можно придумать костыли, к примеру записывать это число в мета-информацию.
                                                  Но подсчет строк действительно очень прост, тем не менее даже его можно было бы модифицировать, к примеру до числа операций. Зачем скобки и комментарии считать? Плюс некоторые строки с тремя-четыремя операциями можно было бы развернуть.
                                        0
                                        Подход описанный f0rk'ом интересен и актуален, для нас он был актуален еще в '08-мом году когда мы реализовали похожим образом движок inthecity.ru, только клиентская часть — не готовое решение, необходимо также методологически увязать и технически проработать серверную составляющую решения. У нас к примеру ключевые сущности (модуль, инфо-блок, шаблон) имели прямую репрезентацию на клиенте, на сервере и на уровне БД, что позволило сильно автоматизировать процесс сборки конкретного сайта и предоставить заказчику конструктор готовых решений, без потери гибкости в плане его кастомизации.
                                          0
                                          Вас накрыло Хабраэффектом? Или весь сайт танцующий цветочек :)
                                            0
                                            Да, очень интересно, что за конструктор сайтов. Проект ещё не стартовал?
                                            –1
                                            Еще один туториал по бэкбоуну. Таких море. Все об одном. Гораздо интереснее было бы увидеть реализацию какого-нибудь фильтра объектов по разным параметрам с помощью бэкбоун или чего-то действительно интересного. А стандартный туториал — очень похоже на «пишем блог на rails за 5 минут»
                                              0
                                              Кстати, в backbone.js начиная с версии 0.5.0 Controller переименован в Router.

                                              Довольно интересная штука. Думаю в связке с Node.js будет отличная. А кто-нибудь использовал это в связке с Django?

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое