Сегодняшняя часть веб-приложения становится очень сложной. Клиенты ожидают функциональных высокопроизводительных приложений с красивым пользовательским интерфейсом, которые работают в браузере, как обычные настольные приложения. Чтобы выполнить эти требования, мы приходим к одностраничным приложениям (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.