Сегодняшняя часть веб-приложения становится очень сложной. Клиенты ожидают функциональных высокопроизводительных приложений с красивым пользовательским интерфейсом, которые работают в браузере, как обычные настольные приложения. Чтобы выполнить эти требования, мы приходим к одностраничным приложениям (SPA). SPA заставляет нас писать много кода на стороне клиента (обычно это JavaScript). Многие клиентские скрипты со временем часто превращаются в неконтролируемый беспорядок.
Решение состоит в том, чтобы структурировать приложение с использованием клиентских сред. Backbone.js является одной из таких таблеток. Это JavaScript-фреймворк с открытым исходным кодом MVC. Разработчик должен описать модели и представления, настроить привязки событий, определить маршрутизацию и предоставить сервис данных (REST-подобный сервис JSON или XML) для своего приложения.
В этом уроке я хочу показать, как использовать Backbone.js и службу ODATA WCF для создания SPA. Я собираюсь использовать образец базы данных Microsoft Northwind. Приложение содержит три страницы: список категорий, список товаров из выбранной категории и сведения о товаре с возможностью его изменения.
Исходный код приложения доступен на GitHub: https://github.com/tabalinas/BackboneOData
Вы готовы? Итак, начнем.
Подготовить заявку
Начните с создания простого веб-приложения ASP.NET с одной страницей Default.aspx в качестве главной страницы SPA.
Откройте Visual Studio и создайте пустое веб-приложение ASP.NET
Проект имеет следующую структуру
- Сценарии — код JavaScript
- Служба — содержит файлы сопоставления .edmx и файлы службы .svc
- Стили — CSS стили
- Default.aspx — главная страница SPA
WCF ODATA Сервис
В демонстрационных целях мы могли бы использовать одну из общедоступных служб ODATA, например, http://services.odata.org/Northwind/Northwind.svc . Но в этом случае одна и та же политика происхождения вызовет некоторые проблемы с манипулированием данными. Итак, давайте создадим нашу собственную службу ODF WCF.
Сначала создайте файл EDMX
Выберите «Создать из базы данных», настройте соединение с вашим MS SQL Server и выберите Northwind в качестве исходной базы данных. Затем выберите объекты БД, которые мы хотим отобразить. Для нашего приложения я выбрал категорию, продукт и поставщика.
Нажмите «Готово». Наш файл .edmx готов
Теперь создайте сервис данных WCF на основе созданного отображения. Дайте ему имя «Northwind.svc»
Измените код услуги следующим образом
public class Northwind : DataService<NORTHWNDEntities> { // This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config) { // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc. // Examples: config.SetEntitySetAccessRule("*", EntitySetRights.All); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3; } }
Поместите класс отображения в универсальный тип DataService (в нашем случае NORTHWINDEntities ).
Предоставить доступ к объектам. Для демонстрации я дал полный доступ ко всем сущностям.
Сервис готов предоставить данные.
Основные компоненты
Как я уже упоминал, Backbone — это фреймворк MVC. Приложение Backbone имеет следующие компоненты:
- Модель описывает сущности и бизнес-логику приложения.
- Коллекция представляет собой партию сущностей. Это как модель другого типа, представляющая коллекцию конкретных моделей.
- Вид — это визуальное представление конкретной модели. Здесь мы можем свободно использовать любой шаблонизатор. Подчеркнуть шаблоны являются наиболее распространенными, хотя.
- Маршрутизатор похож на маршрутизацию в ASP.NET MVC только на стороне клиента. Он описывает навигацию приложения, маршруты и связанные методы обработки.
Меньше слов, больше действий. Давайте создадим страницу со списком доступных категорий продуктов от Northwind и покажем, как заставить все это работать вместе.
Список категорий
Категория Модель
app.Models.Category = Backbone.Model.extend({ url: function() { return "Service/Northwind.svc/Categories(" + this.get("CategoryID") + ")"; }, idAttribute: "CategoryID", defaults: { CategoryID: 0, CategoryName: "", Description: "" } });
Все модели расширяют Backbone.Model . приложение здесь наше корневое пространство имен приложения. Вы можете назвать это как хотите. app.Models — это пространство имен для всех моделей.
Свойства модели:
- url возвращает URL, по которому сущность может быть достигнута на сервере. В нашем случае это «Service / Northwind.svc / Categories ({CategoryID})».
- idAttribute сообщает, какое поле содержит идентификатор объекта.
- По умолчанию это объект, содержащий поля модели со значениями по умолчанию.
Конечно, мы должны поместить все методы бизнес-логики, связанные с категорией, в определение модели.
Коллекция категорий
app.Models.CategoryList = Backbone.Collection.extend({ url: "Service/Northwind.svc/Categories", model: app.Models.Category, load: function(callback) { this.fetch({ success: callback }); } });
Коллекции должны расширять Backbone.Collection .
Свойства коллекции:
- URL-адрес — это URL-адрес коллекции сущностей на сервере.
- Модель является ссылкой на родственный класс модели. Свойство определяет элементы, которые содержат коллекцию типов.
- load — выборочный метод выборки коллекции методовс успешным обратным вызовом. Этот метод инкапсулирует выборку коллекции, поэтому мы можем легко изменить ее в будущем (например, добавить фильтрацию / сортировку или что-то еще).
Вид по категориям
app.Views.CategoryListView = Backbone.View.extend({ template: _.template($("#categoryListViewTemplate").html()), className: "category-list-view", render: function() { this.$el.html(this.template(this.model.toJSON())); return this; } });
Каждое представление расширяет Backbone.View .
Посмотреть свойства:
- template — это настраиваемое поле (не принадлежит Backbone.View ), ссылающееся нашаблон Underscore представления.
- className — это имя класса CSS, добавляемое в контейнер корневых тегов представления. Div используется по умолчанию, но мы можем переопределить его, указавсвойство tagName .
- render — основной метод представления, выполняющего фактическое отображение. В нашем случае мы помещаем результат рендеринга шаблона в контейнер корневого представления.
Default.aspx и Underscore Просмотреть шаблон
Default.aspx должен иметь следующую разметку
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="BackboneApp.Default" %> <!DOCTYPE html> <html> <head runat="server"> <title>Backbone.js with OData Demo Application</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="Styles/bootstrap.css" rel="stylesheet" /> <link href="Styles/style.css" rel="stylesheet" /> </head> <body> <!-- app page --> <form id="form1" runat="server"> <header class="container"> <h3 id="title"></h3> </header> <div id="content" class="container"> </div> </form> <!-- templates --> <script id="categoryListViewTemplate" type="text/template"> <div class="well"> <ul class="list"> {% _.each(this.model.models, function(category) { %} <li> <h4><a href="#category/{{ category.get("CategoryID") }}">{{ category.get("CategoryName") }}</a></h4> <p>{{ category.get("Description") }}</p> </li> {% }); %} </ul> </div> </script> <!-- scripts --> <script src="Scripts/jquery-1.7.1.js"></script> <script src="Scripts/underscore.js"></script> <script src="Scripts/backbone.js"></script> <script src="Scripts/App.js"></script> <script src="Scripts/Models/Category.js"></script> <script src="Scripts/Models/CategoryList.js"></script> <script src="Scripts/Views/CategoryListView.js"></script> </body> </html>
Тело Default.aspx состоит из трех разделов. Первая — разметка страницы приложения. Второй раздел — определение шаблонов. Третий имеет ссылки на необходимые скрипты.
Разметка страницы приложения — это форма, содержащая два дочерних html-элемента: заголовок с заголовком и основной div для разметки содержимого.
Шаблон для списка категорий — это ul с li для каждой категории. Модель для шаблона — это коллекция, поэтому мы должны перебрать массив model.models, чтобы отобразить каждую категорию. Название категории — это ссылка на список товаров в категории. Я использовал Bootstrap , чтобы сделать демо повеял, так что есть некоторые классы конкретных CSS любят хорошо .
Все шаблоны расположены на странице Default.aspx. Чтобы улучшить дизайн приложения, шаблоны могут храниться в отдельных файлах и загружаться асинхронно по требованию.
Невозможно использовать шаблон Underscore с настройками по умолчанию на странице aspx, поскольку он содержит конфликтующие с тегами управления синтаксиса asp <%%>. Чтобы решить проблему, нам нужно выполнить следующий код перед запуском приложения.
// underscore template settings to prevent conflict with ASP tags <% %>
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g, // print value: {{ value_name }}
evaluate: /\{%([\s\S]+?)%\}/g, // execute code: {% code_to_execute %}
escape: /\{%-([\s\S]+?)%\}/g // excape HTML: {%- <script> %} prints <script>s
};
В разделе скриптов есть ссылки на необходимые скрипты. Магистраль требует jQuery (или Zepto) и Underscore (или Lo-Dash). App.js является основным скриптом приложения. Также есть скрипты наших моделей и видов.
App.js и маршрутизация
Пространство имен и маршрутизация корневого приложения определены в App.js. Метод app.renderView визуализирует представление Backbone и помещает результат в div содержимого страницы.
// root namespace for application window.app = { Models: {}, Views: {}, renderView: function(view, title) { $("#content").html(view.render().el); $("#title").text(title); } }; // app router app.ApplicationRouter = Backbone.Router.extend({ routes: { "": "categoryList" }, categoryList: function() { var categoryList = new app.Models.CategoryList(), categoryListView = new app.Views.CategoryListView({ model: categoryList }); categoryList.load(function() { app.renderView(categoryListView, "Categories List"); }); } });
Приложение роутер расширяет Backbone.Router . Маршруты объектов — это карта между шаблонами URL и методами, обрабатывающими запросы к этим URL. В нашем случае мы говорим, что пустой URL ссылается на метод categoryList. Метод categoryList создает модель и соответствующий вид. Затем он получает коллекцию с сервера и отображает представление.
В конце мы должны создать экземпляр маршрутизатора и запустить приложение. Это должно произойти на мероприятии готового документа.
// start app $(function() { app.router = new app.ApplicationRouter(); Backbone.history.start(); });
OData связанные настройки
К сожалению! Запускаем приложение и ничего не видим. Потому что наш запрос к службе OData не выполнен. Нам нужно сделать некоторые настройки, чтобы это работало.
Backbone использует функцию jQuery.ajax для отправки ajax-запроса на сервер. Добавьте специальный предварительный фильтр ajax.
// prepare request according to ODATA $.ajaxPrefilter(function(options, originalOptions, xhr) { // set required HTTP headers options.contentType = "application/json; odata=verbose"; options.headers = $.extend(options.headers || {}, { Accept: "application/json; odata=verbose" }); // add default error handler var originalError = originalOptions.error; options.error = function(xhr, textStatus, errorThrown) { alert("Error during processing the request!"); if(originalError) { originalError.apply(this, arguments); } }; // patch success handler to retrieve data from ODATA json response var originalSuccess = options.success; if(originalSuccess && originalOptions.type === "GET") { options.success = function(data) { originalSuccess(data["d"]); }; } });
Предварительный фильтр добавляет contentType и принимает заголовок к запросу, переносит обработчик ошибок и исправляет обработчик успеха, чтобы получить результат json из ответа службы ODF WCF (поле d ответа json).
Теперь мы можем запустить приложение и увидеть следующий результат
Список продуктов
Теперь давайте создадим второй вид со списком товаров из выбранной категории.
Модель продукта
app.Models.Product = Backbone.Model.extend({ url: function() { return "Service/Northwind.svc/Products(" + this.get("ProductID") + ")"; }, idAttribute: "ProductID", initialize: function(attributes) { this.set({ UnitPrice: parseFloat(attributes.UnitPrice) }); }, defaults: { ProductID: 0, ProductName: "", UnitPrice: 0.0 } });
Метод initialize является конструктором экземпляра модели. Входной параметр — это объект с атрибутами. В конструкторе мы анализируем значение цены за единицу товара, полученное с сервера, в виде строки.
Коллекция продуктов
app.Models.ProductList = Backbone.Collection.extend({ url: "Service/Northwind.svc/Products", model: app.Models.Product, loadByCategory: function(categoryId, callback) { this.fetch({ data: { "$filter" : "CategoryID eq " + categoryId }, success: callback }); } });
Список товаров выбирается по категориям. Метод loadByCategory добавляет новый параметр $ filter в запрос, чтобы мы получали продукты из определенной категории.
Список товаров
app.Views.ProductListView = Backbone.View.extend({ template: _.template($("#productListViewTemplate").html()), className: "product-list-view", render: function() { this.$el.html(this.template(this.model.toJSON())); return this; }, events: { "click #backToCategories": "backToCategories" }, backToCategories: function() { app.router.navigate("/", { trigger: true }); } });
Это представление содержит кнопку, которая возвращает нас на страницу со списком категорий. Чтобы добавить такую логику в представление, я использую объект events , который отображает события в методы обработки. У события есть имя (щелчок) и селектор, к которому нужно прикрепить обработчик ( #backToCategories — кнопка с идентификатором backToCategories). Наш обработчик событий заставляет маршрутизатор перейти к корневому URL-адресу (страница с категориями).
Шаблон списка товаров
<script id="productListViewTemplate" type="text/template"> <p><input id="backToCategories" type="button" value="< Back to Categories" class="btn" /></p> <div class="well"> <ul class="list"> {% _.each(this.model.models, function(product) { %} <li> <h4><a href="#product/{{ product.get("ProductID") }}">{{ product.get("ProductName") }}</a></h4> <p>Price: ${{ product.get("UnitPrice").toFixed(2) }}</p> </li> {% }); %} </ul> </div> </script>
Здесь мы перебираем список продуктов и отображаем их по форматированной цене за единицу.
Список продуктов Маршрутизация
routes: { "": "categoryList", "category/:id": "productList" }, ... productList: function(id) { var productList = new app.Models.ProductList(), productListView = new app.Views.ProductListView({ model: productList }); productList.loadByCategory(id, function() { app.renderView(productListView, "Products List"); }); },
Маршрут для списка товаров добавлен. В шаблоне URL есть параметр : id . Значение параметра будет передано в функцию обработки.
Если мы запустим приложение и откроем категорию, то увидим список продуктов.
информация о продукте
Последняя страница — это информация о продукте, где пользователь может изменить название и цену продукта.
Модификация модели продукта
app.Models.Product = Backbone.Model.extend({ url: function() { return "Service/Northwind.svc/Products(" + this.get("ProductID") + ")"; }, idAttribute: "ProductID", initialize: function(attributes) { this.set({ UnitPrice: parseFloat(attributes.UnitPrice) }); }, defaults: { ProductID: 0, ProductName: "", UnitPrice: 0.0 }, load: function(callback) { this.fetch({ success: callback }); } });
Добавлен новый метод загрузки для инкапсуляции выборки продукта.
Подробнее о продукте
app.Views.ProductDetailsView = Backbone.View.extend({ template: _.template($("#productDetailsViewTemplate").html()), className: "product-details-view", render: function() { this.$el.html(this.template(this.model.toJSON())); return this; }, events: { "click #backToProducts": "backToProducts", "click #saveProduct": "saveProduct", }, backToProducts: function() { window.history.back(); }, saveProduct: function() { var message = this.$el.find("#message").empty(); this.model.save({ ProductName: $('#ProductName').val(), UnitPrice: $('#UnitPrice').val(), }, { success: function() { message.append($("<div />").addClass("alert alert-success").text("Product info successfully saved!")); }, error: function() { message.append($("<div />").addClass("alert alert-error").text("Error occurred while saving product info!")); } }); } });
Наиболее интересным здесь является метод saveProduct . Он принимает данные от входов и требует сохранения метода модели. Также отображается сообщение о состоянии результата операции.
Подробнее о продукте Посмотреть шаблон
Шаблон представления сведений о продукте — из полей продукта.
<script id="productDetailsViewTemplate" type="text/template"> <p><input id="backToProducts" type="button" value="< Back to Products" class="btn" /></p> <div class="well"> <fieldset> <label>Product ID:</label> <input type="text" id="ProductID" disabled value="{{ ProductID }}"> <label>Product Name:</label> <input type="text" id="ProductName" value="{{ ProductName }}"> <label>Unit Price:</label> <input type="text" id="UnitPrice" value="{{ UnitPrice }}"> </fieldset> <div id="message"></div> <input id="saveProduct" type="button" value="Save" class="btn-primary btn" /> </div> </script>
Детали продукта Маршрутизация
Путь к деталям продукта аналогичен маршруту списка продуктов.
routes: { "": "categoryList", "category/:id": "productList", "product/:id": "productDetails" }, ... productDetails: function(id) { var product = new app.Models.Product({ ProductID: id }), productDetailsView = new app.Views.ProductDetailsView({ model: product }); product.load(function() { app.renderView(productDetailsView, "Product \"" + product.get("ProductName") + "\""); }); }
Теперь, если мы откроем продукт из списка, отредактируем название и нажмем «Сохранить», мы получим следующий результат
Демонстрационное приложение довольно простое, но оно демонстрирует, как мы можем построить архитектуру клиентского кода с помощью Backbone.js. Также мы увидели, как создать WCF OData Service и заставить его работать с Backbone.