Node.js
Microsoft Azure で OData を使用して MEAN スタック上に Web アプリをビルドする
Microsoft .NET 開発者が優れたアプリをビルドする場合、クライアント側では JavaScript を、サーバー側では ASP.NET (C# または Visual Basic .NET) を使用するのが一般的です。しかし、ブラウザーやサービスからサーバー側のビジネス処理、さらにはデータベースでのクエリとプログラミングまで、スタック上のすべてのレイヤーのアプリを 1 つの共通言語を使用してビルドできるとしたらどうでしょう。これを実現するのが Node.js です。Node.js は数年前からありますが、大きく取り上げられるようになったのは最近です。MEAN (MongoDB、Express、AngularJS、Node.js) スタックなどの Node.js スタックは、フロントエンド、中間レイヤー、バックエンドの開発者どうしの意思疎通がほとんど必要ないことなど、アプリのビルドに多くのメリットをもたらします。多くの場合、同じプログラマーがアプリのすべてのレイヤーを開発できます。これは、すべての開発を JavaScript で行うためです。さらに、完全なデバッグ機能を含む Node.js Tools for Visual Studio (NTVS) を使用すれば、Visual Studio 2013 内で直接 Node.js アプリをビルドできるようになります。
作業の開始
今回は、MEAN スタックを使用すると、作成、読み取り、更新、削除 (CRUD) を頻繁に行うアプリを高速かつ簡単にビルドできることを示します。今回は、AngularJS (angularjs.org、英語)、Node.js (nodejs.org、英語)、MongoDB (mongodb.org、英語)、および Express (expressjs.com、英語) の基本概念を理解していることが前提です。ここでの説明に沿って計画を進める場合は、以下をインストールしておく必要があります。
- Visual Studio 2013 Update 4 (https://www.visualstudio.com/ja-jp/downloads/download-visual-studio-vs#d-visual-studio-2013-update)
- Node.js Tools for Visual Studio (nodejstools.codeplex.com、英語)
- MongoDB (ダウンロードについては bit.ly/1rw0BZm (英語)、インストールについては bit.ly/1uJN8eO (英語) を参照)
まず、Visual Studio で [新しいプロジェクト] ダイアログを開き、[Blank Microsoft Azure Node.js Web Application] (空の Microsoft Azure Node.js Web アプリケーション) テンプレートを選択します (図 1 参照)。[基本的な Microsoft Azure Express アプリケーション] テンプレートを選択するといくつかの項目を省略できますが、空のテンプレートは Node.js アプリのミドルウェアとしてインストールする項目を細かく制御できます。
図 1 空の Microsoft Azure Node.js Web アプリの作成
Node.js ミドルウェアとは何でしょう。非常に簡単に言えば、Node.js アプリの Express HTTP 要求パイプラインにプラグ インできるモジュールにすぎません。一般に、ミドルウェアは HTTP 要求ごとに実行されます。
次に、ノード パッケージ マネージャー (NPM) を使用して Express をインストールします。NuGet パッケージをご存知であれば、NPM パッケージは基本的には NuGet パッケージと同じものですが、Node.js アプリを対象にする点が異なります。
図 2 に示すように、Express 3 の最新バージョンをインストールするために、[Other npm arguments] (その他の npm 引数) テキスト フィールドに「@3」を追加しました。Express 4 がリリースされていますが、インストールする他のモジュールには、Express 4 に加えられた変更に対応していないものがあるため、Express 3 を使用する必要があります。
図 2 Express などの NPM パッケージの検索とインストール
その他に必要な NPM パッケージとして、express、odata-server、stringify-object、および body-parser をダウンロードしてインストールする必要があります。ただし、これらの各 npm パッケージの最新バージョンを使用する予定なので、[その他の npm 引数] を指定する必要はありません。
Server.js ファイルのセットアップ
server.js (app.js とも呼ばれる) ファイル (図 3 参照) が、基本的には Node.js アプリの開始点です。このファイルでアプリを構成し、必要なミドルウェア モジュールをすべて挿入します。
図 3 Server.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 パッケージまたはライブラリをダウンロードして使用するには、図 3 の 1 ~ 6 行目に示すように、require("package name") キーワードを使用して特定の Node.js クラスのスコープにそれらのライブラリを取り込む必要があります。ここで、server.js の内容を簡単に確認しておきます。
- 1 ~ 6 行目: 必要なすべてのパッケージを server.js スコープに取り込み、初期化して HTTP 要求パイプラインにプラグ インできるようにする。
- 7 行目: 新しい Express Web アプリを初期化する。
- 8 行目: REST エンドポイント向けに OData 構成を定義する (後ほど詳しく説明)。
- 10 行目: express.static にプラグ インしてディレクトリ パスを渡し、渡されたディレクトリ パスをパブリックに公開する (NodejsWebApp/Public ディレクトリに配置されているコンテンツにだれでもアクセスできるようにします。たとえば、http://localhost:1337/image/myImage.gif は、NodejsWebApp/Public/image/myimage.gif の画像をブラウザーにレンダリングします)。
- 12 行目: app.get メソッドを使用して、既定のランディング ページを設定する (最初のパラメーターはアプリケーションのルート パスを受け取ります。ここでは単純に、静的 HTML ファイルへのパスを指定することによって、そのファイルをレンダリングしています)。
- 15 行目: HTTP 要求をリッスンして使用するポートをアプリに指示する (今回の開発目的ではポート 1337 を使用しているため、アプリは http://localhost:1337 で要求をリッスンします)。
- 16 行目: Node.js コンソール ウィンドウに環境変数を出力し、Node.js 環境への一定の可視性を提供する。
OData の構成
server.js のセットアップでは、OData REST エンドポイントを構成する 8 行目に注目します。まず、2 つのモジュール 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 },
// Other entity registrations removed for brevity, please see actual source code.
} );
// Other entity definitions removed for brevity, please see actual source code.
NorthwindContext.generateTestData = function( context, callBack ) {
var category1 = new Northwind.Category( { CategoryName: 'Beverages',
Description: 'Soft drinks, coffees, teas, beer, and ale' } );
// Other category instances removed for brevity, please see actual source code.
context.Categories.add( category1 );
// Other category inserts removed for brevity, please see actual source code.
context.Products.add( new Northwind.Product(
{ ProductName: 'Ipoh Coffee', EnglishName: 'Malaysian Coffee',
UnitPrice: 46, UnitsInStock: 670, Discontinued: false, Category: category1 } ) );
// Other product inserts removed for brevity, please see actual source code.
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 データベース、つまり、リレーショナルではないドキュメント データベースです。NoSQL モデルを利用するために、以前から使われている Northwind データベースを MongoDB に移行する場合、多種多様な構造が考えられます。ここでは、Northwind スキーマにほぼ手を加えずに使用します (簡潔にするために、図 4 には他のエンティティ モデルの定義、登録、挿入を含めていません)。
図 4 では、モジュールとエンティティを定義しているだけです。このモジュールとエンティティを後からクライアント側で再利用して、新しい商品を作成するなどの、CRUD 操作を実行します。NorthwindContext.generateTestData メソッドは、アプリが再起動されるたびにデータベースをシードします。これは、ライブ デモ サイトにアプリを配置するときに便利です。このメソッドにより、必要に応じてアプリをリサイクルするだけで、データを簡単に最新状態に更新できます。このコードを Azure WebJob にラップして、設定した頻度で更新するようにスケジュールを設定する洗練されたアプローチもありますが、今のところそのままにしてあります。このモジュールの最終行の module.exports = exports = NorthwindContext はすべてをラップして、後からこのモジュールが "必要になる" ときに、"new" 演算子を使用して Northwind オブジェクト型の新しいインスタンスを作成できるようにします。これは NodejsWebApp/server/data/odata.js モジュールで行います (図 5 参照)。
コマンド ラインを使用するか多数の MongoDB GUI ツールのいずれか (RoboMongo など) を使用して MongoDB に対してクエリを実行すると、シード データが実際に挿入されたことを確認できます。今回は OData に注目しているため LINQPad を使用します。LINQPad には、OData のバージョン 3 に対して LINQ クエリを実行する組み込みのプロバイダーが含まれています。
エンドポイントをテストするには、LINQPad (linqpad.net) をダウンロードしてインストールしてから、アプリを実行します (Visual Studio 2013 では F5 キーを押します)。その後、LINQPad を起動し、OData エンドポイントへの新しい接続をセットアップします。そのためには、[Add connection](接続の追加) をクリックし、LINQPad データ プロバイダーとして OData を選択します。次に、URI "http://localhost:1337/northwind.svc"、ユーザー名 "Admin"、パスワード "Admin" で OData LINQ 接続を構成します。LINQPad では、図 6 の左上に示されているように、OData CSDL エンドポイントに基づいて階層が表示されます。
図 6 検出されたデータ モデルを使用した LINQ クエリとその結果
Products のデータは、サーバー側 (NodejsWebApp/server/northwind.js) で使用されるシード データに基づいているため、LINQPad を使用して Products に対してすばやく LINQ クエリを実行します。
Products.Take(100)
図 6 は、クエリとその結果を示しています。
ご覧のとおり、OData サーバーが正しくセットアップされ、HTTP 経由で LINQ クエリを発行して、商品の一覧を取得できます。[Request Log](要求ログ) タブをクリックすると、HTTP GET OData URL LINQPad が LINQ ステートメント http://localhost:1337/northwind.svc/Products()?$top=100 から生成されるのを実際に確認できます。
OData サーバーが実際に Node.js Express Web アプリで実行されていることを確認したら、これを使用して、OData の優れた点を生かす一般的なユース ケースのビルドを始めます。クライアント側のすべてを "public" フォルダーに配置し、サーバー側で実行するすべてのコードを Server というフォルダーに配置します。アプリに必要なすべてのファイルを前もってスタブまたはプレースホルダーとして作成してから、あちこち確認し、空白を埋めます。図 7 は、NodejsWebApp プロジェクトの構造を示しています。
図 7 NodejsWebApp プロジェクト
図 8 に示した app.js ファイル (NodejsWebApp/public/app/app.js) が、基本的には (クライアント側の) AngularJS アプリの開始点です。詳細については触れませんが、ここで重要なのは、Single Page Application (SPA、単一ページ アプリケーション) のクライアント側ルートを $routeProvider に登録する点です。ルート (.when メソッドで定義) ごとに、templateUrl プロパティを設定して表示するビュー (HTML) へのパスを指定し、特定のルートのコントローラー プロパティを設定してビューのコントローラーを指定します。AngularJS コントローラーには、ビューが必要とするものを支援するコード、つまりビューのすべての JavaScript コードを含めます。.otherwise メソッドは、どのルートとも一致しない着信要求用の既定のルート (home ビュー) を構成するために使用します。
図 8 App.js ファイル
'use strict';
var myApp = angular.module('myApp',
[
'ngRoute',
'ngAnimate',
'kendo.directives',
'jaydata'
])
.factory("northwindFactory",
[
'$data',
'$q',
function($data, $q) {
// Here you wrap a jquery promise into an angular promise.
// Simply returning jquery promise causes bogus things
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'
});
});
ここで、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンの懸念事項がアプリでどのように表されるか、その概要を示します。
- View = *.html
- ViewModel = *controller.js
- Model = エンティティ (REST エンドポイントから返されるエンティティで、通常はドメイン モデルとエンティティのいずれかまたは両方)
図 9 は、アプリのどのファイルが MVVM パターンのどの懸念事項に対処するかを示します。
図 9 モデル - ビュー - ビューモデル パターン
JayData のクライアント側の DataContext を AngularJS サービスとして定義
ほとんどのコントローラーは Northwind コンテキストを使用するため、northwindFactory というサービスまたはファクトリを作成します。また、Northwind コンテキストの初期化は非同期に行われるため、Northwind コンテキストの初期化が完了していて、コントローラーが読み込まれるまでに使用準備が整うように、JavaScript promise をセットアップします。つまり、Northwind コンテキストの読み込みは、northwindFactory に依存するコントローラーが読み込まれる前に完了するようにします。構成したすべてのルートに "resolve" プロパティがあるのがわかります。このプロパティが、コントローラーが読み込まれる前に解決する必要がある promise を定義する方法です。この場合、"northwind" プロパティを northwindFactory に設定します。"northwind" というプロパティ名は、コントローラーに挿入されるインスタンスの名前にもなります。productController.js のコンストラクター関数については後で確認します (図 11 参照)。この関数では、northwind として northwindFactory が挿入されます。これは、ルートの resolve プロパティで northwindFactory に設定されるプロパティ名です。
Index.html (図 10 参照) が基本的にはレイアウト ページで、AngularJS が ng-view 属性を使用して div に切り替えるビューを把握します。"ng-view" 属性を指定した div の親要素である HTML 要素を構成することによって、AngularJS アプリを指定する必要があります。この場合、"ng-app" を "myApp" に設定します。"myApp" は、app.js で名付けます。
図 10 Index.html ファイル
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<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>
<!-- Binding the application to our AngularJS app: "myApp" -->
<div class="container body-content" ng-app="myApp">
<br />
<br/>
<!-- AngularJS will swap our Views inside this 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>
クライアント側の JavaScript ライブラリのすべての include にコンテンツ配信ネットワーク (CDN) を使用していることに注意してください。クライアント側のライブラリは、コマンド ラインで Bower を使用してローカルにダウンロードできます (一般に、パッケージ マネージャー コンソールを使用して NuGet で .NET プロジェクトに対して行うのと同様です)。Microsoft .NET Framework では、クライアント側とサーバー側の両方のパッケージに NuGet を使用します。ただし、Node.js の分野では、Bower はクライアント側のライブラリまたはパッケージのダウンロードに使用し、NPM はサーバー側のライブラリまたはパッケージのダウンロードとインストールに使用します。
レイアウト UI では、通常のブートストラップ テーマ、つまり、Visual Studio ASP.NET MVC 5 プロジェクト テンプレートによって生成されるテーマを使用します。
商品ビュー
商品ビュー (NodejsWebApp/public/app/views/products.html) に必要なのは、数行の HTML だけです。最初のブロックは、グリッドを表示するための AngularJS に対する Kendo ディレクティブです。
<!-- Kendo UI's AngularJS directive for the grid -->
<div kendo-grid="grid" k-options="options"></div>
<!-- AngularJS template for our View Detail Button in the Grid Toolbar -->
<script type="text/x-kendo-template" id="viewDetail">
<a
class="k-button "
ng-click="viewDetail(this)">View Detail</a>
</script>
2 番目のブロックは、グリッドのツール バーに追加するカスタムの [View Detail] ボタン用の AngularJS テンプレートです。
図 11 は、商品コントローラーの NodejsWebApp/app/controllers/productController.js を示しています。
図 11 商品コントローラー
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);
};
});
商品グリッドを処理するには、Kendo UI DataSource ($scope.options.dataSource) のインスタンスを作成する必要があります。JayData は、OData REST エンドポイントにバインドされる Kendo UI DataSource を初期化するヘルパー メソッドを提供します。JayData の asKendoDataSourcehelper メソッドは、OData サーバー (http://localhost:1337/northwindsvc) によって発行されるメタデータ情報に基づいて DataSource を作成する方法を把握します。そのため、app.js の northwindFactory で $data インスタンスを構成するために使用します。Kendo DataSource の詳細については、Kendo DataViz グラフ作成フレームワークを使用して表示デモを行うときに説明します。
グリッドのツール バーに配置する既定のボタン (作成、保存、キャンセル) と、選択された商品行の詳細 ($scope.viewDetail) を表示する別のビューに移動するためのカスタム ボタンを追加します。[View Detail] ボタンのクリック イベントが発生したら、選択された商品の DataItem を取得し、AngularJS $location サービスを使用して商品の編集ビュー (MyNodejsWebApp/scripts/app/views/edit.html) に移動します。
図 12 は、Edit.html ファイル NodejsWebApp/public/app/views/edit.html を示しています。
図 12 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>
入力を ng-model 属性で装飾する方法がわかります。この属性は、ng-model 値が $scope コントローラーで設定されるプロパティに、その入力の値を格納することを宣言によって示す AngularJS の方法です。たとえば、このビューの最初の入力フィールドは、HTML 要素 ID を productName (id="productName") に設定し、ng-model を product.ProductName に設定しています。つまり、ユーザーが入力フィールド (テキスト ボックス) に入力した値に応じて、$scope.productName の値が設定されます。さらに、プログラムによって editController で $scope.product.productName に設定された値が、productName の入力フィールドの値に自動的に反映されます。
たとえば、ビューが初めて読み込まれるときは、URL によって渡された ID で商品を読み込んでから、$scope.product をその商品に設定します (図 13 参照)。これが行われると、ng-model を $scope.property.* に設定したビューに含まれるすべての内容が、$scope.product 内のすべてのプロパティ値に反映されます。これまでの開発者は DOM の操作の種類に対して jQuery または昔ながらの JavaScript を使用して入力フィールドに値を設定するのが一般的でした。MVVM パターン (フレームワークを問わない) を使用してアプリをビルドする場合、直接ではなく (たとえば、JavaScript または jQuery を使用するのではなく) ビューモデルだけを変更して DOM を操作するのがベスト プラクティスです。決して JavaScript や jQuery に問題があると言っているのではありません。しかし、パターンを使用して特定の問題を解決する (ここでは、MVVM を使用して ビュー、ビューモデル、モデルに懸念事項を分離する) 場合、パターンはアプリ全体で一貫している必要があります。
図 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();
// Unwrap kendo dataItem to pure JayData object
northwind.saveChanges();
};
$scope.cancel = function() {
$location.url("/product");
};
});
通常は ASP.NET Web API で処理している POST サーバー側の操作を Node.js に実装できることに注意してください。ただし、今回の目的は、Node.js と OData を使用してこれを行う方法についてのデモを行うことです。
app.post('/api/updateProduct', function(req, res) {
var product = req.body;
// Process update here, typically what is done with the ASP.NET Web API
});
グラフ ビュー (NodejsWebApp/public/app/views/chart.html) の場合、必要なのは 1 行のマークアップだけです。
<kendo-chart k-options="options"></kendo-chart>
ここで行われているのは、Kendo UI の棒グラフ ディレクティブを宣言し、それらのオプションを options というコントローラーのプロパティにバインドするように設定することだけです。図 14 は商品のグラフ ビュー、図 15 は商品のグラフ コントローラーを示しています。
図 14 商品のグラフ ビュー
図 15 商品のグラフ コントローラー
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 としてコントローラーの構築関数に挿入し、再び JayData ヘルパーの asKendoDataSource メソッドを持つ Kendo dataSource を作成します。以下に、グラフ コントローラーで何が行われるのかについて詳しく説明します。
$scope.options.series
- type: グラフの種類を構成します。
- field: 系列 (x 軸) の値に使用するモデルまたはエンティティのフィールドです。
$scope.options.valueAxis
- majorUnit: 主な目盛りの間隔です。valueAxis.type を対数に設定している場合、majorUnit 値が対数の基数に使用されます。
- plotBands: グラフのプロット バンドで、商品の数量を視覚的に示すために使用されます。数量が指定したレベルを下回ったら、ユーザーは商品の補充処理を呼び出します。
- max: y 軸の最大値です。
$scope.options.categoryAxis
- field: x 軸のフィールド ラベルです。
- labels.rotation: ラベルを回転する角度です。ここでは、x 軸のラベルの値を -90 (度) に設定する、つまり、ラベルを 90 度反時計回りに回転することで、現行の x 軸に垂直になるように構成します。
- majorGridLines.visible: 主要グリッド線のオンとオフを切り替えます。見やすくするために、グリッド線をオフにして、グラフを簡潔で洗練された外観にすることをお勧めします。
- tooltip.visible: ユーザーが縦棒をマウスでポイントしたときに、ツール ヒントが有効になります。
詳細については、Kendo UI Chart API (bit.ly/1owgWrS、英語) を参照してください。
Azure Web サイトの配置
便利なことに、ソース コードは CodePlex Git リポジトリでホストされるため、Azure Web サイトを使用して継続的配置 (継続的デリバリー) をセットアップするのが非常に単純です。
- Azure Web サイトのダッシュボードに移動し、[ソース管理からのデプロイの設定] を選択します。
- リポジトリを選択します。この例では、[CodePlex] を選択します。
- [次へ] をクリックします。
- CodePlex プロジェクトを選択します。
- 分岐を選択します。
- [チェック] をクリックします。
- Git リポジトリに同期するたびに、ビルドと配置が行われます。
これだけです。数回のクリック操作のみで、アプリが継続的な統合とデリバリーを使用して配置されます。Git を使用した配置の詳細については、https://azure.microsoft.com/ja-jp/documentation/articles/web-sites-publish-source-control/ を参照してください。
私は .NET 開発者として、ASP.NET MVC、ASP.NET Web API、OData、Entity Framework、AngularJS、および Kendo UI を使用して CRUD を頻繁に行うアプリをいかにすばやく簡単にビルドするかを心から楽しむようになりました。今では、MEAN スタックでの開発により、このドメインの知識と経験のほとんどを JayData ライブラリを使用して活用することができます。2 つのスタックの相違点は、サーバー側のレイヤーだけです。ASP.NET MVC と ASP.NET Web API を使用して開発を行っている場合、既に JavaScript の基本的な経験があるため、Node.js で多くの問題が発生することはないでしょう。今回の例の完全なソース コードは msdnmeanstack.codeplex.com (英語) からダウンロードできます。また、ライブ デモについては、meanjaydatakendo.azurewebsites.net (英語) で視聴できます。
Long Le は、CBRE Inc. の主任アプリ/開発アーキテクトで、Telerik/Kendo UI MVP です。彼は、フレームワークとアプリケーション ブロックの開発、ベスト プラクティスとパターンのガイダンスの提供、およびエンタープライズ テクノロジ スタックの標準化に大部分の時間を費やしています。余暇には、ブログ記事の執筆 (blog.longle.net、英語) や、Call of Duty、メンタリング (codementor.io/lelong37、英語) を楽しんでいます。彼の Twitter (twitter.com/LeLong37、英語) を見つけてフォローしてください。
この記事のレビューに協力してくれた技術スタッフの Robert Bany (JayData)、Burk Holland (Telerik)、および Peter Zentai (JayData) に心より感謝いたします。