Статьи

Сценарии: переход на сторону клиента с листовкой, позвоночником и жасмином

Мы наконец прибыли в момент карты. Для последних нескольких статей в серии Loccasions я обещал такие вещи, как «в следующем посте мы разберемся с картой» и «Я снизлю налоги», и я не выполнил. В этом посте я выполню хотя бы одно из этих обещаний.

Добавление карты в это приложение — почти полностью клиентское предложение. Таким образом, этот пост (и один или два следующих за ним) будет метрической тонной JavaScript и достоинством Ruby.

Библиотеки, фреймворки и карты, О, МОЙ!

Как я уже упоминал при настройке этого приложения, я использую Backbone в качестве основы для javascript и Jasmine в качестве моей среды тестирования на стороне клиента. Я оправдываю этот выбор тем, что мне нравятся оба фреймворка … очень.

Однако я не использую драгоценный камень для генерации файлов Backbone или волшебным образом подключаю Backbone к серверу. План состоит в том, чтобы написать классы Backbone с нуля, добавив в конфигурацию, чтобы они взаимодействовали с сервером по мере необходимости. Я абсолютно ничего не имею против использования драгоценного камня для Backbone и rails, за исключением того, что, похоже, существует путаница относительно того, кто что делает (если вы идете в репозиторий rails-backbone , он говорит вам использовать gem "backbone-rails" , но если Вы идете в репозиторий backbone-rails , он говорит вам использовать gem "rails-backbone" . Я был в бесконечном цикле, глядя на них обоих, спас только мою жену, отключив электричество в моем офисе.)

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

Для Жасмин я использую жасминовый камень, потому что он поддерживается Pivotal, и они качаются. Однако для Loccasions я буду использовать определенную ветку гема, которая способна запускать спецификации и исходные файлы javascript через конвейер ресурсов. В конце концов, мы используем Rails 3.1.

В наши дни все веб-карты — это JavaScript. Выбор подходящей среды js-map был немного утомительным, особенно с учетом того, что большую часть своей карьеры я потратил на его использование (ArcGIS Server ESRI, если вам интересно.

  • Google Maps отсутствует из-за возможности Google вытащить Сумасшедшего Ивана по поводу лицензирования.
  • Yahoo нет, потому что, честно говоря, потому что он мертв.
  • Я кратко посмотрел на OpenLayers и, возможно, все еще использую его, поскольку это настоящий открытый исходный код, который мне нравится.
  • Я также попытался использовать Mapstraction , которая абстрагирует провайдера и позволяет вам менять провайдеров на лету. Однако, когда я попытался использовать OpenLayers с Mapstraction, я не мог понять, как изменить тему, поэтому я пошел дальше.
  • Наконец, я остановился на Leaflet , в основном потому, что он выглядит великолепно, и мне пока нравится API.

Последний бит — это дополнение к применению. Я использовал Backbone несколько раз, и вам обычно нужны шаблоны для представления вашей модели и данных коллекции в HTML. Я действительно не хотел вводить другой язык шаблонов (в дополнение к хамлу), если бы мог избежать этого, поэтому я нашел хамл-кофе . Этот драгоценный камень позволяет вам писать свои шаблоны с использованием HAML, а затем делает их доступными в вашем javascript на клиенте. Мы рассмотрим хотя бы один сценарий, который ясно показывает это в следующем посте.

Настроить

Уф! Теперь давайте настроим приложение со всеми нашими новыми вкусностями на стороне клиента. Сначала загрузите весь код, который нам понадобится:

  • Leaflet помещает это в vendor / assets / javascripts / leaflet / leaflet.js Leaflet также имеет таблицу стилей CSS и изображения. Поместите их (каталог css-файлов и изображений) в vendor / assets / stylesheets / leaflet
  • Backbone поместите это в vendor / assets / javascripts / Backbone / Backbone-min.js
  • Подчеркните, поместите это в vendor / assets / javascripts / Backbone / underscore-min.js

Я также создал файл vendor / javsascripts / vendor.js, который будет загружать эти файлы. Конвейер активов будет делать это автоматически, но я — фанат контроля.

 // vendor/assets/javascripts/vendor.js //= require leaflet/leaflet //= require Backbone/underscore-min //= require Backbone/Backbone-min 

Точно так же я создал файл vendor / stylesheets / vendor.css для Leaflet CSS:

 /* * This is the vendor.css in our vendor/assets/stylesheets dir *=require leaflet/leaflet * */ 

Наконец, добавьте пару драгоценных камней в наш Gemfile:

 group :assets do ... gem 'haml-coffee' end group :test, :development do ... gem 'jasmine', :git => 'git://github.com/pivotal/jasmine-gem.git', :branch => '1.2.rc1' end 

Вышеупомянутые (я использую это слово) haml-coffee и jasmine (помните, мы используем ветку) драгоценные камни. О, и я уверен, что вы не забыли bundle install , верно?

Теперь пришло время настроить jasmine, поэтому запустите rails g jasmine:install для настройки поддержки Jasmine в приложении. Жасмин поставляется с собственным сервером, который запускается набрав rake jasmine . Будучи опытными пользователями Foreman (помните последний пост ), мы добавим его в наш Procfile:

 web: rails s db: mongod --dbpath=/Users/ggoodrich/db/data test: guard jasmine: bundle exec rake jasmine 

В следующий раз, когда ты foreman start , будет бежать Жасмин Сервер Jasmine работает по адресу http: // localhost: 8888 , но он не будет очень интересным, пока мы не добавим некоторые спецификации.

Файлы спецификации добавляются в Jasmine в файле spec / javascript / support / jasmine.yml . Поскольку мы используем ветку, которая поддерживает конвейер ресурсов, наш файл jasmine.yml немного отличается от того, который использовался в текущем выпуске. Вот суть того, что я настроил для Loccasions. Одним из изменений, которое я сделал, было добавление наших файлов coffeescript (это то, что дает нам ветка) и добавление вручную javascripts наших поставщиков (jQuery, Underscore, Backbone и Leaflet). Другое изменение — последняя строка файла, идентифицирующая приложение / ресурсы как путь, который должен обслуживаться конвейером активов.

Наконец, я хочу использовать Sinon для насмешек и окурков, когда это необходимо в моих тестах javascript. Здесь есть хороший плагин для Жасмин для Синона. Загрузите оба этих файла в наш каталог spec / javascripts / helpers / . Вам нужно будет добавить файл spec / javascripts / helpers / jquery.js (вы можете скопировать его с сайта jQuery или из гема jquery-rails), потому что Jasmine еще не загрузит jQuery (они работают над этим… .).

Я бы посоветовал прочитать этот сериал о Жасмин и Синоне, чтобы узнать, как все это сочетается. Вы без сомнения заметите его влияние на Locccasions.

Вот так … просто как очень тяжелый пирог.

Структуры каталогов на стороне клиента и женщины, которые их любят

На каждые две вводные статьи по Backbone есть одна, посвященная структурированию кода Backbone . Структура файлов относится только к разработке, так как конвейер ресурсов поместит весь javascript в один файл application.js. Прочитав некоторые статьи, я пришел к следующему:

Наш Хребет

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

Как я уже говорил, конвейер ресурсов в Rails 3.1 будет загружать все эти вещи для вас, но я хочу контролировать порядок загрузки вещей. Поэтому нам нужно внести изменения в ваш файл app / assets / javascript / application.js , например так:

 //= require jquery //= require jquery_ujs //= require vendor //= require ./app //= require ./router //= require_tree ./lib //= require_tree ./models //= require_tree ./collections //= require_tree ./views //= require_tree ./templates 

Настройка завершена, что теперь?

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

Что ты видишь

Один из способов подойти к этой странице с помощью Backbone — это разделить страницу на виды, например так:

Что видит позвоночник

Такой подход принят, и нам должно хватить, чтобы начать писать тесты.

Джентльмен, прямо сейчас на этапе 3, сложите руки для JAAASSSMMIIIIINE

Давайте начнем с просмотра карты. Вот наш первый тест Жасмин

 // spec/javascripts/views/mapView_spec.js describe("MapView", function() { describe("initialize", function() { beforeEach(function() { loadFixtures("map.html"); }); it("should use the #map element by default", sinon.test(function() { })); it("should create a map", sinon.test(function() { })); }); }); 

Вы можете видеть, что Jasmine очень похож на любой синтаксис BDD-фреймворка. Существуют блоки «description» (которые могут быть вложенными) и блоки «it» для проверки конкретного поведения. Кроме того, наш beforeEach позволяет нам загрузить файл beforeEach . В этом случае наш файл фикстуры добавляет на страницу div#map (на самом деле это просто <div id='map'></div> ), которая будет содержать нашу карту для тестов. Клиентское тестирование часто зависит от структуры разметки, поэтому возможность загружать файлы фикстур (которые Jasmine будет выгружать после теста) действительно хороша.

Наши тесты просмотра карты просты. Во-первых, убедитесь, что представление использует элемент #map . Во-вторых, убедитесь, что он создает «карту».

В качестве краткого примечания вы могли заметить, it функции it обернуты с помощью sinon.test() . Это создает «песочницу» для теста. Если у MapView есть какие-либо зависимости (и они есть), мы будем заглушки / насмешки по мере необходимости. Песочница позволяет легко восстанавливать любые заглушки или насмешливые объекты после завершения теста.

Что такое карта? Я уже упоминал, что мы будем использовать Leaflet, но я не уверен, что мы не будем менять поставщиков карт. Таким образом, мы должны скрыть карту за абстракцией в представлении. Кроме того, мне не нужно загружать реальную карту Leaflet для моих тестов. Поэтому я создал концепцию «MapProvider», которую я передаю представлению инициализации. Мы можем использовать Sinon, чтобы издеваться над поставщиком услуг, не допуская проблем с тестами.

Интерфейс / протокол MapProvider сейчас очень прост:

 App.MapProviders.Leaflet = -> # Create new map createMap: (elementId) -> addBaseMap: ()-> addLayerToMap: (layer) -> setViewForMap: (options) -> 

Всего четыре функции, названные так, чтобы сделать их цель очень очевидной. Реализация поставщика Leaflet здесь, если вы заинтересованы.

Давайте завершим наши 2 теста вида карты:

 it("should use the #map element by default", sinon.test(function() { // Arrange var mp = new App.MapProviders.Leaflet(); var mapSpy = this.stub(mp, "createMap"); var setViewSpy = this.stub(mp,"setViewForMap"); //Act var view = new App.MapView({ mapProvider: mp }); //Assert expect(view.el.id).toEqual("map"); mapSpy.restore(); setViewSpy.restore(); </code></pre> })); it("should create a map", sinon.test(function() { //Arrange var mp = new App.MapProviders.Leaflet(); var mapProviderMock = this.mock(mp); mapProviderMock.expects("createMap").withArgs("map").once(); mapProviderMock.expects("setViewForMap").once(); //Act var view = new App.MapView({ mapProvider: mp }); //Assert mapProviderMock.verify(); })); 

Эти два теста показывают разные виды тестирования. Первый тест гарантирует, что MapView делает правильные вещи, при условии, что его зависимость делает правильные вещи. Нам не важно, что MapProvider делает для этого теста, потому что это не имеет отношения к результату теста. Таким образом, мы отключаем эти методы, что не дает вызовам добраться до API-интерфейса Leaflet, при этом следя за тем, чтобы они не выдавали ошибку.

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

Перезагрузив нашу тестовую страницу Жасмин (http: // localhost: 8888, помните?), Мы видим:

Провал

Мы можем это исправить.

Я Карта [Просмотр]!

Есть ли там поклонники Доры? Нет? Верно, я тоже Вот реализация MapView:

 ### app/assets/javascripts/views/mapView.js ### class App.MapView extends Backbone.View el: "div#map", initialize: -> @mapProvider = this.options.mapProvider @initialCenter = this.options.initialCenter || { latitude: 51.505, longitude: 0.09 } @render() setInitialView: -> @mapProvider.setViewForMap latitude: @initialCenter.latitude, longitude: @initialCenter.longitude zoomLevel: 13 render: -> @mapProvider.createMap(@el.id) @setInitialView() 

После добавления этого файла перезагрузка тестовой страницы Jasmine выглядит следующим образом:

Проходят

Знаете ли вы путь к карте, Хосе?

Итак, у нас есть MapView. Как нам заставить нашу страницу использовать ее? Я имею в виду, я не вижу карту, когда захожу на свою страницу «/ events» в Loccasions. Именно здесь вступает в игру «Маршрутизатор» Backbone (или «Формально известный как художник»). В этом случае мы хотим направить «корневой маршрут» или «/» в место, где он знает, чтобы создать наш MapView. Я написал пару спецификаций для выполнения этого требования:

 // spec/javascripts/appRouter_spec.js describe("AppRouter", function() { describe("index", function() { beforeEach(function() { loadFixtures("map.html"); this.mapViewStub = sinon.spy(App.MapView.prototype, "initialize"); window.bootstrapEvents = []; }); afterEach(function(){ this.mapViewStub.restore(); }); it("should create a map view", function() { this.router = new App.Router(); this.router.index(); expect(this.mapViewStub).toHaveBeenCalled(); }); }); describe("/", function () { it("should respond to empty hash with index", function() { this.router = new App.Router(); this.routeSpy = sinon.spy(); try { Backbone.history.start({silent:true, pushState:true}); } catch(e) {} this.router.navigate("elsewhere"); this.router.bind("route:index", this.routeSpy); this.router.navigate("", true); expect(this.routeSpy).toHaveBeenCalledOnce(); expect(this.routeSpy).toHaveBeenCalledWith(); }); }); }); в // spec/javascripts/appRouter_spec.js describe("AppRouter", function() { describe("index", function() { beforeEach(function() { loadFixtures("map.html"); this.mapViewStub = sinon.spy(App.MapView.prototype, "initialize"); window.bootstrapEvents = []; }); afterEach(function(){ this.mapViewStub.restore(); }); it("should create a map view", function() { this.router = new App.Router(); this.router.index(); expect(this.mapViewStub).toHaveBeenCalled(); }); }); describe("/", function () { it("should respond to empty hash with index", function() { this.router = new App.Router(); this.routeSpy = sinon.spy(); try { Backbone.history.start({silent:true, pushState:true}); } catch(e) {} this.router.navigate("elsewhere"); this.router.bind("route:index", this.routeSpy); this.router.navigate("", true); expect(this.routeSpy).toHaveBeenCalledOnce(); expect(this.routeSpy).toHaveBeenCalledWith(); }); }); }); 

В этом случае мы используем третью короткую шутку Синона, называемую «шпион». Шпион похож на макет, но вы не можете установить для него возвращаемое значение. Шпион существует только для записи, был ли он вызван и какие аргументы использовались для его вызова. Это хорошо работает для нашего первого теста, где мы проверяем, что метод инициализации в нашем MapView вызывается. Во втором тесте используется шпион, чтобы убедиться, что при переходе пользователя к «корневому маршруту» вызывается функция индекса AppRouter #.

Перезагрузка нашей страницы Jasmine дает нам соответствующие сбои, поэтому давайте напишем наш класс AppRouter:

 ### app/assets/javascripts/router.js.coffee< ### class App.Router extends Backbone.Router routes: "" : "index" index: -> if $('#map').length > 0 @mapView = new App.MapView( mapProvider: new App.MapProviders.Leaflet() ) 

Выполнено. Спецификации проходят.

Начни меня

Последнее, что нам нужно сделать, — это загрузить код нашего приложения Backbone. Другими словами, мы должны сказать Backbone, чтобы установить маршрутизатор, создать все представления и все остальное, что ему нужно сделать. Я создал небольшой файл coffescript для этого:

 ### app/assets/javascripts/app.js.coffee ### window.App = start: -> new App.Router() Backbone.history.start( pushState: true, root: "/events" ) $(App.start) 

Этот файл определяет пространство имен моего приложения ( App ) и создает нашу функцию start . функция запуска просто создает наш маршрутизатор и сообщает Backbone начать обработку маршрутизации. Если вы хотите точно понять, что делает Backbone.history.start , посмотрите здесь .

Обновить

Г-н Генри (из комментариев ниже) отметил следующее:

  • Добавьте div #map в ваши события / index.html.haml
  • Добавьте #map {height: 350px} в приложение / assets / stylesheets / events.css.scss

Вот страница с картой:

Что ты видишь

У нас еще много работы на этой странице. Тем не менее, это хорошая остановка, и я хочу немного поиграть с картой.

Мой блоггер пошел повсюду, и все, что я получил, было этой паршивой картой

Я знаю, это была тонна работы, чтобы получить карту. Тем не менее, мы также теперь имеем:

  • Структура нашего клиентского кода.
  • Способ вытеснения клиентского кода с помощью тестов (TDD / BDD / ETC)
  • КАРТА!

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