Pull to refresh

Руководство по basis.js. Часть 1: Начало работы, представления, модули, инструменты

JavaScript *

basis.js – JavaScript-фреймворк для разработки одностраничных веб-приложений, ориентированный на скорость и эффективность. Возможно он пока не такой популярный. Но благодаря моим выступлениям на различных конференциях и meetup'ах, некоторые уже слышали о нем и заинтересовались. Однако, чтобы начать использовать фреймворк или разбираться в нем, большинству не хватает руководства.

И вот, собрав волю в кулак (ну какой программист не любит писать документацию?), я сел писать руководство. Просто, доступно, последовательно.

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

В первой части руководства будет рассмотрено как начать работать с basis.js и какие инструменты можно использовать. В качестве примера будет создано несколько простых представлений, затронут вопрос модульности и организации файлов проекта.



Подготовка


Для разработки нам потребуются:

  • консоль (командная строка)
  • локальный веб-сервер
  • браузер (желательно Google Chrome)
  • ваш любимый редактор

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

Dev-сервер


Проекты на basis.js не требуют сборки в процессе разработки. Но для их работы требуется веб-сервер. Может подойти любой веб-сервер, но лучше использовать dev-сервер, входящий в состав basisjs-tools, так как он дает больше возможностей при разработке.

basisjs-tools – набор консольных инструментов, написанный на javascript и работающий под управлением node.js. Этот набор включает в себя сборщик, dev-сервер и кодогенератор. Устанавливается как обычный npm модуль:

> npm install -g basisjs-tools

Если установить инструменты глобально (флаг -g), то в консоли станет доступна команда basis.

Давайте запустим dev-сервер, для этого выполним в консоли простую команду:

> basis server

После этого запустится сервер на порту 8000 (это можно изменить, используя флаг --port или -p). Теперь можно открыть в браузере http://localhost:8000 и убедиться, что сервер работает. Тем не менее он отдает ошибку, так как папка нашего проекта еще пуста. Давайте же исправим это.

Индексный файл и подключение basis.js


Для начала нужно добавить папку с исходниками basis.js в проект. Для этого можно либо клонировать проект из репозитария, либо использовать bower.

> bower install basis

А теперь создадим основной html файл приложения, который пока лишь будет подключать basis.jsindex.html.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config=""></script>
</body>
</html>

Пока ничего необычного. Единственное, что может вызвать вопросы – атрибут basis-config у тега <script>.

Этот атрибут дает возможность ядру basis.js найти тег <script>, которым он был подключен. Это необходимо для того, чтобы определить путь к исходникам basis.js и разрешать пути к его модулям.

Первое представление


Сейчас наша страница как белый лист бумаги – абсолютно пуста. Давайте наполним ее смыслом и выведем традиционное «hello world».

Сделаем это, создав представление с таким отображением. Вот, что должно получиться:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config=""></script>
  <script>
    basis.require('basis.ui');

    var view = new basis.ui.Node({
      container: document.body,
      template: '<h1>Hello world!</h1>'
    });
  </script>
</body>
</html>

Обновив страницу, увидим задуманное – «Hello world!». Рассмотрим, что здесь происходит.

Во-первых, мы сказали, что нам нужен модуль basis.ui, используя функцию basis.require. Эта функция работает практически так же, как функция require в node.js и умеет подключать модули по их имени или по имени файла. В данном случае basis.ui это имя модуля. Как мы увидим дальше, эта функция может «подключить» любой файл по его имени.

Нам потребовался модуль basis.ui, так как он предоставляет все необходимое для построения интерфейса. Этому модулю необходимы другие модули, но об этом не нужно заботиться, оставьте это basis.js. Нужно подключать лишь то, что непосредственно используется в том коде, что пишете вы.

Во-вторых, мы создали само представление, экземпляр класса basis.ui.Node. Пусть вас не смущает название Node, вместо традиционного View. Дело в том, что в basis.js все компоненты и представления вкладываются в друг друга. Так, некоторый блок может выглядеть как единое целое, но на самом деле может состоять из множества вложенных представлений (узлов).

В целом, весь интерфейс организуется в виде одного большого дерева. Его листьями являются узлы (node), которые представляют собой представления и имеют перекрестные ссылки. Можно трансформировать это дерево, добавляя, удаляя и перемещая узлы. Основное API для этого имеет много общего с браузерным DOM. Но мы к этому еще вернемся.

А пока посмотрим как мы создали представление. Для этого мы передали в конструктор объект с «настройками» – конфиг. Задав свойство container мы указали куда нужно поместить DOM фрагмент представления, когда он будет создан. Это должен быть DOM элемент. А в свойстве template указали описание шаблона. Это описание указано в самом конфиге для примера. Такая возможность, указывать описание шаблона строкой в конфиге, удобна для прототипирования и примеров. Но для публикуемых (production) приложений она не используется и позже мы это переделаем.

Модули


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

Давайте вынесем код представления в отдельный модуль. Для этого создадим файл hello.js и перенесем в него то, что было указано в <script>.

Этого оказывается достаточно, и пока больше ничего с кодом делать не нужно. Осталось только подключить модуль в index.html:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config=""></script>
  <script>
    basis.require('./hello.js');
  </script>
</body>
</html>

Здесь снова использована функция basis.require, но на этот раз ей передан путь к файлу. Важно, чтобы путь к файлу начинался с "./", "../" или "/". Так basis.require однозначно поймет, что значение является путем к файлу, а не именем модуля. Такое же соглашение действует и в node.js.

Продолжим разбираться с модульностью. Например, разметка в коде нам ни к чему. А раз так, вынесем описание шаблона в отдельный файл – hello.tmpl. Тогда код представления примет такой вид:

basis.require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  template: basis.resource('./hello.tmpl')
});

Все осталось как прежде, но строка с описанием шаблона заменена на вызов функции basis.resource. Эта функция создает «интерфейс» к файлу. Такой подход дает возможность определять какие файлы нужны, не скачивая их до тех пор, пока нет в этом необходимости.

Интерфейс, создаваемый basis.resource, представляет собой функцию с дополнительными методами. Вызов такой функции, или ее метода fetch, приводит к загрузке файла. Загружается файл лишь раз, а результат кешируется. Больше деталей можно найти в статье Ресурсы (модульность).

Еще один момент: на самом деле, вызов basis.require('./file.name') эквивалентен basis.resource('./file.name').fetch().

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

Преимущества модулей


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

Например, имя файла можно получить из переменной __filename, а папку размещения модуля из переменной __dirname.

Но важнее, что становятся доступны локальные функции require и resource. Они работают так же как basis.require и basis.resource, за исключением того, как разрешаются относительные пути к файлам. Если для функциям basis.require и basis.resource передается относительный путь, то он разрешает относительно html файла (в нашем случае это index.html). В тоже время, require и resource разрешают такие пути относительно модуля (то есть его __dirname).

В модулях удобнее использовать именно локальные функции require и resource. Таким образом, код hello.js немного упрощается:

require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  template: resource('./hello.tmpl')
});

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

То же относится и к css, файлам локализации и другим типам файлов. Единственные изменения, требующие перезагрузки страницы, это изменение html файла и изменение javascript модулей, которые уже инициализированы.

Механизм обновления файлов обеспечивает dev-сервер из basisjs-tools. Это одна из главных причин почему стоит использовать именно его, а не обычный веб-сервер.

Давайте попробуем как это работает. Создадим файл hello.css, такого вида:

h1
{
  color: red;
}

После этого немного изменим шаблон (hello.tmpl):

<b:style src="./hello.css"/>
<h1>Hello world!</h1>

Как только изменения в шаблоне будут сохранены, текст станет красным. При этом совсем не нужно обновлять страницу.

В шаблон мы добавили специальный тег <b:style>. Этот тег говорит, что когда используется данный шаблон, то на страницу нужно подключить заданный файл стилей. Относительные пути разрешаются относительно файла шаблона. Один шаблон может подключать произвольное количество файлов стилей. Нам не нужно беспокоиться о добавлении и удалении стилей, об этом заботится фреймворк.

Итак, мы создали простое статическое представление. Но веб-приложения, это в первую очередь динамика. Так что давайте попробуем использовать в шаблоне значения из представления и как то взаимодействовать с ним. Для первого используются биндинги (bindings), а для второго – действия (actions).

Биндинги и действия


Биндиги позволяют переносить значения из представления в его DOM фрагмент. В отличие от большинства шаблонизаторов, шаблоны basis.js не имеют прямого доступа к свойствам представления. И могут использовать только те значения, что само представление предоставляет шаблону.

Для задания значений доступных шаблону используется свойство binding в описании экземпляра или класса, унаследованного от basis.ui.Node. Значения задаются в виде объекта, где ключ – это имя, которое будет доступно в шаблоне, а значение – функция, вычисляющая значение для шаблона. Таким функциям единственным параметром передается владелец шаблона, то есть само представление. Вот так можно предоставить шаблону значение name:

require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  name: 'world',
  template: resource('./hello.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  }
});

Стоит добавить, что свойство binding является авто-расширяемым свойством. Когда задается новое значение для свойства, при создании экземпляра или класса, то новое значение расширяет предыдущее, добавляя и переопределяя прежние значения. По умолчанию у basis.ui.Node уже есть несколько полезных значений, которые можно использовать наряду с определенным нами name.

Изменим шаблон (hello.tmpl), чтобы использовать name.

<b:style src="./hello.css"/>
<h1>Hello, {name}!</h1>

В шаблонах используются специальные вставки – маркеры. Они используются для получения ссылок на определенные части шаблона и расстановки значений. Такие вставки указываются в фигурных скобках. В данном случае мы добавили {name}, для вставки значения как обычный текст.

Описание шаблона выглядит похожим на формат описания в других шаблонизаторах. Но в отличие от них, шаблонизатор basis.js работает с DOM узлами. Для данного описания будет создан элемент <h1>, в котором будет содержаться три текстовых узла "Hello,", "{name}" и "!". Первый и последний будут статичными и их текст не будет меняться. А вот среднему будет проставляться значение из представления (будет меняться его свойство nodeValue).

Но хватит слов, давайте обновим страницу и посмотрим на результат!

А теперь добавим поле, в которое будем вводить имя и чтобы оно подставлялось в заголовок. Начнем с шаблона:

<b:style src="./hello.css"/>
<div>
  <h1>Hello, {name}!</h1>
  <input value="{name}" event-keyup="setName"/>
</div>

В шаблоне добавился элемент <input>. Для его атрибута value использован тот же биндинг, что и в заголовке – {name}. Но это работает только для записи в DOM.

Для того, чтобы представление реагировало на события в его DOM фрагменте, нужному элементу добавляется атрибут, именем которого является название события с префиксом "event-". Мы можем добавить выполнение действия любому элементу на любое событие. Да и действий на одно событие может быть несколько, главное разделить имена действий пробелом.

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

А теперь добавим описание действия. Для этого используется свойство action. Работает оно аналогично binding, но только описывает действия. Функции в action получают параметром объект события. Это не оригинальное событие, а его копия с дополнительными методами и свойствами (оригинальное событие хранится в его свойстве event_).

Вот как теперь будет выглядеть представление (hello.js):

require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  name: 'world',
  template: resource('./hello.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  },
  action: {
    setName: function(event){
      this.name = event.sender.value;
      this.updateBind('name');
    }
  }
});

Здесь мы читаем значение из event.sender, а это элемент, у которого произошло событие – <input>. Для того чтобы представление заново вычислило значение и передало его шаблону, мы вызвали метод updateBind.

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

Представления, как и модели, умеют хранить данные в виде ключ-значение. Данные хранятся в свойстве data и меняются методом update. Когда меняются значения в data, срабатывает событие update. Воспользуемся этим механизмом для хранения имени:

require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  data: {
    name: 'world'
  },
  template: resource('./hello.tmpl'),
  binding: {
    name: {
      events: 'update',
      getter: function(node){
        return node.data.name;
      }
    }
  },
  action: {
    setName: function(event){
      this.update({
        name: event.sender.value
      });
    }
  }
});

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

require('basis.ui');

var view = new basis.ui.Node({
  container: document.body,
  data: {
    name: 'world'
  },
  template: resource('./hello.tmpl'),
  binding: {
    name: 'data:name'
  },
  action: {
    setName: function(event){
      this.update({
        name: event.sender.value
      });
    }
  }
});

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

Основное, что нужно запомнить. Представление вычисляет и передает значения шаблону, для этого используется binding. А шаблон перехватывает и передает представлению события, вызывая действия из action. Фактически, binding и action основные точки соприкосновения представления и шаблона. При этом, представление практически ничего не знает об устройстве шаблона, а шаблон – об устройстве представления. Вся логика (javascript) находится на стороне представления, а работа с отображением (DOM) – на стороне шаблона. Так, в подавляющем большинстве случаев, достигается полное разделение логики и представления.

Разделение логики и представления


Список


Итак, теперь мы знаем как создать простое представление. Давайте создадим еще одно, немного посложнее – список. Для этого создадим новый файл list.js с таким содержанием:

require('basis.ui');

var list = new basis.ui.Node({
  container: document.body,
  template: resource('./list.tmpl')
});

var Item = basis.ui.Node.subclass({
  template: resource('./item.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  }
});

list.appendChild(new Item({ name: 'foo' }));
list.appendChild(new Item({ name: 'bar' }));
list.appendChild(new Item({ name: 'baz' }));

Код этого модуля похож на hello.js, но добавились новые конструкции.

Прежде чем их разобрать, отметим, что в basis.js используется компонентный подход. Так, если мы делаем, например, список, то это будет не одно представление, а несколько. Одно представление это сам список. И каждый элемент списка – это тоже представление. Так мы отдельно описываем поведение списка, и поведение элементов списка. Чуть более подробно про этот подход, например, рассказано в докладе «Компонентный подход: скучно, неинтересно, бесперспективно»: слайды и видео.

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

Описание самого списка ничем не отличается от того, что мы делали ранее. Далее по коду был создан новый класс, унаследованный от basis.ui.Node. В этом классе указан файл шаблона и простой биндинг. После этого было создано три экземпляра этого класса и добавлены списку.

Как было сказано выше, для организации дерева представлений используются принципы DOM. Для вставки используются методы appendChild и insertBefore, для удаления removeChild, а для замены replaceChild. Так же есть нестандартные методы: setChildNodes позволяет задать список дочерних представлений, а clear – удаляет все дочерние представления махом.

Поэтому уже сейчас можно сделать код немного проще:

require('basis.ui');

var list = new basis.ui.Node({
  container: document.body,
  template: resource('./list.tmpl')
});

var Item = basis.ui.Node.subclass({
  template: resource('./item.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  }
});

list.setChildNodes([
  new Item({ name: 'foo' }),
  new Item({ name: 'bar' }),
  new Item({ name: 'baz' })
]);

Список дочерних узлов можно задать и в момент создания представления. Попробуем:

require('basis.ui');

var Item = basis.ui.Node.subclass({
  template: resource('./item.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  }
});

var list = new basis.ui.Node({
  container: document.body,
  template: resource('./list.tmpl'),
  childNodes: [
    new Item({ name: 'foo' }),
    new Item({ name: 'bar' }),
    new Item({ name: 'baz' })
  ]
});

Самостоятельно создавать однотипные дочерние узлы не так интересно. Хотелось бы просто указывать конфиг и чтобы список сам их создавал, если это необходимо. И такая возможность есть. Это регулируется двумя свойствами childClass и childFactory. Первое задает класс экземпляра, который может быть добавлен как дочерний узел. А второе свойство определяет функцию, которой передается, добавляемое как дочерний узел, значение, которое не является экземпляром childClass. Задача такой функции создать подходящий экземпляр. По умолчанию, эта функция создает экземпляр childClass, используя переданное значение как конфиг:

basis.ui.Node.prototype.childFactory = function(value){
  return new this.childClass(value);
};

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

Таким образом, все что нам нужно, это определить childClass. Тогда станет возможно добавлять новые элементы в список не только создавая экземпляр Item, но и передавая конфиг.

require('basis.ui');

var Item = basis.ui.Node.subclass({
  template: resource('./item.tmpl'),
  binding: {
    name: function(node){
      return node.name;
    }
  }
});

var list = new basis.ui.Node({
  container: document.body,
  template: resource('./list.tmpl'),
  childClass: Item,
  childNodes: [
    { name: 'foo' },
    { name: 'bar' },
    { name: 'baz' }
  ]
});

Продолжим улучшать код. Ведь его можно сделать еще проще.

Класс Item нигде больше не используется, потому нет смысла сохранять его в переменную. Этот класс можно сразу задать в конфиге. Но это не все что мы можем сделать. Когда создается новый класс или экземпляр и некоторое его свойство является классом, а мы хотим создать новый класс на его основе, то не обязательно создавать класс явно, можно просто задать объект с расширениями нового класса. Звучит сложно, но, на самом деле, это все про то, что нам не обязательно указывать basis.ui.Node.subclass, можно просто передать объект. И мы получаем:

require('basis.ui');

var list = new basis.ui.Node({
  container: document.body,
  template: resource('./list.tmpl'),
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      name: function(node){
        return node.name;
      }
    }
  },
  childNodes: [
    { name: 'foo' },
    { name: 'bar' },
    { name: 'baz' }
  ]
});

Вот, так гораздо лучше. Осталось лишь описать шаблоны.

Сначала создаем шаблон списка list.tmpl:

<div id="mylist">
  <h2>Мой первый список</h2>
  <ul{childNodesElement}/>
</div>

Совершенно обычная разметка, за исключением того, что после имени тега ul идет незнакомая конструкция {childNodesElement}. Знакомтесь, это тоже маркер. Так мы говорим, что мы хотим ссылаться на этот элемент по имени childNodesElement. На самом деле, лично нам эта ссылка пока не нужна. Но она нужна представлению списка, что понять куда вставлять DOM фрагменты дочерних узлов. Если ее не указать, то дочерние узлы будут вставляться в корневой элемент (в нашем случае это <div id="mylist">).

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

Теперь нужно создать шаблон для элемента списка (item.tmpl):

<li>
  {name}
</li>

И последнее, нужно подключить модуль в нашу страницу:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config=""></script>
  <script>
    basis.require('./hello.js');
    basis.require('./list.js');
  </script>
</body>
</html>

Обновив страницу, мы увидим наш замечательный список из трех элементов.

Композиция


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

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

Для начала изменим сами модули. Во-первых, нужно убрать использование свойства container, так как их расположение будет определять родительское представление. А во-вторых, нужно чтобы модуль возвращал само представление, чтобы его можно было использовать. Для этого используется exports или module.exports (все как в node.js).

Теперь hello.js примет такой вид:

require('basis.ui');

module.exports = new basis.ui.Node({
  data: {
    name: 'world'
  },
  template: resource('./hello.tmpl'),
  binding: {
    name: 'data:name'
  },
  action: {
    setName: function(event){
      this.update({
        name: event.sender.value
      });
    }
  }
});

А модуль списка (list.js) такой:

require('basis.ui');

module.exports = new basis.ui.Node({
  template: resource('./list.tmpl'),
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      name: function(node){
        return node.name;
      }
    }
  },
  childNodes: [
    { name: 'foo' },
    { name: 'bar' },
    { name: 'baz' }
  ]
});

Как видно, поменялось не много.

У любого приложения обычно есть единственная точка входа. Это такой модуль, который создает корневое представление и делает ключевые настройки. Создадим такой файл app.js:

require('basis.ui');

new basis.ui.Node({
  container: document.body,
  childNodes: [
    require('./hello.js'),
    require('./list.js')
  ]
});

Здесь все уже должно быть знакомо. Можно заметить, что для представления мы не задали шаблон. В этом случае, по умолчанию, будет использоваться пустой <div>. Пока нас устроит.

Осталось поменять сам index.html:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config=""></script>
  <script>
    basis.require('./app.js');
  </script>
</body>
</html>

Два вызова basis.require заменились на один. Но не писать и его, а использовать опцию autoload в basis-config:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>My first app on basis.js</title>
</head>
<body>
  <script src="bower_components/basisjs/src/basis.js" basis-config="autoload: 'app'"></script>
</body>
</html>

Согласитесь, стало гораздо лучше.

И все же осталась небольшая проблема. Да, порядок дочерних представлений задается в корневом представлении. Но они добавляются последовательно, один за другим. А достаточно часто нам необходимо размещать дочерние представления в конкретные точки разметки, более сложной чем просто пустой <div>. Для этого необходимо использовать – сателлиты.

Сателлиты


Сателлиты – это именованные дочерние представления. Этот механизм используется для представлений, которые играют определенную роль и не повторяются.

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

Вот так будет выглядеть app.js, с использованием сателлитов:

require('basis.ui');

new basis.ui.Node({
  container: document.body,
  template: resource('./app.tmpl'),
  binding: {
    hello: 'satellite:hello',
    list: 'satellite:list'
  },
  satellite: {
    hello: require('./hello.js'),
    list: require('./list.js')
  }
});

Здесь все должно быть понятно, код не очень сложный. Это полная запись, явное объявление сателлитов и использование их в биндингах. Но то же можно описать и короче:

require('basis.ui');

new basis.ui.Node({
  container: document.body,
  template: resource('./app.tmpl'),
  binding: {
    hello: require('./hello.js'),
    list: require('./list.js')
  }
});

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

Осталось описать шаблон, задающий разметку и расположение сателлитов:

<div>
  <div id="sidebar">
    <!--{list}-->
  </div>
  <div id="content">
    <!--{hello}-->
  </div>
</div>

Здесь использованы комментарии с маркером. Можно использовать и другие типы узлов, элементы или текстовые узлы. Которые так же будут заменены на корневые элементы сателлитов. Но чаще использование комментариев более выгодно: если не будет необходимого сателлита, то просто ничего не выведется.

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

Реструктуризация файлов проекта


Результатом наших экспериментов стали три основных представления, три модуля и 9 файлов:

Структура файлов


В настоящих приложениях десятки и сотни модулей. А среднее приложение на basis.js это, обычно, 800-1200 файлов. Хранить все файлы в одной папке неудобно и неразумно. Попробуем реструктурировать расположение файлов.

Создадим папку hello и перенесем туда файлы относящиеся к этому модулю (т.е. hello.js, hello.tmpl и hello.css). А так же папку list, в которую перенесем list.js, list.tmpl и item.tmpl. Все что нам осталось – это поменять пути подключения модулей в app.js:

require('basis.ui');

new basis.ui.Node({
  container: document.body,
  template: resource('./app.tmpl'),
  binding: {
    hello: require('./hello/hello.js'),  // здесь
    list: require('./list/list.js')      // и здесь
  }
});

Больше ничего менять не нужно. Можно убедиться, что все работает как прежде, но структура файлов теперь такая:

Структура файлов


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

<!-- было -->
<script src="bower_components/basisjs/src/basis.js" basis-config="autoload: 'app'"></script>

<!-- стало -->
<script src="bower_components/basisjs/src/basis.js" basis-config="autoload: 'src/app'"></script>

Структура файлов должна получится такой:

Структура файлов


Если пойти по пути универсализации, то можно организовать файлы, например, так:

Структура файлов


Так дочерние модули располагаются в папке module. Основной javascript файл модуля называется index.js. Шаблоны и все что к ним относится (стили, изображения и т.д.) располагаются в папке template. Это наиболее частая структура организации проектов на данный момент.

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

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

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

Итоговый результат можно посмотреть здесь.

Инструменты


С ростом приложения растет количество файлов и его сложность. Для того чтобы было проще разрабатывать нужны инструменты. У basis.js есть два вспомогательных инструмента: devpanel и плагин для Google Chrome.

devpanel – это небольшая панель с кнопками, которую можно перетаскивать. Выглядит она так:

devpanel


Для ее подключения нужно добавить такой вызов, лучше всего в основной модуль (app.js):

/** @cut */ require('basis.devpanel');

После перезагрузки страницы, панель должна появиться. Здесь использован специальный комментарий /** @cut */, он позволяет вырезать строки при сборке. Нам ведь не нужно показывать эту панель пользователям, правда?

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

Плагин устанавливается из Google Web Store вот по этой ссылке. Для его работы необходима devpanel, так как она предоставляет API для работы с basis.js.

Плагин предоставляет:

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

Вот так выглядит наше приложение глазами сборщика:

Граф приложения


Сборка


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

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

Сборщик старается самостоятельно разобрать и понять структуру проекта. Ему практически не нужно что-то специально объяснять. Ведь как вы узнаете, где и что подключается и используется? Вы открываете исходный код, читаете и понимаете. Вот так же работает и сборщик basisjs-tools.

Сначала он получает на вход html файл (в нашем случае index.html). Он анализирует его, находит теги <script>, <link rel="stylesheet">, <style> и другие. Понимает какие файлы подключаются. Потом приступает к анализу этих файлов, находит в них конструкции подключающие другие файлы (например, в javascript вызовы basis.require, basis.resource и другие, а в css@import, url(..) и т.д.). Так, рекурсивно обрабатывая все файлы, сборщик строит граф приложения. После этого он анализирует связи, перестраивает и оптимизирует файлы. А результат своей работы складывает в отдельную папку, в виде гораздо меньшего количества файлов.

Давайте соберем проект. Все что для этого нужно — выполнить простую команду:

> basis build

Вот и все. Результатом сборки будут три файла index.html, script.js и style.css, расположенные в папке build. Эти три файла и есть наше приложение в собранном виде. Все что нужно сделать после сборки – это скопировать содержимое папки build на сервер. Все необходимые файлы для работы приложения будут в ней.

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

> basis build --help

Например, самые частые оптимизации, такие как удаление отладочного кода и сжатие javascript и css можно выполнить указав флаг --pack (или его короткую версию -p):

> basis build --pack

Вот что мы увидим в консоли выполнив эту команду:

Результат выполнения basis build --pack

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

Заключение


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

В следующей части будут рассмотрены механизмы работы с данными и их использование совместно с представлениями.



Если вы нашли ошибку, неточность или знаете как улучшить статью – напишите мне на хабрапочту. Эта статья так же доступна на GitHub, вы можете сделать PR и я бережно перенесу правки сюда.

Чтобы вам не пришлось мучать поисковики, привожу список полезных ссылок по теме:

Приветствуются любые вопросы, критика, поддержка и помощь, в чем бы она не выражалась :)
Tags:
Hubs:
Total votes 61: ↑61 and ↓0 +61
Views 25K
Comments Comments 58

Please pay attention