Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров48K
Данная статья является вольным переводом. Оригинал тут.

Введение


Думаю, ни для кого не секрет, что клиентские приложения в современных веб-сервисах становится все сложнее и количество 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>


Все приложение готово. Конечно, это только очень малая часть тех возможностей которые предоставляют данные библиотеки, но думаю, что данного примера достаточно, что почувствовать вкус к разработке, с помощью этих инструментов.
Теги:
Хабы:
Всего голосов 70: ↑64 и ↓6+58
Комментарии51

Публикации