More Related Content Similar to エンタープライズ分野での実践AngularJS Similar to エンタープライズ分野での実践AngularJS (20) エンタープライズ分野での実践AngularJS6. 開発対象概要
開発対象
ZAC Enterprise(管理会計システム)
開発体制
日本3~4名
海外(ベトナム)1名
※将来的には、20~30名のエンジニアが開発する想定
技術構成
クライアントサイドAngularJS + TypeScript
サーバーサイドASP.NET + ASP.NET Web API 2 + SQL Server
7. What is ZAC?
案件・プロジェクトベースで、売上や原価の管理を行う
統合型基幹業務システム(ERPパッケージです)
・販売管理
・購買管理
・在庫管理
・勤怠管理
・工数管理
・経費管理
・and more...
http://www.oro.co.jp/zac/
https://www.oro.co.jp/reforma-psa/ (小規模向けのReformaPSAというサービスも10月からスタートしました)
ぜひご検討を!!
8. 導入のきっかけ
コード量を減らしたい→ data binding
UIを統一したい→ directive
クライアント側の開発を、多人数開発で効率よく行いたい→ フルスタック
検討を開始したのは、2013年10月ごろ
利用は、2014年5月ごろから
11. フォルダ構成
/b 既存のシステムと共存できる用、フォルダを分けた
/Content ASP.NETの構成に準拠
/css
/common 共通のcssファイル(sassをここに入れて、VisualStudioで自動コンパイ
ル)
/directives directive用のcss, sassファイル
/Script
/vendor AngularJS本体や、各種プラグイン
/zac
/common 既存のライブラリなど
/directives directive本体が入る
/services service本体が入る
/UI
/Views ベースHTMLが入る
/directives directive用のHTMLファイル
/services
/UI UIを伴うservice用のHTMLファイル
/project 各機能画面(これは、例として案件管理)
/views 案件管理機能のみで使用する、HTMLファイル(専用
directive用も入る)
/directives 案件管理機能のみで使用する、directive本体が入る
project.ts ルーティング用のファイル
project-controller.ts AngularJS用のコントローラー
index.cshtml HTMLファイル(一部ASP.NETのテンプレートを使用)
/approval
~~~ /project と同じ構成~~~
13. ベースHTML
作成
コントローラ
ー作成
API作成
(サーバーサイ
ド)
APIに対応す
る
サービス
作成
モデルクラス
作る
HTMLに肉付
けをしていく
HTMLをディ
レクティブ化
する
必要ならフィ
ルターも作る
完成!
AngularJSを中心に据えた場合の、画面開発の流れ
※T4 Templateで、C#側のモ
デルを修正すると、
TypeScript側のモデルのコー
ドも自動で吐くようになって
ます
明らかな場合は、ディレクテ
ィブに分けて作り始めます
ワイヤー
作成
デザイン
作成
HTML
モック作成
16. VisualStudio
Visual Studio 2013 Express for Webが無料
WebEssentialsを入れると、sassのコンパイルやminifyもラクラク
TypeScriptの恩恵を最大限受けられる
TypeScript
Visual Studio 2013 Update 2からversion 1.0がリリース
Express for Webでも使える
AngularJS
最新の1.3.1もNuGetですぐ入る
19. 型情報について
TypeScriptを使っている場合、型情報を定義したファイルが必要です。(コンパイルエラーになります)
下記から使用したい型情報が書かれたファイルをダウンロードし参照させます。
http://definitelytyped.org/
VisualStudioを使っている場合、NuGetで取得することも可能です。
declare module ng {
// not directly implemented, but ensures that constructed class implements $get
interface IServiceProviderClass {
new (...args: any[]): IServiceProvider;
}
interface IServiceProviderFactory {
(...args: any[]): IServiceProvider;
}
// All service providers extend this interface
interface IServiceProvider {
$get: any;
}
angular.d.ts
21. モジュール
var app = angular.module("app", ["ui.router", "ui.bootstrap", "ui.sortable"]); JavaScript
app.config(["$stateProvicer", function($stateProvider) {
// providerの初期化処理など
});
var app = angular.module("app", ["ui.router", "ui.bootstrap", "ui.sortable"]);
app.config(["$stateProvider", ($stateProvider: ng.ui.IStateProvider) => {
// providerの初期化処理など
});
TypeScript
まずはモジュールを作成する
特に大きな違いはありませんが、DIするオブジェクトにも型を指定可能
23. app.controller("myController", ["$scope", "serviceA", function($scope, serviceA) {
$scope.title = "hoge";
}]);
JavaScript
module Controllers {
'use strict';
export interface IMyControllerScope extends ng.IScope {
title: string;
}
export class MyController {
public static $inject = ["$scope", "serviceA"];
constructor($scope: IMyControllerScope , serviceA: Services.ServiceA) {
$scope.title = "hoge";
}
}
angular.module("app").controller("myController", MyController);
}
TypeScript
JavaScriptの場合
TypeScriptの場合
24. ディレクティブ
<hoge-directive title="hoge" max-count="5" />
angular.module("app").directive("hogeDirective", ["serviceA", "serviceB", function(serviceA, serviceB) {
return {
restrict: "EA",
require: "?ngModel",
scope: {
"title": "?=",
"max-count": "?="
},
templateUrl: this.serviceB.RootPath + "/HogeDirective.html",
link: function(scope, element, attrs, ctrls, transclude) {
element.find("span.title").text(scope.title);
serviceA.do(function(result) {
});
}
};
}]);
HTML
JavaScript
titleとmax-countを取るような単純なディレクティブ
25. module Directives {
'use strict';
export interface HogeDirectiveScope {
title: string;
maxCount: number;
}
export class HogeDirective {
public restrict = "EA";
public require = "?ngModel";
public templateUrl = this.serviceB.RootPath + "/HogeDirective.html"; // templateを指定することもあります
public static $inject = ["serviceA", "serviceB"]; // $injectorを使うため、ここでDIするモジュールを定義している
constructor(private serviceA: ServiceA, private serviceB: ServiceB) {
// コンストラクタにprivateなどのアクセス修飾子をつけると、変数が自動で定義されます
}
public link = (scope: HogeDirectiveScope, element: JQuery, attrs: ng.IAttributes) {
element.find("span.title").text(scope.title);
serviceA.do((result) => {
});
};
}
angular.module("app").directive("hogeDirective", ["$injector", ($injector) => {
return $injector.instantiate(HogeDirective);
}]);
// Directives.addDirective(HogeDirective); // 実際はこういったヘルパーを用意しています
}
TypeScript
26. サービス
angular.module("app").service("zacCookie", function() {
return {
read: function(name) {
return jQuery.cookie(name);
},
write: function(name, value, options) {
jQuery.cookie(name, value, options);
},
remove: function(name, options) {
return jQuery.removeCookie(name, options);
}
}
});
JavaScript
jQuery cookieをラップするサービス(デフォルトのangularjsのcookieライブラリは、pathが変えられれないため
JavaScriptの場合
27. module Services {
'use strict';
export class ZacCookie {
public read(name: string): any {
return jQuery.cookie(name);
}
public write(name: string, value: any, options?: any): void {
jQuery.cookie(name, value, options);
}
public remove(name: string, options? : any) : boolean {
return jQuery.removeCookie(name, options);
}
}
angular.module('app.services').ser
vice("zacCookie", function () {
return new ZacCookie();
});
}
TypeScript
TypeScriptの場合
28. APIと連携するサービス
API側からデータを取ってくるサービスを作る場合、$resourceを使っています(内部で$httpを使う形でも良いかと思います)
angular.module("app").service("projectService", ["$resource", function($resource) {
return $resource('/Project', {}, {
'get': {
url:"/Projects/:id',
method: 'GET',
cache: true
},
'query': {
method: 'GET',
url: "/Projects"
params: { /* デフォルト値があれば設定*/ }
cache: true,
isArray: true
},
'check': { // Restfulでないメソッドも追加可能
method: 'GET'
params: {}
url: "/Projects/Check/:id
}
}
}]);
JavaScript
29. module Services {
'use strict';
export interface IProjectService extends ng.resource.IResourceClass<Models.ProjectItem> {
check(params?: Object, success?: Function, error?: Function): ng.IPromise<Models.ProjectItem>;
}
export function ProjectService($resource: ng.resource.IResourceService) {
return $resource<Models.ProjectItem>("/Project', {}, {
'get': {
url:"/Projects/:id',
method: 'GET',
cache: true
},
'query': {
method: 'GET',
url: "/Projects"
params: { /* デフォルト値があれば設定*/ }
cache: true,
isArray: true
},
'check': { // Restfulでないメソッドも追加可能
method: 'GET'
params: {}
url: "/Projects/Check/:id
}
}]);
}
ProjectService.$inject = ["$resource"];
angular.module("app").factory("projectService", ProjectService);
}
TypeScript
TypeScriptの場合
31. module Filters {
'use strict';
export function ProjectNumber(serviceA: Services.ServiceA) {
return function (input) {
if (angular.isUndefined(input) || input == null) return "";
return "#" + input;
}
}
angular.module("app").filter("projectNumber", ProjectNumber);
}
TypeScript
angular.module("app").filter("projectNumber", ["serviceA, function(serviceA) {
return function(input) {
if (angular.isUndefined(input) || input == null) return "";
return "#" + input;
}
}]);
JavaScript
JavaScriptの場合
TypeScriptの場合
32. フォーム
<form ng-form name="myForm">
<input type="text" name="title" ng-model="title" ng-maxlength="5">
<div ng-if="myForm.title.$error.maxlength">5文字を超えています</div>
</form>
HTML
既存のフォームの書き方は、特に変わりはないです(そりゃそうだ・・・)
34. app.controller('myController', ['$scope', function($scope) {
$scope.name = "taro yamada";
$scope.reset = function() {
$scope.name = "taro yamada";
$scope.myForm.$setPristine();
};
}]);
app.directive("fullnameInput", [function() {
return {
restrict: "EA",
require: ["?ngModel", "?^form"],
scope: {},
replace: true,
template: "<div><input type='text' ng-model='firstName'><input type='text' ng-model='lastName'></div>",
link: function(scope, element, attrs, ctrls, transclude) {
var ngModelCtrl = ctrls[0];
var ngFormCtrl = ctrls[1];
if (ngFormCtrl) {
// 標準のinput要素とは別で、状態が変わる操作があれば、明示的にngFormCtrl.$setDirty();を呼ぶ
ngFormCtrl.$setDirty();
}
JavaScript
JavaScriptの場合
35. JavaScript
replace: true,
template: "<div><input type='text' ng-model='firstName'><input type='text' ng-model='lastName'></div>",
link: function(scope, element, attrs, ctrls, transclude) {
var ngModelCtrl = ctrls[0];
var ngFormCtrl = ctrls[1];
if (ngFormCtrl) {
// 標準のinput要素とは別で、状態が変わる操作があれば、明示的にngFormCtrl.$setDirty();を呼ぶ
ngFormCtrl.$setDirty();
}
if (ngModelCtrl) {
ngModelCtrl.$render = function() {
var value = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
if (angular.isUndefined(value) || value === null) {
// クリア処理
scope.firstName = "";
scope.lastName = "";
} else {
// 値をelementに設定する処理
var names = value.split(" ");
scope.firstName = (0 < names.length) ? names[0] : "";
scope.lastName = (1 < names.length) ? names[1] : "";
}
}
}
36. JavaScript
}
function setViewValue() {
var name = scope.firstName + (scope.lastName ? " " : "") + scope.lastName;
ngModelCtrl.$setViewValue(name);
}
scope.$watch("firstName", function(newValue) {
setViewValue();
});
scope.$watch("lastName", function(newValue) {
setViewValue();
});
}
};
}]);
39. app.directive("customMaxlength", function() {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ngModelCtrl) {
var maxlength = attrs['customMaxlength'];
if (maxlength) {
ngModelCtrl.$validators['customMaxlength'] = function(modelValue, viewValue) {
var value = modelValue || viewValue;
if (angular.isUndefined(value) || value === null) return true;
return value.length <= maxlength;
}
}
}
}
}
});
JavaScript
JavaScriptの場合
40. module Directives {
'use strict';
export class CustomMaxlength {
public restrict = "A";
public require = "ngModel";
constructor() {
}
public link = (scope:ng.IScope, element: JQuery, attrs: ng.IAttributes, ngModelCtrl: ng.INgModelController) {
var maxlength: number = attrs['customMaxlength'];
if (maxlength) {
ngModelCtrl.$validators['customMaxlength'] = function(modelValue, viewValue) {
var value = modelValue || viewValue;
if (angular.isUndefined(value) || value === null) return true;
return value.length <= maxlength;
}
}
};
}
angular.module("app").directive("customMaxlength", ["$injector", ($injector) => {
return $injector.instantiate(CustomMaxlength);
}]);
}
TypeScript
TypeScriptの場合
41. 非同期バリデーション
<form name="myForm">
<input type="text" name="projectCode" check-project-code ng-model="projectCode">
<div ng-if="myForm.projectCode.$error.checkProjectCode">無効なコードです</div>
</form>
HTML
サーバー側でコードチェックを行いたい、というのはよくあります。
check-project-codeを実装してみる
42. app.directive("checkProjectCode", ["$q", "projectService", function($q, projectService) {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ngModelCtrl) {
var deferred =$q.defer();
projectService.get(function(project) {
// なんらかの追加のチェックをここで行う
deferred.resolve(project);
}, function(error) {
deferred.reject(error);
});
return deferred.promise;
}
}
}]);
JavaScript
JavaScriptの場合
※非同期バリデーションの場合、promiseを返す。有効な値の場合はresolve、無効な値の場合は、rejectを呼び出す
サービスがpromiseを返すようになっている場合($http.getなど)そのまま返しても良い
43. module Directives {
'use strict';
export class CheckProjectCode {
public restrict = "A";
public require = "ngModel";
public static $inject = ["$q", "projectService"];
constructor($q: ng.IQService, projectService: Services.Project) {
}
public link = (scope:ng.IScope, element: JQuery, attrs: ng.IAttributes, ngModelCtrl: ng.INgModelController) {
var deferred = this.$q.defer();
this.projectService.get((project) => {
deferred.resolve(project);
}, (error) => {
deferred.reject(error);
});
return deferred.promise;
};
}
angular.module("app").directive("checkProjectCode", ["$injector", ($injector) => {
return $injector.instantiate(CheckProjectCode );
}]);
}
TypeScript
TypeScriptの場合
46. レイアウト
<custom-layout="Layout1">
<div ng-repeat="layout in layouts">
<span>{{ layout.name }}</span>
</div>
</custom-layout>
HTML
単純なレイアウト(ng-repeatで繰り返す)
分岐を伴うレイアウト
<custom-layout="Layout2">
<div ng-repeat="layout in layouts">
<div ng-if="layout.type == 'hoge'">
</div>
<div ng-if="layout.type == 'fuga'">
</div>
<layout-item item="layout" /> <!-- 分岐の中身が複雑な場合、専用のdirectiveを作ります-->
</div>
</custom-layout>
HTML
47. レイアウト
module Directives {
export interface ICustomLayoutScope extends ng.IScope {
layouts:LayoutItem[]; // 別で定義した、モデルクラスデータ(ASP.NET側とDSLを使ってモデルクラスのスキーマは
共有しています)
}
export class CustomLayout {
public static $inject = ["layoutService"];
public restrict = "EA";
public scope = {};
public transclude = true;
public template = "<div ng-transclude></div>";
constructor(private layoutService: Services.LayoutService) {
}
public link = (scope: ng.IScope, element: JQuery, attrs: ng.IAttributes) {
var layoutName = attrs["zacLayout"];
if (layoutName) {
layoutService.get(layoutName, function(layouts) {
scope.layouts = layouts;
});
}
}
}
}
TypeScript
48. 権限
<div class="a" has-permission="permissionA"><!-- name --></div>
<div class="b" has-permission="permissionB"><!-- sales --></div>
<div class="c" has-permission="permissionC"><!-- cost --></div>
HTML
権限の有無によって、表示/非表示を切り替える
例えば、人によって売上データを見せたくないという場合がある。
API側ではデータを制御し、AngularJS側では表示を制御する
49. module Directives {
export interface IHasPermission {
show: boolean;
};
export class HasPermission {
public restrict = "A";
public static $inject = ["permissionService"];
constructor(permissionService: Services.Permission) {
}
public link = (scope:IHasPermission, element: JQuery, attrs: ng.IAttributes) => {
element.hide();
var typePermission= attrs["hasPermission"] || "";
if (typePermission == "") return;
this.permissionService.get(typePermission, (hasPermission) => { // サーバーから権限情報を取得
if (hasPermission) {
element.show(); // 権限があれば、elementを表示する
}
});
};
}
}
TypeScript
単純に、サーバーから権限情報を取得して、表示を制御する
50. <permission-manager>
<div class="a" has-permission="permissionA"><!-- name --></div>
<div class="b" has-permission="permissionB"><!-- sales --></div>
<div class="c" has-permission="permissionC"><!-- cost --></div>
</permission-manager>
HTML
実際は、個別に取得すると重いので(AngularJSを使ってると、気づくと非同期処理が大量になりがちです)
親ディレクティブを追加して、子ディレクティブと連携し、まとめて取得するようにしています
51. export class HasPermission {
// 省略
public require = "?^PermissionManager";
public template = "<div ng-if="show" ng-transclude></div>";
constructor(permissionService: Services.Permission) {
}
public link = (scope: IHasAuthority, element: JQuery, attrs: ng.IAttributes, ctrl: PermissionManagerCtrl) => {
var get = ctrl ? ctrl.get || permissionService.get; // PermissionManagerがいたら、そっちのgetを使う
get(typePermission, (hasPermission) => {
if (hasPermission) {
element.show(); // 権限があれば、elementを表示する
}
});
};
}
TypeScript
52. export class PermissionManagerCtrl {
public queue: any = [];
public get = (typePermission: string, success: (hasPermission: boolean) => void) => {
this.queue.push({
typePermission:typePermission,
success: success
});
};
public processAllQueues = () => {
// 1.サーバーから権限情報(queueの中身を使って)をまとめて取得する(APIを用意してお
く)
// 2.queue内のsuccessをそれぞれ呼び出す
}
}
TypeScript
子ディレクティブからアクセスするためのコントローラーを用意
53. export class PermissionManager {
public restrict = "EA";
public transclude = true;
public template = "<div ng-transclude></div>";
public controller = PermissionManagerController;
constructor() {
}
public link = (scope: IHasPermission, element: JQuery, attrs: ng.IAttributes) => {
this.controller.processAllQueues();
};
}
TypeScript
linkではprocessAllQueuesを呼び出す
54. カスタマイズ対応
5つの心得
1.Controllerは肥大化させない(scopeで渡すだけに専念させる) 個社で画面を作り直すときに、ほとんど再利用できなくなります
2.View側も、ディレクティブを使ってパーツ化を進める上と同じ
3.環境ごとの対応は、ControllerとViewを作り直したほうが、コストが低くなる分岐点がある(保守のコストも鑑みて)
Controllerが肥大化していなく、Viewがディレクティブ化されていれば、新しい画面を作るのは楽です
4.画面を作り直すほどではない、多少の分岐をどうしても入れたい場合、ディレクティブを分けて分岐する
5.そもそも個社毎にカスタムしないで済むように作る
56. Tips
フィルター処理は、サーバーor クライアントサイドどっちでするべきか
「サーバーサイドはAPIのみの実装にしましょう。」という話はよく聞きますが、
フォーマット処理などはどちらにすればいいの?と疑問に思うことがあります。
僕のチームでは、フィルター処理はサーバーサイドで行っています。
多言語対応も考えると、API側で可能な限り言語も言語ごとのフォーマットをしてから、返すべきだからです。
どうもクライアント側に色々処理をもたせようとして、色々フィルターを作ってしまうのですが、
それはクライアント側でするべきか考えましょう!フィルター処理はご存知の通り、重いです。
認証後の認証情報(例えばユーザーIDなど)はどこに持たせておくべきか
これは、ZACではheadのmetaで持たせています。(良いやり方とは思っていませんが)
API側はOAuth認証等でクライアントと分離して認証できるべきかと思います。
その中で、AngularJS側でセッションを管理するサービスを作り、ユーザー情報を取得するようにすると良い、と思いま
す。
アプリ+ APIの構成でアプリを作っていると、自ずとログイン情報を取得するAPIを作り、
アプリ側でログイン情報をキャッシュすると思いますが、考え方はアプリを作成する時と同じです。
57. ・VisualStudio + TypeScript + AngularJSで、Windows畑でも導入可能
フルスタックで型セーフなのは、多人数開発にも有利です
・TypeScriptのサンプルは少ないですが、JSとの互換性が高い
AngularJSのモジュールの考え方と方向性があっている
・基幹システム開発になると、権限管理や複雑なインプット
大量データ表示など、通常のWeb開発では当たらない問題が多いが
AngularJSを使用すると、スマートに解決できる
サービス、ディレクティブ、コントローラー、フィルターをうまく使いこなす
まとめ
Editor's Notes レガシーなコードも混じっている、そういうパッケージも多いのでは。部分的に変えていっております。 ・MSBuildでビルド、NUnitでテスト、IISにアプリを立てて、PhantomJSでJasmineを動かす(クロスブラウザも考えて、Seleniumにする予定) 既存の機能が数多く有り、それをどのようにAngularJSで表現するかが、一番迷う
(さきほどのレイアウトだったり、権限だったり、他にもめちゃくちゃいっぱいあります) ちょっと開発側の人事も手伝っていることもあり、宣伝させてください。