home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

JavaScript/ExtJS/「メンテナブルJavaScript」とExtJS

JavaScript/ExtJS/「メンテナブルJavaScript」とExtJS

JavaScript / ExtJS / 「メンテナブルJavaScript」とExtJS
id: 1298 所有者: msakamoto-sf    作成日: 2014-07-06 19:04:50
カテゴリ: ExtJS JavaScript 

「メンテナブルJavaScript」を読みまして、ExtJSではどうなるのか調べてみました。

「メンテナブルJavaScript」ではJavaScriptならではの落とし穴や、メンテナンス性を下げるようなBad Practiceと、より良い実装のGood Practiceが紹介されています。
ExtJSでは、それら実装が難しい処理のいくつかについて、その実装を隠蔽しよりクリーンなインターフェイスとして、開発者がその実装を気にしなくても安全に使えるようなユーティリティクラス・メソッドを提供しています。
本記事では「メンテナブルJavaScript」で「それ、ExtJSでできるよ」というトピックについて、一部サンプルコードも交えてExtJSで提供されている機能を紹介していきます。(特に紹介していない章については、ExtJSでも通用する普遍的な内容でした。)

注意:本記事ではあくまでも、ExtJSが詳細を隠蔽してくれているクラス・メソッドの紹介までに留めています。隠蔽している実装が本当に妥当なものであるかどうかの検証はしていません。一応ExtJS 5.0のソースを軽く読んでみて、大体書籍にあるようなノウハウが組み込まれている「っぽい感じ」はしてますが、徹底した厳密な調査・検証・テストは実施できていません。そのため、実際に使ってみると「このケースだとExtJSの実装で不都合が出てしまうな~」というのも出てくるかもしれませんが、ご了承ください。

環境:

  • ExtJS 5.0.0 GPLバージョン(ExtJSのCDN環境で配布されているものを使用)
  • ※ExtJS 4.2 でも同名のクラス・メソッドの存在まで確認しています。ただし動作までは検証していないため、もしかしたら動作が多少変わるかもしれません。

公式APIドキュメント:

目次:


サンプルコードを動かしたい場合の準備:Sencha公式サイトの"Hello World"を用意する。

本記事で紹介しているサンプルコードを動かしたい場合の準備について説明します。

まず、Sencha公式サイトで紹介されている"Hello World"を動かします。

index.html: 公式サイトのサンプルでは"ext-all.js"を読み込んでいますが、幸い"ext-all-debug.js"が置いてあるようでしたのでそちらに修正してます。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <link rel="stylesheet" type="text/css" href="http://cdn.sencha.com/ext/gpl/5.0.0/build/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css">
  <script type="text/javascript" src="http://cdn.sencha.com/ext/gpl/5.0.0/build/ext-all-debug.js"></script>
  <script type="text/javascript" src="app.js"></script>
</head>
<body>
</body>
</html>

app.js: 公式サイトのままですので割愛します。

動作確認ができましたら、"app.js" をロードしている部分をサンプルのJSファイルに書き換えて、試すことができます。

「3章 文と式 3.5 forループ, 3.6 for-inループ」

「3.5 forループ」では配列をループさせるときのやり方について、「3.6 for-inループ」についてはオブジェクトのプロパティをiterateする方法が紹介されています。
hasOwnProperty()を使ったり、配列を間違ってfor-inループで使わないなどのノウハウが解説されていますが、ExtJSの提供するいくつかのユーティリティクラスを使うことで、そうしたプラクティスの詳細を意識しなくて済むようになります。

配列の要素をループ処理したい時は Ext.iterate() (=Ext.Array.each())

Ext.iterate()は内部で引数の型を見て、Arrayの場合はExt.Array.each()を呼び出します。

iter-arr-el.js:

var arr = [10, 20, 30];
var sum = 0;
Ext.iterate(arr, function(v, index) {
  console.log('v=', v, 'index=', index);
  sum += v;
});
console.log('sum=', sum);
 
sum = 0;
Ext.Array.each(arr, function(v, index) {
  console.log('v=', v, 'index=', index);
  sum += v;
});
console.log('sum=', sum);

→ブラウザの開発ツールのコンソールログ:

v= 10 index= 0    iter-arr-el.js (4 行目)
v= 20 index= 1    iter-arr-el.js (4 行目)
v= 30 index= 2    iter-arr-el.js (4 行目)
sum= 60           iter-arr-el.js (7 行目)
v= 10 index= 0    iter-arr-el.js (11 行目)
v= 20 index= 1    iter-arr-el.js (11 行目)
v= 30 index= 2    iter-arr-el.js (11 行目)
sum= 60           iter-arr-el.js (14 行目)

オブジェクトのプロパティをループ処理したい時は Ext.iterate() (=Ext.Object.each())

Ext.iterate()は内部で引数の型を見て、Objectの場合はExt.Object.each()を呼び出します。
Ext.Object.each()ではhasOwnProperty()を内部でチェックしてくれますので、プロトタイプチェーンは辿らず、そのオブジェクト自身のプロパティだけを処理できます。

Ext.iterate()およびExt.Object.each()(とExt.Array.each()) では、ループを処理するfunctionをどのスコープで処理するのか、thisとなるオブジェクトを指定できます。
以下のサンプルでは、MyHandlerというロギングハンドラ用のオブジェクトを作成し、そのhandle()メソッドをループ処理中から呼び出します。そのため、MyHandlerのインスタンスをscopeとしてExt.iterate()の第3引数にしていしています。これにより、第二引数のfunction中から、"this.handle(...)" としてMyHandlerのインスタンスのhandle()メソッドを呼び出せます。
iter-obj-props.js:

var MyHandler = function(handlerName, appData) {
  this.name = handlerName;
  this.data = appData;
  this.handle = function() {
    console.log(this.name, this.data, arguments);
  };
};
var h1 = new MyHandler('handler1', 'appdata1');
var h2 = new MyHandler('handler2', 'appdata2');
 
var o1 = {
  k1: 'hello',
  k2: 100,
  k3: [10, 20, 30],
  k4: {
    k41: 'foo',
    k42: 'bar',
    k43: 'baz'
  }
};
console.log('--------------->> o1');
// o1をループ処理する。このときは、MyHandlerのインスタンスとして h1 を使う。
Ext.iterate(o1, function(propName, propValue) {
  // このthisは h1 インスタンスとなる。
  this.handle(propName + ':' + propValue);
}, h1);
 
var Klass1 = function(firstName, familyName) {
  this.fullName = firstName + ' ' + familyName;
};
Klass1.prototype = o1;
var o2 = new Klass1('myFirstName', 'myFamilyName');
console.log('--------------->> o2 (prototype is o1)');
// o1をループ処理する。このときは、MyHandlerのインスタンスとして h2 を使う。
Ext.iterate(o2, function(propName, propValue) {
  // このthisは h2 インスタンスとなる。
  this.handle(propName + ':' + propValue);
}, h2);
 
console.log('--------------->> for-in o2 (prototype is o1)');
for (prop in o2) {
  h1.handle(prop, o2[prop]);
}

→ブラウザの開発ツールのコンソールログ:

--------------->> o1                                         iter-obj-props.js (21 行目)
handler1 appdata1 ["k1:hello"]                               iter-obj-props.js (5 行目)
handler1 appdata1 ["k2:100"]                                 iter-obj-props.js (5 行目)
handler1 appdata1 ["k3:10,20,30"]                            iter-obj-props.js (5 行目)
handler1 appdata1 ["k4:[object Object]"]                     iter-obj-props.js (5 行目)
--------------->> o2 (prototype is o1)                       iter-obj-props.js (31 行目)
handler2 appdata2 ["fullName:myFirstName myFamilyName"]      iter-obj-props.js (5 行目)
--------------->> for-in o2 (prototype is o1)                iter-obj-props.js (36 行目)
handler1 appdata1 ["fullName", "myFirstName myFamilyName"]   iter-obj-props.js (5 行目)
handler1 appdata1 ["k1", "hello"]                            iter-obj-props.js (5 行目)
handler1 appdata1 ["k2", 100]                                iter-obj-props.js (5 行目)
handler1 appdata1 ["k3", [10, 20, 30]]                       iter-obj-props.js (5 行目)
handler1 appdata1 ["k4", Object { k41="foo", k42="bar", k43="baz"}]  iter-obj-props.js (5 行目)

「4章 変数、関数、演算子」

ExtJSで用意されているいくつかのユーティリティクラス、ユーティリティメソッドに関連するトピックがありますので、紹介します。

「4.5 等価性」

書籍の方ではプリミティブ値との比較で、自動変換がかかる場合など細かいノウハウが紹介されています。
ExtJSではプリミティブ値の比較については特にサポート用のクラスやメソッドはありません。ただし、オブジェクトインスタンスの比較については Ext.Object.equals() というメソッドが用意されていますので、サンプルコードを紹介します。

オブジェクト同士の比較 -> Ext.Object.equals()

ext-obj-equals.js:

var o1 = { k1: 10, k2: 'abc' };
var o2 = { k1: 10, k2: 'abc' };
console.log(Ext.Object.equals(o1, o2));
 
o2.k3 = 20;
console.log(Ext.Object.equals(o1, o2));
 
o2 = o1;
console.log(Ext.Object.equals(o1, o2));

→ブラウザの開発ツールのコンソールログ:

true     ext-obj-equals.js (3 行目)
false    ext-obj-equals.js (6 行目)
true     ext-obj-equals.js (9 行目)

「4.6 eval()」

書籍では setInterval(), setTimeout() での eval() の使用例が紹介されています。
ここではeval()それ自体のトピックではなく、ExtJSで setInterval(), setTimeout() を利用するためのお作法について紹介します。

ExtJSでの setTimeout() -> Ext.defer() (=Ext.Function.defer())

指定したミリ秒後に処理を実行するには、通常のJavaScriptではsetTimeout()を使います。
ExtJSにおいては、Ext.defer() (=Ext.Function.defer()) というラッパーメソッドが提供されています。

以下、scope + args指定付きのサンプルです。
ext-defer.js:

var MyHandler = function(handlerName, appData) {
  this.name = handlerName;
  this.data = appData;
  this.handle = function() {
    console.log(this.name, this.data, arguments);
  };
};
var h1 = new MyHandler('handler1', 'appdata1');
var h2 = new MyHandler('handler2', 'appdata2');
 
Ext.defer(function(name, age, hobby) {
  this.handle(Ext.String.format(
    'My name is {0}, {1} years old, {2} is my hobby.',
    name, age, hobby));
}, 1000, h1, ['Bob', 20, 'cooking']);
 
Ext.Function.defer(function(){
  console.log('2000 ms');
}, 2000);
 
Ext.defer(function(name, age, hobby) {
  this.handle(Ext.String.format(
    'Your name is {0}, {1} years old, {2} is your hobby.',
    name, age, hobby));
}, 3000, h2, ['Bob', 20, 'cooking']);

→ブラウザの開発ツールのコンソールログ:

handler1 appdata1 ["My name is Bob, 20 years old, cooking is my hobby."]   ext-defer.js (5 行目)
2000 ms                                                                    ext-defer.js (18 行目)
handler2 appdata2 ["Your name is Bob, 20 yea... cooking is your hobby."]   ext-defer.js (5 行目)
ExtJSでの setInterval() -> 特になし。そのまま使う。

setInterval() については、特にラッパーメソッドなど無いようです。
以下のフォーラムでも、setInterval()をそのまま使うよう、Senchaの人から回答が来ています。

「4.7 プリミティブラッパー型」

プリミティブラッパー型の話題とはあまり関係ありませんが、Extでは独自にExt.StringやExt.Numberといったユーティリティクラスを用意しています。
その中で便利そうなメソッドを幾つか紹介します。

数値表現の文字列(多分)を、数値に変換したい -> Ext.Number.from()

ユーザ入力やAPIのレスポンスから受け取った文字列を、数値に変換したい場面は多いと思います。
自力で行おうとすると、JavaScriptの提供するパース処理+エラーハンドリングを実装する必要があります。
Ext.Number.from()を使えば、不正な文字列だった場合のデフォルト値を渡すことで、エラーハンドリングの実装を隠蔽できます。

str-to-num.js:

var testee = [
  '1.23',
  '0.0',
  '+0',
  '-0',
  '1.23ab',
  'abc',
  undefined,
  null,
  '',
  {},
  [],
  'dsako091e8w7qxsa.asd0923'
];
Ext.iterate(testee, function(v) {
  console.log(v, '=>', Ext.Number.from(v, 1));
});

→ブラウザの開発ツールのコンソールログ:

1.23 => 1.23           str-to-num.js (16 行目)
0.0 => 0               str-to-num.js (16 行目)
+0 => 0                str-to-num.js (16 行目)
-0 => -0               str-to-num.js (16 行目)
1.23ab => 1            str-to-num.js (16 行目)
abc => 1               str-to-num.js (16 行目)
undefined => 1         str-to-num.js (16 行目)
null => 1              str-to-num.js (16 行目)
=> 1                   str-to-num.js (16 行目)
Object {} => 1         str-to-num.js (16 行目)
[] => 1                str-to-num.js (16 行目)
dsako091e8w7qxsa.asd0923 => 1  str-to-num.js (16 行目)
sprintfを使いたい -> Ext.String.format() , HTMLエスケープしたい -> Ext.String.htmlEncode() , 同じ文字列を複数回繰り返しした文字列を生成したい -> Ext.String.repeat()

Ext.Stringクラスに用意されている便利なstaticメソッドをいくつか紹介します。

str-utils.js:

console.log(Ext.String.format('my name is {0}, age is {2}, hobby is {1}', ['bob', 'cooking', 20]));
 
var s1 = '<b>Hello, "jon" & \'bob\'.</b>';
var s2 = Ext.String.htmlEncode(s1);
console.log(s2);
var s3 = Ext.String.htmlDecode(s2);
console.log(s3);
 
console.log(Ext.String.repeat('--', 3, '/'));

→ブラウザの開発ツールのコンソールログ:

my name is bob,cooking,20, age is , hobby is                       str-utils.js (1 行目)
&lt;b&gt;Hello, &quot;jon&quot; &amp; &#39;bob&#39;.&lt;/b&gt;     str-utils.js (5 行目)
<b>Hello, "jon" & 'bob'.</b>                                       str-utils.js (7 行目)
--/--/--                                                           str-utils.js (9 行目)
空の関数を指定する -> Ext.emptyFn

デフォルトのイベントハンドラなどで、空っぽの関数を指定しておきたい場合があります。
Ext.emptyFnを指定しておけば、 "function(){}" をタイピングする手間が減ります。

「5章 UI層での疎結合」

この章でのトピックについては、特にExtJSでの開発で気にする必要はなさそうに思いました。
もともとExtJSでの開発では、ExtJSのクラスタシステム上でUIがJavaScriptで構築できるようになっており、HTMLのDOM要素やCSS設定について、Ext.dom.Elementなどで疎結合になるよう調整されています。
DOM要素の構築についてもExt.DomHelperクラスからJavaScriptのスキーム上でDOM要素を構築できるようになっています。
必要であれば、Ext.XTemplateを使ったテンプレート処理も可能です。

「6章 グローバル変数/関数を作らない」

この章でのトピックについては、特にExtJSでの開発で気にする必要は無いと思います。
ExtJSのクラスシステムや、MVCアプリケーションのアーキテクチャでは名前空間を分離してクラスを構築する仕組みが組み込まれているため、開発者としては特に意識しない限りはグローバルスコープを操作することは無いと思います。

「7章 イベント処理」

この章でのトピックについては、ExtJS側でも気にしておく必要はありそうです。
ExtJSでは、イベント設定こそ、各種ラッパークラス等でブラウザ間の細かい差異を気にせずに設定できるようになっていますが、イベントハンドラの引数は意識しておく必要があります。
そのため、特にイベントハンドラ設定が煩雑になりがちなControllerクラスやカスタムコンポーネントでこそ、引数で渡されるイベントオブジェクトを引き回さず、必要な情報だけを実際の処理に渡すような実装を心がけておいたほうが良さそうです。

「8章 nullとの比較を避ける」 -> Ext.isEmpty(), Ext.isDefined(), 型の判定なら Ext.isXxxx(), 型情報の取得なら Ext.typeOf()

ExtJSでは、nullやundefined周りや型情報を扱うときの細かい実装調整を隠蔽してくれる、便利なユーティリティメソッドを用意してくれていますので、いくつか紹介します。
(ドキュメントを確認したり、ソースを見ればすぐ分かるようなメソッドばかりですので、サンプルコードは割愛します。)

空かどうか、定義済みかどうか。

Ext.isEmpty()
null, undefined周りの細かい調整を気にせずに、単純に空かどうかを判定してたい、特に空文字列かどうかを判定したいときに利用できます。
Ext.isDefined()
undefined周りの細かい操作を気にせずに、変数が定義済みか、未定義かを判定できます。

型情報を取得したい。

Ext.typeOf()
その変数の型を、文字列で取得できます。どの型だとどんな文字列で返されるかは、APIのドキュメントを参照して下さい。

型を判定したい。

Ext.isXxxxx()
ひと通り揃っています。 -> Ext.isArray()/isBoolean()/isDate()/isElement()/isFunction()/isMSDate()/isNumber()/isNumeric()/isObject()/isPrimitive()/isString()/isTextNode()

「10章 自前のエラーを投げる」

ExtJSでは、Ext.Error というヘルパークラスが用意されています。Ext.Error.raise()で、Ext.Errorクラスのインスタンスとして例外を投げてくれます。debugビルドの場合は、自動的にExt.log()によるロギングもしてくれます。

ext-error.js:

try {
  Ext.Error.raise('test error');
} catch(e) {
  console.log(e);
}
 
try {
  Ext.Error.raise({
    msg: 'error message',
    option: {k1: 10, k2: 'hello'},
    'error code': 100
  });
} catch(e) {
  console.log(e);
}
 
Ext.Error.ignore = true;
try {
  Ext.Error.raise('ignored');
  console.log('completed');
} catch(e) {
  console.log(e);
}
 
// handlerのデフォルト実装がignore設定を返す仕組みのため、
// handlerを独自に設定すると、ignore設定は無視されるようになります。
Ext.Error.handle = function(err) {
  if (err.skipflag === true) {
    return true;
  }
};
try {
  Ext.Error.raise({
    msg: 'skip this',
    skipflag: true
  });
  Ext.Error.raise('dont skip');
} catch(e) {
  console.log(e);
}

※ダンプログのコピペが大変なので、コンソールログ出力は省略します。

「12章 ブラウザ判定」

ExtJSではブラウザやOS判定用のユーティリティクラスやメソッドが用意されています。ExtJS 4.2 と ExtJS 5.0 で構成が変わっていましたので、簡単にクラス名とメソッド名だけそれぞれ紹介します。

ExtJS 5.0:

Ext.browser
ブラウザ判定用のクラス。
Ext.os
ブラウザのOS判定用のクラス。

ExtJS 4.2:

ブラウザ判定
Ext.isFFnn()/isGeckonn()/isIEnn()/isOperann()/isSafarinn()/isWebKit()/isChrome() (nnの部分は判定でポイントとなるバージョン番号), Ext.(chrome|firefox|ie|opera|safari|webKit)Version プロパティ
ブラウザのOS判定
Ext.isLinux()/isMac()/isWindows()

「第Ⅲ部 自動化」

ExtJSの場合は、Sencha Cmdと組み合わせることでJavaScriptのminifyやSass/Compassを使ったスタイルシート処理を統合してくれています。
ただ、ドキュメンテーションについてはExtJSの公式ドキュメントでも明記されてる箇所が見当たりませんでしたので、それについてだけメモしておきます。
ExtJSにおけるテストとその自動化については、重たい内容であり、自分自身勉強不足な分野でもあるため本記事では取り上げません。

「18章 ドキュメンテーション」

ExtJSのクラスシステムでは、configurationやmixin, staticなどExtJS独自の概念が使われているため、ドキュメンテーションツールについてもそれらに対応したものが必要となります。
ExtJSでは、JSDuckというドキュメントジェネレータを使っています。

"@"によるアノテーションの種類が豊富なので、自在に使えるようになるまでは時間がかかるかもしれません。自分も、まだ満足に使いこなせてない状況です。


あとがき

ExtJSは年月を経て進化を続けている、割りと大きめなJavaScriptフレームワークです。そのため、ソースを覗いてみても即座に理解できるような単純な構成にはなっていません。
公式APIドキュメントやサンプルコードをコピペしてもある程度のものは作れますが、やはり実際の開発現場では、ドキュメントを読んだりサンプルを見ただけではすぐに実現方法が分からないような機能要求も出てきます。
そうした場合に、やはりソースを読んで理解を深めていかざるを得ません。

幸いなことに、公式APIドキュメントではクラスやメソッド名の近くに表示されている"view source"をクリックすれば該当するソースコードを閲覧できます。
本記事が、ExtJSが提供している便利なユーティリティクラス・メソッドを紹介するだけでなく、ソースを読んでより深くExtJSを理解するきっかけになれば幸いです。

冒頭にも記載しましたが、「メンテナブルJavaScript」で推奨されている方式がExtJSで実装されているのかどうか、そこまでは今回は検証していません。
ExtJSのユーザそれぞれが、本記事をヒントにしてそれぞれでExtJSの実装を調べて、改善提案などをSenchaに寄せていただければ、本記事の執筆者の喜びとしてこれに勝るものはありません。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2014-07-06 20:25:10
md5:9589dfaf089421e76bcf7136ffce7ed6
sha1:4389a1bfe4668b796fa055524d63debe95bd25a7
コメント
コメントを投稿するにはログインして下さい。