- Пишем одностраничный клиент на javascript
- Введение
- Постановка задачи
- Инструменты
- Взгляд на backbone.js
- Модель
- Коллекции
- Представления
- Соберем все вместе
- Контроллер
- Создание клиента JavaScript
- Добавление библиотеки Knockout
- Создание модели представления
- Добавление пакета скриптов
- Разработка клиент-серверной инфраструктуры на javascript (часть 1 — клиент)
Пишем одностраничный клиент на javascript
Данная статья является вольным переводом. Оригинал тут.
Введение
Думаю, ни для кого не секрет, что клиентские приложения в современных веб-сервисах становится все сложнее и количество JS кода в них растет. До недавнего времени архитектура клиентской части, как правило, разрабатывалась с нуля и была специфична для каждого проекта. Не удивительно что приходилось снова и снова сталкиваться с типичными задачами.
К MVC-фреймворкам на серверной стороне все уже привыкли, но JS код на клиенте часто бывает плохо структурирован.
Предлагаю ознакомиться с решением на базе backbone.js, underscore.js и jQuery, которое поможет решить эту проблему.
Постановка задачи
- Должен быть удобный способ описать модели нашей предметной области.
- Любые изменения в модели должны немедленно отражаться в пользовательском интерфейсе, если модель в нем представлена каким-либо образом.
- Понятная и легко-поддерживаемая структуризация кода в стиле 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');
Выполнить какие-то проверки или иные действия при создании объекта, можно расширив модель функцией
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(), ); 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(); this.movieList.append(view.render().el); >, removeMovie: function(movie) < this.$('#movie_' + movie.cid).remove(); >>);
Ну и собственно индексный файл со всеми зависимостями и шаблонами:
Все приложение готово. Конечно, это только очень малая часть тех возможностей которые предоставляют данные библиотеки, но думаю, что данного примера достаточно, что почувствовать вкус к разработке, с помощью этих инструментов.
Создание клиента JavaScript
В этом разделе вы создадите клиент для приложения с помощью HTML, JavaScript и библиотеки Knockout.js . Мы создадим клиентское приложение поэтапно:
Библиотека Knockout использует шаблон Model-View-ViewModel (MVVM):
- Модель представляет собой представление данных на стороне сервера в бизнес-области (в нашем случае это книги и авторы).
- Представление — это уровень представления (HTML).
- Модель представления — это объект JavaScript, содержащий модели. Модель представления — это абстракция кода пользовательского интерфейса. Он не имеет сведений о представлении HTML. Вместо этого он представляет абстрактные признаки представления, такие как «список книг».
Представление привязано к данным модели представления. Обновления модели представления автоматически отражаются в представлении. Модель представления также получает события из представления, например нажатия кнопок.
- , а затем изменить его позже на таблицу.
Добавление библиотеки Knockout
В Visual Studio в меню Сервис выберите Диспетчер пакетов NuGet. Затем щелкните Консоль диспетчера пакетов. В окне «Консоль диспетчера пакетов» введите следующую команду:
Install-Package knockoutjs
Эта команда добавляет файлы Knockout в папку Scripts.
Создание модели представления
Добавьте файл JavaScript с именем app.js в папку Scripts. (В Обозреватель решений щелкните правой кнопкой мыши папку Скрипты, выберите Добавить, а затем файл JavaScript.) Вставьте следующий код:
var ViewModel = function () < var self = this; self.books = ko.observableArray(); self.error = ko.observable(); var booksUri = '/api/books/'; function ajaxHelper(uri, method, data) < self.error(''); // Clear error message return $.ajax(< type: method, url: uri, dataType: 'json', contentType: 'application/json', data: data ? JSON.stringify(data) : null >).fail(function (jqXHR, textStatus, errorThrown) < self.error(errorThrown); >); > function getAllBooks() < ajaxHelper(booksUri, 'GET').done(function (data) < self.books(data); >); > // Fetch the initial data. getAllBooks(); >; ko.applyBindings(new ViewModel());
В Knockout класс включает привязку observable данных. Когда содержимое наблюдаемого изменения изменяется, наблюдаемый уведомляет все элементы управления с привязкой к данным, чтобы они могли обновить себя. (Класс observableArray является версией массива observable.) Для начала наша модель представления имеет два наблюдаемых элемента:
- books содержит список книг.
- error содержит сообщение об ошибке в случае сбоя вызова AJAX.
Метод getAllBooks выполняет вызов AJAX для получения списка книг. Затем он помещает результат в books массив.
Метод ko.applyBindings является частью библиотеки Knockout. Он принимает модель представления в качестве параметра и настраивает привязку данных.
Добавление пакета скриптов
Объединение — это функция в ASP.NET 4.5, которая упрощает объединение или объединение нескольких файлов в один файл. Объединение сокращает количество запросов к серверу, что может сократить время загрузки страницы.
Откройте файл App_Start/BundleConfig.cs. Добавьте следующий код в метод RegisterBundles.
public static void RegisterBundles(BundleCollection bundles) < // . // New code: bundles.Add(new ScriptBundle("~/bundles/app").Include( "~/Scripts/knockout-.js", "~/Scripts/app.js")); >
Разработка клиент-серверной инфраструктуры на javascript (часть 1 — клиент)
О чем эта статья. Я хочу поделиться опытом разработки мобильного приложения на phonegap. В итоге получился целый програмный комплекс с RESTfull сервером, клиентами, да еще хостится на PaaS. Поэтому я опишу отдельно архитектуру клиентского приложения (html5 single page app, завернутое в phonegap), серверного (nodejs с swagger-node-express + node-orm2), и как разместить все это на openshift PaaS.
Разработка клиент-серверной инфраструктуры на javascript (часть 2 — сервер и размещение)
Думаю, сайт сразу ляжет от хабраэффекта. Это триальный аккаунт с всего-лишт двумя слотами для ноды. Но об этом в конце.
Хочу сразу уточнить — проект не закончен, но уже стабильно работает, плюс можно показать основные части и архитектурные решения.
Начнем, пожалуй, с самого главного — клиента. Я использовал (ex)twitter bootstrap, я понимаю что шаблон пока не очень — но главное это js логика. Само приложение построено на require.js, хотя я сам не против минимизации всего проекта. Дело в том, что на телефоне файлы быстро будут подгружатся, а для сайта, вообще-то, планируется отдельное приложение в дальнейшем. В качестве javascript фреймворка я выбрал marionette.
Сейчас реализовано два основных модуля: Auth и Conferences.
Отключил автостарт:
this.startWithParent = false;
И вручную запускаю их после инициализации главного модуля.
require( [ 'css!bootstrap_css', 'bootstrap', 'app/modules/conferences', 'app/modules/auth', ], function () < app.start(); >);
MyConference.addInitializer(function(options)< mainLayout = new MainLayout; MyConference.mainView.show(mainLayout); var headerView = new HeaderView; headerView.MyConference = MyConference; mainLayout.header.show(headerView); MyConference.Conferences.start(); MyConference.Auth.start(); >);
Auth — регистрация/авторизация. Хочу обратить внимание на социальную авторизацию. Я всегда сам реализую авторизацию и не пользуюсь сторонними агрегаторами, незнаю хорошо это или плохо. Реализовано Google, LinkedIn, Facebook, Twitter, можете просто взять мой код, если вам нужно реализовать у себя что-то похожее. Суть социальной авторизации в том, что я с помощью js на клиенте получаю Api key, а потом передаю его на сервер для проверки. Например facebook:
var afterInit = function()< var sendAccessToken = function(response)< $.post( cfg.baseUrl + 'auth.json/facebook', , function(data, message, xhr)< process_social_resporce(model, data, xhr); >, "text" ); > FB.getLoginStatus(function(response) < if(response.status == "not_authorized" || response.status == "unknown")< FB.login(function(response, a) < if (response.authResponse) < sendAccessToken(response); >else < console.log(response, a) >>, ); >else < sendAccessToken(response); >>); > window.fbAsyncInit = function() < FB.init(< appId : cfg.facebookAppId, // App ID status : true, // check login status cookie : true, // enable cookies to allow the server to access the session xfbml : true // parse XFBML >); afterInit(); >; // Load the SDK asynchronously (function(d) < var js, , ref = d.getElementsByTagName('script')[0]; if (d.getElementById(id)) js = d.createElement('script'); js.id = id; js.async = true; js.src = "//connect.facebook.net/en_US/all.js"; ref.parentNode.insertBefore(js, ref); >(document));
Для постов/лайков мне этот ключ не нужен, только узнать что это за пользователь. Так что, если вам нужно использовать offline доступ, то такой метод может не сработать из-за того, что нужно получать отдельный ключ, который js клиентам не выдают.
Отдельная история с twitter. У него нет браузерной клиентской авторизации, поэтому я реализовал серверную. Открывается окошко, там пользователь авторизируется и потом родительское окно считывает ответ с дочернего. Это может работать не во всех браузерах, так что, скорее всего, придется немного его изменить. Но в android проложении работает нормально.
var childWin = window.open(cfg.baseUrl + 'auth.json/twitter/'+Storage.get('API_KEY'), 'Twitter Auth', "height=640,width=480"); childWin.onunload = function() < var check = function()< if(childWin.document)< var body = childWin.document.getElementsByTagName("body")[0]; if(!model.isNew() || body.textContent.length >0)< process_social_resporce(model, body.textContent); childWin.close(); >else < setTimeout(check, 100); >>else < setTimeout(check, 100); >> setTimeout(check, 100); >
Теперь перейдем к основному модулю — Conferences. Здесь, на самом деле, все очень просто. Описываю контролер з роутами.
var ConferencesController = Marionette.Controller.extend(new function()< return < main: function()< MyConference.mainView.currentView.header.currentView.setHeader('Conferences'); var conferencesCollection = new ConferencesCollection; var spinnerView = new SpinnerView(); spinnerView.render(); conferencesCollection.fetch(< error: function()< console.log('error'); >, success: function(collection) < var mainView = new MainView; mainView.collection = conferencesCollection; MyConference.mainView.currentView.content.show(mainView); spinnerView.remove(); >>) >, conference: function(id)< var conferenceModel = new ConferenceModel; conferenceModel.set('id', id); conferenceModel.fetch(< error: function()< var conferenceNotFoundView = new ConferenceNotFoundView; MyConference.mainView.currentView.content.show(conferenceNotFoundView); >, success: function(conference) < var conferenceFullView = new ConferenceFullView; conferenceFullView.model = conference; MyConference.mainView.currentView.content.show(conferenceFullView); >>); >, streams: function(conference_id) < ShowStreams( conference_id, function()< ShowStream(streams.at(0).get('id')); >); >, stream: function(id) < ShowStream(id); >> >); var MainRouter = Backbone.Marionette.AppRouter.extend(< appRoutes: < "conference/:id": "conference", "conferences": "main", "": "main", "streams/:conference_id": "streams", "stream/:id": "stream" >, controller: new ConferencesController >);
Как видно, создается обычная бекбоновская модель/коллекция, получаются данные и передается в вьюху. Список конференций это обычный CollectionView. Детальнее остановлюсь для View подробного описания конференции. Поддержка OpenStreetMap и GoogleMaps реализована вручную. Можно, конечно, использовать Leaflet, но я не уверен, что гуглу нравится прямой доступ к их картинкам. Так же там картинка/pdf отображается или ссылка на файл, если есть. И вверху справа ссылка на список докладов.
Если пользователь залогинен, он видит три кнопки, «пойти» на конференцию, в избранное и отказаться. Детальное описание я наследовал не от ItemView, а от Layout, поэтому просто определил блок, куда рендерить эти три кнопки.
И в зависимости от статуса пользователя, показываю ту или иную вьюху.
if(MyConference.Auth.getUser().isNew())< var view = new GuestDecisionView; >else< var desisionModel = new DecisionModel(view.model.get('decision')); var view = new LoggedInDecisionView(); view.parent = this; > this.decision.show(view);
Осталось только собрать и запустить проет на телефоне. Если нужно додать платформу выполните «cordova platform add android»
cordova platform add android
Чтобы посмотреть у себя на телефоне.
Конечной целью является размещение приложений в Google play, Apple App Store и Windows Store. Но основной моей деятельностью является web, а не мобильная, разработка, поэтому я еще не зарегистрирован как разработчик ни в одном из этих магазинов.
Надеюсь кому-то будет полезна эта статья. Я пытался не сильно ее раздувать, но обратить внимание на все основные моменты. Буду рад критике, пожеланиям, pull request’ам в репозиторий. Из-за того, что материала вышло много, я его разбил на две статьи — клиент и сервер. В следующей статье я опишу создание restfull сервера на nodejs с orm’ом автодокументацией и memcached’ом. И как я деплоил все это на PaaS от RedHat — openshift.