Разработчики под Microsoft .NET обычно создают прекрасные приложения, используя JavaScript на клиентской стороне и ASP.NET (C# или Visual Basic .NET) на серверной. Ну а если бы можно было использовать один общий язык для создания приложений на всех уровнях стека — от браузеров и уровня сервисов до обработки бизнес-логики на серверной стороне и даже запросов и программирования применительно к базам данных? Теперь это возможно — благодаря Node.js. Node.js существует уже несколько лет, но его внедрение значительно ускорилось за последние годы. Стеки Node.js, такие как стек MongoDB, Express, AngularJS, Node.js (MEAN), приносят в разработку приложений много преимуществ, включая тот факт, что между разработчиками клиентской части, промежуточного уровня и серверной части существует лишь очень малый разрыв (если он вообще есть). Во многих случаях один и тот же программист может разработать все уровни приложения, потому что все это делается на JavaScript. Более того, теперь можно создавать приложения Node.js напрямую из Visual Studio 2013 с установленным компонентом Node.js Tools for Visual Studio (NTVS) и даже полностью отлаживать их в этой среде.
Приступаем
В этой статье я намерен показать, что, используя стек MEAN, можно быстро и легко создавать CRUD-приложения (create, read, update and delete). Я исхожу из того, что у вас есть базовое концептуальное понимание AngularJS (angularjs.org), Node.js (nodejs.org), MongoDB (mongodb.org) и Express (expressjs.com). Если вы собираетесь следовать за мной, пожалуйста, убедитесь, что у вас установлено следующее ПО:
Первый шаг — открытие диалога New Project в Visual Studio и выбор шаблона Blank Microsoft Azure Node.js Web Application (рис. 1). Вы могли бы несколько ускорить создание проекта, выбрав шаблон Basic Microsoft Azure Express Application, но пустой шаблон обеспечивает более детализированный контроль над тем, что устанавливается в качестве промежуточного ПО для приложения Node.js.
Рис. 1. Создание проекта Blank Microsoft Azure Node.js Web Application
Что такое промежуточное ПО Node.js? Если предельно упростить, то это модули, которые вы можете подключать к конвейеру запросов Express HTTP своего приложения Node.js. Как правило, промежуточное ПО выполняется при каждом HTTP-запросе.
Затем установите Express через Node Package Manager (NPM). Если вы знакомы с NuGet-пакетами, то NPM-пакеты, по сути, то же самое, но для приложений Node.js.
Как видно на рис. 2, я добавил @3 в текстовое поле Other npm arguments, чтобы установить самую свежую версию Express 3. Хотя уже выпущен Express 4, вы должны придерживаться Express 3, поскольку другие модули, которые будут устанавливаться, пока не обновлены под некоторые разрушающие изменения в Express 4.
Рис. 2. Поиск и установка NPM-пакетов, таких как Express
Вам потребуется скачать и установить остальные необходимые NPM-пакеты: express, odata-server, stringify-object и body-parser, но указывать что-либо в поле Other npm arguments не нужно, поскольку я буду использовать самые новые версии этих NPM-пакетов.
Подготовка файла Server.js
Файл server.js (иногда называемый app.js) (рис. 3) является фактически отправной точкой для приложения Node.js. Именно здесь вы конфигурируете свое приложение и указываете любые необходимые модули промежуточного ПО.
1 var http = require('http');
2 var express = require( 'express' );
3 var odata = require( './server/data/odata' );
4 var stringify = require( 'stringify-object' );
5 var config = require("./server/config/config");
6 var bodyParser = require("body-parser");
7 var app = express( );
8 odata.config( app );
9 app.use(bodyParser.json());
10 app.use( express.static( __dirname + "/public" ) );
11 var port = process.env.port || 1337;
12 app.get("/", function(req, res) {
13 res.sendfile("/public/app/views/index.html", { root: __dirname });
14 });
15 http.createServer(app).listen(port);
16 console.log(stringify( process.env ));
Чтобы использовать необходимые NPM-пакеты или библиотеки, которые вы скачали, нужно указывать ключевое слово require("имя пакета"), благодаря чему эти библиотеки попадают в область видимости для данного Node.js-класса, как показывают строки 1–6 на рис. 3. Я кратко рассмотрю содержимое server.js.
- Строки 1–6 Все обязательные пакеты вводятся в область видимости server.js, чтобы их можно было инициализировать и включить в конвейер HTTP-запросов.
- Строка 7 Инициализация нового веб-приложения Express.
- Строка 8 Определение конфигурации OData для конечных точек REST (подробнее об этом чуть позже).
- Строка 10 Подключение express.static и передача пути к каталогу, чтобы сделать его общедоступным. Это позволяет всем получать доступ к любому контенту, помещенному в каталог NodejsWebApp/Public. Например, http://localhost:1337/image/myImage.gif приведет к рендерингу изображения в NodejsWebApp/Public/image/myimage.gif для браузера.
- Строка 12 Настраиваем страницу перехода (landing page) по умолчанию с помощью метода app.get. В первом параметре передается путь (корневой путь приложения). Здесь я просто выполняю рендеринг статического HTML-файла, предоставляя путь к нему.
- Строка 15 Указываю приложению слушать и обслуживать HTTP-запросы по заданному порту; для целей разработки я использую порт 1337, поэтому мое приложение будет слушать запросы по http://localhost:1337.
- Строка 16 Вывод переменных окружения в консольное окно Node.js, чтобы в какой-то мере визуализировать среду Node.js.
Конфигурирование OData
Подготовив server.js, я сосредоточусь на строке 8, где я настраиваю конечные точки OData REST. Сначала надо создать два модуля: NodejsWebApp/server/data/northwind.js (рис. 4) и NodejsWebApp/server/data/odata.js (рис. 5).
Рис. 4. NodejsWebApp/server/data/northwind.js
$data.Entity.extend( 'Northwind.Category', {
CategoryID: { key: true, type: 'id', nullable: false, computed: true },
CategoryName: { type: 'string', nullable: false, required: true, maxLength: 15 },
Description: { type: 'string', maxLength: Number.POSITIVE_INFINITY },
Picture: { type: 'blob', maxLength: Number.POSITIVE_INFINITY },
Products: { type: 'Array', elementType: 'Northwind.Product', inverseProperty: 'Category' }
} );
$data.Entity.extend( 'Northwind.Product', {
ProductID: { key: true, type: 'id', nullable: false, computed: true },
ProductName: { type: 'string', nullable: false, required: true, maxLength: 40 },
EnglishName: { type: 'string', maxLength: 40 },
QuantityPerUnit: { type: 'string', maxLength: 20 },
UnitPrice: { type: 'decimal' },
UnitsInStock: { type: 'int' },
UnitsOnOrder: { type: 'int' },
ReorderLevel: { type: 'int' },
Discontinued: { type: 'bool', nullable: false, required: true },
Category: { type: 'Northwind.Category', inverseProperty: 'Products' },
Order_Details: { type: 'Array', elementType: 'Northwind.Order_Detail',
inverseProperty: 'Product' },
Supplier: { type: 'Northwind.Supplier', inverseProperty: 'Products' }
} );
$data.Class.define( "NorthwindContext", $data.EntityContext, null, {
Categories: { type: $data.EntitySet, elementType: Northwind.Category },
Products: { type: $data.EntitySet, elementType: Northwind.Product },
// Регистрация остальных сущностей удалена для краткости;
// пожалуйста, см. полный исходный код
} );
// Определения остальных сущностей удалены для краткости;
// пожалуйста, см. полный исходный код
NorthwindContext.generateTestData = function( context, callBack ) {
var category1 = new Northwind.Category( { CategoryName: 'Beverages',
Description: 'Soft drinks, coffees, teas, beer, and ale' } );
// Экземпляры других категорий удалены для краткости;
// пожалуйста, см. полный исходный код
context.Categories.add( category1 );
// Вставки других сущностей удалены для краткости;
// пожалуйста, см. полный исходный код
context.Products.add( new Northwind.Product(
{ ProductName: 'Ipoh Coffee', EnglishName: 'Malaysian Coffee',
UnitPrice: 46, UnitsInStock: 670, Discontinued: false, Category: category1 } ) );
// Вставки других сущностей удалены для краткости;
// пожалуйста, см. полный исходный код
context.saveChanges( function ( count ) {
if ( callBack ) {
callBack( count );
}
} );
};
module.exports = exports = NorthwindContext;
Рис. 5. Модуль NodejsWebApp/server/data/odata.js
( function (odata) {
var stringify = require( 'stringify-object' );
var config = require( "../config/config" );
console.log( stringify( config ) );
odata.config = function ( app ) {
var express = require( 'express' );
require( 'odata-server' );
var northwindContextType = require( './northwind.js' );
var northwindContext = new NorthwindContext( {
address: config.mongoDb.address,
port: config.mongoDb.port,
username: config.mongoDb.username,
password: config.mongoDb.password,
name: config.mongoDb.name,
databaseName: config.mongoDb.databaseName,
dbCreation: $data.storageProviders.DbCreationType.DropAllExistingTables
} );
console.log( "northwindContext :" );
stringify( northwindContext );
northwindContext.onReady( function ( db ) {
northwindContextType.generateTestData( db, function ( count ) {
console.log( 'Test data upload successful. ', count, 'items inserted.' );
console.log( 'Starting Northwind OData server.' );
app.use( express.basicAuth( function ( username, password ) {
if ( username == 'admin' ) {
return password == 'admin';
} else return true;
} ) );
Заметьте, что MongoDB — это база данных NoSQL, т. е. не реляционная база данный документов. При миграции традиционной базы данных Northwind в MongoDB, чтобы задействовать преимущества модели NoSQL, ее можно структурировать множеством способов. Для целей этой статьи я оставлю схему Northwind по большей части неизменной. (Я удалил с рис. 4 определения других моделей сущностей, регистрацию и вставки для краткости.)
На рис. 4 модели и сущности просто определяются, и их потом можно использовать повторно на клиентской стороне при выполнении CRUD-операций, таких как создание нового Products. Кроме того, метод NorthwindContext.generateTestData будет инициализировать базу данных при каждом перезапуске приложения, что удобно, когда приложение развертывается на демонстрационном сайте. Это упрощает обновление данных простым повторным использованием приложения. Более элегантный подход — обертывание этого кода в Azure WebJob и планирование обновления с заданной частотой, но пока мы оставим все, как есть. Последняя строка в этом модуле (module.exports = exports = NorthwindContext) обертывает все так, что потом вы сможете «потребовать» этот модуль и использовать оператор new для создания нового экземпляра объектного типа Northwind; это делается в модуле NodejsWebApp/server/data/odata.js (рис. 5).
Вы можете запрашивать MongoDB из командной строки или с помощью одной из многих GUI-утилит MongoDB (например, RoboMongo), чтобы убедиться, что начальные данные действительно были вставлены. Поскольку основное внимание в этой статье фокусируется на OData, используйте LINQPad, так как она включает встроенный провайдер для LINQ-запроса к OData версии 3.
Чтобы протестировать конечные точки, скачайте и установите LINQPad (linqpad.net), а затем запустите свое приложение (F5 в Visual Studio 2013). Теперь запустите LINQPad и установите новое соединение с конечной точкой OData. Для этого щелкните Add connection и выберите OData в качестве провайдера данных LINQPad. Потом сконфигурируйте LINQ-соединение OData с помощью следующего URI: http://localhost:1337/northwind.svc; имя пользователя (username) и пароль (password) — Admin. LINQPad будет визуализировать иерархию на основе конечной точки OData CSDL, как можно увидеть в верхнем левом углу рис. 6.
Рис. 6. LINQ-запрос и его результаты с использованием обнаруженной модели данных
В Products должны быть данные, основанные на начальных данных, которые используются на серверной стороне (NodejsWebApp/server/northwind.js), поэтому вы захотите сделать быстрый LINQ-запрос к Products, используя LINQPad:
На рис. 6 также показаны запрос и его результаты.
Как видите, OData-сервер настроен корректно, и вы можете выдавать LINQ-запросы по HTTP, а также получать списки товаров в ответ на запросы. Если перейти на вкладку Request Log, можно увидеть HTTP GET OData URL, который генерируется LINQPad из LINQ-выражения: http://localhost:1337/northwind.svc/Products()?$top=100.
Убедившись, что ваш OData-сервер действительно выполняется в веб-приложении Node.js Express, вы захотите воспользоваться этим и приступить к выработке некоторых распространенных случаев применения, где могут быть задействованы преимущества OData. Поместите все на клиентской стороне в папку public, а весь код, выполняемый на серверной стороне, — в папку Server. Заранее создайте все файлы, необходимые вашему приложению, как заглушки (stubs) или заполнители (placeholders), а затем вернитесь и заполните заготовки (blanks). На рис. 7 показана структура проекта NodejsWebApp.
Рис. 7. Проект NodejsWebApp
Файл app.js (NodejsWebApp/public/app/app.js), приведенный на рис. 8, является, по сути, начальной точкой приложения AngularJS (на клиентской стороне). Я не буду вдаваться во все детали; «сухой остаток» в том, что вам нужно зарегистрировать свои маршруты для одностраничного приложения (single-page application, SPA) на клиентской стороне в $routeProvider. Для каждого из маршрутов (определенных с помощью метода .when) предоставьте путь к представлению (HTML) для рендеринга, задав свойство templateUrl, и укажите контроллер представления, настроив свойство controller для данного маршрута. Контроллер AngularJS — то место, куда помещается весь код, выполняющий все, что требуется представлению; если в двух словах, то весь JavaScript-код для представления. Метод .otherwise используется для настройки маршрута по умолчанию (начальное представление) для любых входящих запросов, которые не подходят ко всем остальным маршрутам.
'use strict';
var myApp = angular.module('myApp',
[
'ngRoute',
'ngAnimate',
'kendo.directives',
'jaydata'
])
.factory("northwindFactory",
[
'$data',
'$q',
function($data, $q) {
// Здесь вы обертываете jquery-обещание (promise)
// в angular-обещание. Простой возврат jquery-обещания
// вызывает всяческие проблемы.
var defer = $q.defer();
$data.initService("/northwind.svc").then(function(ctx) {
defer.resolve(ctx);
});
return defer.promise;
}
])
.config(function($routeProvider) {
$routeProvider
.when('/home',
{
templateUrl: 'app/views/home.html'
})
.when('/product',
{
templateUrl: 'app/views/product.html',
controller: 'productController',
resolve: {
northwind: 'northwindFactory'
}
})
.when('/edit/:id',
{
templateUrl: 'app/views/edit.html',
controller: 'editController',
resolve: {
northwind: 'northwindFactory'
}
})
.when('/chart',
{
templateUrl: 'app/views/chart.html',
controller: 'chartController',
resolve: {
northwind: 'northwindFactory'
}
})
.otherwise(
{
redirectTo: '/home'
});
});
Ниже дано краткое напоминание того, как обязанности шаблона Model-View-ViewModel (MVVM) представляются в приложении:
- View = *.html;
- ViewModel = *controller.js;
- Model = сущности, возвращаемые конечными точками REST, обычно являются моделями и/или сущностями предметной области.
На рис. 9 показано, какие файлы в приложении отвечают за ту или иную обязанность шаблона MVVM.
View | View |
ViewModel | ViewModel |
Model | Model |
commands | команды |
update | обновление |
binding | связывание |
read | чтение |
Views | Представления |
Controllers | Контроллеры |
Entities | Сущности |
Определение клиентского DataContext в JayData как сервиса AngularJS
Поскольку большинство контроллеров будет использовать контекст Northwind, вам понадобится создать сервис/фабрику с именем northwindFactory. А так как контекст Northwind инициализируется асинхронно, вам потребуется настроить JavaScript-обещание, чтобы быть уверенным в том, что инициализация контекста Northwind завершилась и что этот контекст готов к использованию к тому моменту, когда загружен любой из контроллеров. Поэтому, если не вдаваться в детали, контекст Northwind закончит загружаться до того, как будет загружен любой контроллер с зависимостью от northwindFactory. Заметьте, что у всех сконфигурированных маршрутов есть свойство resolve; с его помощью вы определяете, какие обещания нужно разрешать до загрузки контроллера. В данном случае свойству northwind присваивается northwindFactory. Имя свойства northwind также будет именем экземпляра, который будет встроен в контроллер. Вы увидите функцию конструктора для productController.js чуть позже; в ней northwindFactory встраивается как northwind, а это имя свойства, заданное для northwindFactory в свойстве resolve в маршрутах.
Поскольку большинство контроллеров будет использовать контекст Northwind, вам понадобится создать сервис/фабрику с именем northwindFactory.
Index.html, показанный на рис. 10, является в основном страницей разметки, и AngularJS будет знать, какие представления подставлять в div с атрибутом ng-view. Заметьте, что вы должны указать приложение AngularJS, настроив любой HTML-элемент, который является родительским элементом для div с атрибутом ng-view. В данном случае ng-app присваивается myApp — именно так называется приложение в app.js.
<!DOCTYPE html>
<html >
<head>
<meta charset="utf-8" />
<title>NodejsWebApp</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
rel="stylesheet">
<link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.common.min.css"
rel="stylesheet" />
<link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.bootstrap.min.css"
rel="stylesheet" />
<link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.dataviz.min.css"
rel="stylesheet" />
<link href="//cdn.kendostatic.com/2014.2.716/styles/
kendo.dataviz.bootstrap.min.css" rel="stylesheet" />
<link href="../../css/site.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">NodejsWebApp</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a href="#/home">Home</a>
</li>
<li>
<a href="#/about">About</a>
</li>
<li>
<a href="#/contact">Contact</a>
</li>
<li>
<a href="#/product">Product</a>
</li>
<li>
<a href="#/chart">Chart</a>
</li>
</ul>
</div>
</div>
</div>
<!-- Связываем приложение с нашим приложением AngularJS: "myApp" -->
<div class="container body-content" ng-app="myApp">
<br />
<br/>
<!-- AngularJS будет обменивать наши представления в этом div -->
<div ng-view></div>
<hr />
<footer>
<p>© 2014 - My Node.js Application</p>
</footer>
</div>
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src=
"//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="//code.angularjs.org/1.3.0-beta.16/angular.min.js"></script>
<script src="//code.angularjs.org/1.3.0-beta.16/angular-route.min.js"></script>
<script src="//code.angularjs.org/1.3.0-beta.16/angular-animate.min.js"></script>
<script src="//cdn.kendostatic.com/2014.2.716/js/kendo.all.min.js"></script>
<script src="//include.jaydata.org/datajs-1.0.3-patched.js"></script>
<script src="//include.jaydata.org/jaydata.js"></script>
<script src="//include.jaydata.org/jaydatamodules/angular.js"></script>
<script src="/lib/jaydata-kendo.js"></script>
<!--<script src="//include.jaydata.org/jaydatamodules/kendo.js"></script>-->
<script src="/app/app.js"></script>
<script src="/app/controllers/productController.js"></script>
<script src="/app/controllers/chartController.js"></script>
<script src="/app/controllers/editController.js"></script>
</body>
</html>
Заметьте, что я использую сеть доставки контента (content delivery network, CDN) для всех своих JavaScript-библиотек, включаемых на клиентской стороне. Вы можете скачать клиентские библиотеки локально, используя Bower в командной строке (как вы обычно делаете это для .NET-проектов с NuGet, используя консоль Package Manager). В Microsoft .NET Framework вы применяете NuGet как для клиентских пакетов, так и для серверных. Однако в царстве Node.js для скачивания клиентских библиотек/пакетов применяется Bower, а для скачивания и установки серверных библиотек/пакетов — NPM.
Для разметки UI я использую исходную тему начальной загрузки (vanilla bootstrap theme), которую генерирует шаблон проекта ASP.NET MVC 5 в Visual Studio.
Представление product
Для представления product (NodejsWebApp/public/app/views/products.html) требуется всего несколько строк HTML. Первый блок — это директива Kendo для AngularJS визуализировать сетку:
<!-- Директива Kendo UI для AngularJS визуализировать сетку -->
<div kendo-grid="grid" k-options="options"></div>
<!-- Шаблон AngularJS для нашей кнопки View Detail на панели инструментов Grid -->
<script type="text/x-kendo-template" id="viewDetail">
<a
class="k-button "
ng-click="viewDetail(this)">View Detail</a>
</script>
Второй блок — это просто шаблон AngularJS для пользовательской кнопки View Detail, которую вы добавляете на панель инструментов сетки.
На рис. 11 показан контроллер product, NodejsWebApp/app/controllers/productController.js.
Рис. 11. Контроллер представления product
myApp.controller("productController",
function($scope, northwind, $location) {
var dataSource =
northwind
.Products
.asKendoDataSource({ pageSize: 10 });
$scope.options = {
dataSource: dataSource,
filterable: true,
sortable: true,
pageable: true,
selectable: true,
columns: [
{ field: "ProductID" },
{ field: 'ProductName' },
{ field: "EnglishName" },
{ field: "QuantityPerUnit" },
{ field: "UnitPrice" },
{ field: 'UnitsInStock' },
{ command: ["edit", "destroy"] }
],
toolbar: [
"create",
"save",
"cancel",
{
text: "View Detail",
name: "detail",
template: $("#viewDetail").html()
}
],
editable: "inline"
};
$scope.viewDetail = function(e) {
var selectedRow = $scope.grid.select();
if (selectedRow.length == 0)
alert("Please select a row");
var dataItem = $scope.grid.dataItem(selectedRow);;
$location.url("/edit/" + dataItem.ProductID);
};
});
Для заполнения сетки Products нужно создать экземпляр Kendo UI DataSource ($scope.options.dataSource). JayData предоставляет вспомогательный метод для инициализации Kendo UI DataSource, связанного со своими конечными точками OData REST. JayData-метод asKendoDataSourcehelper знает, как создать DataSource на основе метаданных, публикуемых OData-сервером (http://localhost:1337/northwindsvc), которые потом используются для конфигурирования экземпляра $data в northwindFactory в app.js. Вы еще увидите Kendo DataSource, когда я буду демонстрировать зрительные образы (visual impressions) с помощью инфраструктуры создания диаграмм Kendo DataViz.
Наряду с готовыми кнопками (create, save и cancel) на панели инструментов сетки вы добавляете свою кнопку для перехода в другое представление, которое будет визуализировать все подробности по выбранному в строке продукту ($scope.viewDetail). Когда происходит событие щелчка кнопки View Detail, мы получаем DataItem выбранного продукта, а затем, используя AngularJS-сервис $location, переходим в представление edit (MyNodejsWebApp/scripts/app/views/edit.html) для этого продукта.
На рис. 12 показан файл edit.html (NodejsWebApp/public/app/views/edit.html).
<div class="demo-section">
<div class="k-block" style="padding: 20px">
<div class="k-block k-info-colored">
<strong>Note: </strong>Please fill out all of the fields in this form.
</div>
<div>
<dl>
<dt>
<label for="productName">Name:</label>
</dt>
<dd>
<input id="productName" type="text"
ng-model="product.ProductName" class="k-textbox" />
</dd>
<dt>
<label for="englishName">English Name:</label>
</dt>
<dd>
<input id="englishName" type="text"
ng-model="product.Englishname" class="k-textbox" />
</dd>
<dt>
<label for="quantityPerUnit">Quantity Per Unit:</label>
</dt>
<dd>
<input id="quantityPerUnit" type="text"
ng-model="product.QuantityPerUnit" class="k-textbox" />
</dd>
<dt>
<label for="unitPrice">Unit Price:</label>
</dt>
<dd>
<input id="unitPrice" type="text"
ng-model="product.UnitPrice" class="k-textbox" />
</dd>
<dt>
<label for="unitsInStock">Units in Stock:</label>
</dt>
<dd>
<input id="unitsInStock" type="text"
ng-model="product.UnitsInStock" class="k-textbox" />
</dd>
<dt>
<label for="reorderLevel">Reorder Level</label>
</dt>
<dd>
<input id="reorderLevel" type="text"
ng-model="product.ReorderLevel" class="k-textbox" />
</dd>
<dt>
<label for="discontinued">Discontinued:</label>
</dt>
<dd>
<input id="discontinued" type="text"
ng-model="product.Discontinued" class="k-textbox" />
</dd>
<dt>
<label for="category">Category:</label>
</dt>
<dd>
<select
kendo-drop-down-list="dropDown"
k-data-text-field="'CategoryName'"
k-data-value-field="'CategoryID'"
k-data-source="categoryDataSource"
style="width: 200px"></select>
</dd>
</dl>
<button kendo-button ng-click="save()"
data-sprite-css-class="k-icon k-i-tick">Save</button>
<button kendo-button ng-click="cancel()">Cancel</button>
<style scoped>
dd {
margin: 0px 0px 20px 0px;
width: 100%;
}
label {
font-size: small;
font-weight: normal;
}
.k-textbox { width: 100%; }
.k-info-colored {
margin: 10px;
padding: 10px;
}
</style>
</div>
</div>
</div>
Заметьте: теги input дополнены атрибутом ng-model; это способ, применяемый в AngularJS для декларативного указания на то, что значение для этого input будет храниться в свойстве, заданном значением ng-model, в $scope контроллера. Например, в первом поле ввода в этом представлении, чей HTML-элемент id имеет значение «productName (id="productName")», ng-model присваивается значение product.ProductName. Это означает, что независимо от того, что именно вводит пользователь в это поле (textbox), для $scope.productName будет установлено соответствующее значение. Более того, что бы ни было программно присвоено $scope.product.productName, в editController будет автоматически отражаться значение поля ввода для productName.
Например, при первой загрузке представления загружается объект product по ID, переданному в URL, затем в $scope.product помещается этот product (рис. 13). После этого все, что есть в представлении с атрибутом ng-model, установленном в $scope.property.*, будет отражать значения всех свойств в $scope.product. В прошлом для любых манипуляций над DOM разработчики обычно задавали значения в полях ввода с помощью jQuery или непосредственно JavaScript. При создании приложения по шаблону MVVM (независимо от инфраструктуры) лучше всего манипулировать с DOM только через изменения в ViewModel, причем не напрямую (скажем, через JavaScript или jQuery). Я отнюдь не хочу сказать, что с JavaScript или jQuery что-то неладно, но, если вы решили применять шаблон для решения конкретной задачи (в моем случае — MVVM для разделения обязанностей между View, ViewModel и Model), вы должны быть последовательны в рамках всего приложения.
Рис. 13. Файл editController.js
myApp.controller("editController",
function($scope, northwind, $routeParams, $location) {
var productId = $routeParams.id;
$scope.categoryDataSource = northwind.Categories.asKendoDataSource();
northwind
.Products
.include("Category")
.single(
function(product) {
return product.ProductID == productId;
},
{ productId: productId },
function(product) {
$scope.product = product;
northwind.Products.attach($scope.product);
$scope.dropDown.value($scope.product.Category.CategoryID);
$scope.$apply();
});
$scope.save = function() {
var selectedCategory = $scope
.categoryDataSource
.get($scope.product.Category.CategoryID);
console.log("selecctedCategory: ", selectedCategory.innerInstance());
$scope.product.Category = selectedCategory.innerInstance();
// Разворачиваем Kendo dataItem в чистый JayData-объект
northwind.saveChanges();
};
$scope.cancel = function() {
$location.url("/product");
};
});
Вы могли бы реализовать серверную POST-операцию в Node.js, что, как правило, делается с помощью ASP.NET Web API. Однако здесь моя цель — продемонстрировать, как сделать это с помощью Node.js и OData:
app.post('/api/updateProduct', function(req, res) {
var product = req.body;
// Здесь обрабатываем обновление,
// обычно это делается с помощью ASP.NET Web API
});
Для представления chart (NodejsWebApp/public/app/views/chart.html) вам нужна всего одна строка разметки:
<kendo-chart k-options="options"></kendo-chart>
Все, что здесь происходит, — это объявление директивы Kendo UI Bar Chart, задание options для связывания со свойством в контроллере с именем options. На рис. 14 показано представление product chart, а на рис. 15 приведен исходный код контроллера product chart.
Рис. 14. Представление product chart
Рис. 15. Контроллер product chart
myApp.controller("chartController",
function($scope, northwind) {
var dataSource = northwind.Products.asKendoDataSource();
$scope.options = {
theme: "metro",
dataSource: dataSource,
chartArea: {
width: 1000,
height: 550
},
title: {
text: "Northwind Products in Stock"
},
legend: {
position: "top"
},
series: [
{
labels: {
font: "bold italic 12px Arial,Helvetica,sans-serif;",
template: '#= value #'
},
field: "UnitsInStock",
name: "Units In Stock"
}
],
valueAxis: {
labels: {
format: "N0"
},
majorUnit: 100,
plotBands: [
{
from: 0,
to: 50,
color: "#c00",
opacity: 0.8
}, {
from: 50,
to: 200,
color: "#c00",
opacity: 0.3
}
],
max: 1000
},
categoryAxis: {
field: "ProductName",
labels: {
rotation: -90
},
majorGridLines: {
visible: false
}
},
tooltip: {
visible: true
}
};
});
Как и в случае productController.js, здесь вы также встраиваете northwindFactory как northwind в функцию конструирования контроллера, вновь создавая Kendo dataSource через вспомогательный JayData-метод asKendoDataSource. Ниже подробнее описывается, что происходит в контроллере chart.
$scope.options.series
- type — конфигурирование типа диаграммы;
- field — поле из модели/сущности, которое будет использоваться для значения series (по X-оси).
$scope.options.valueAxis
- majorUnit — интервал между основными делениями (major divisions). Если valueAxis.type равен log, значение majorUnit будет использоваться как основание логарифма;
- plotBands — полосы графика, используемые для отображения количества товаров. Если количество падает ниже указанного уровня, пользователь должен запустить процесс пополнения товара;
- max — максимальное значение для Y-оси.
$scope.options.categoryAxis
- field — надписи для поля по X-оси;
- labels.rotation — углы поворота надписей. Здесь вы настраиваете надписи так, чтобы они были перпендикулярны X-оси, и для этого задаете значение –90 (градусов), т. е. поворачиваете надписи против часовой стрелки на 90 градусов;
- majorGridLines.visible — включение или выключение основной сетки. Вы можете выключить ее отображение в косметических целях, чтобы диаграмма выглядела понятнее и нагляднее;
- tooltip.visible — включает отображение всплывающих подсказок, когда пользователь задерживает курсор мыши над вертикальной полоской.
Подробнее о Kendo UI Chart API см. по ссылке bit.ly/1owgWrS.
Развертывание на веб-сайте Azure
Поскольку исходный код удобно размещен в репозитарии CodePlex Git, настройка Azure Web Sites на непрерывное развертывание (непрерывную доставку) весьма проста.
- Перейдите к информационной панели вашего веб-сайта Azure и выберите Set up deployment from source control.
- Выберите свой репозитарий; для этого примера — CodePlex.
- Щелкните Next.
- Укажите свой проект CodePlex.
- Выберите ветвь (branch).
- Щелкните Check.
- При каждой синхронизации с вашим репозитарием Git будут выполняться сборка и развертывание.
Вот и все. Несколько щелчков, и ваше приложение развертывается с непрерывной интеграцией и доставкой. Подробнее о развертывании с помощью Git см. по ссылке bit.ly/1ycBo9S.
Как .NET-разработчик я по-настоящему наслаждался тем, насколько легко и быстро на основе ASP.NET MVC, ASP.NET Web API, OData, Entity Framework, AngularJS и Kendo UI создаются приложения, интенсивно выполняющие CRUD-операции. Теперь благодаря разработке в стеке MEAN я могу по-прежнему использовать большую часть знаний предметной области с помощью библиотек JayData. Единственная разница между двумя стеками — уровень на серверной стороне. Если вы вели разработки с применением ASP.NET MVC и ASP.NET Web API, то Node.js не должна принести вам особых сложностей, так как у вас уже есть некий базовый опыт работы с JavaScript. Полный исходный код примера в этой статье вы найдете в msdnmeanstack.codeplex.com, а интерактивную демонстрацию — в meanjaydatakendo.azurewebsites.net.