Pull to refresh

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

Reading time 5 min
Views 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>


Все приложение готово. Конечно, это только очень малая часть тех возможностей которые предоставляют данные библиотеки, но думаю, что данного примера достаточно, что почувствовать вкус к разработке, с помощью этих инструментов.
Tags:
Hubs:
+58
Comments 51
Comments Comments 51

Articles