「メンテナブルJavaScript」を読みまして、ExtJSではどうなるのか調べてみました。
「メンテナブルJavaScript」ではJavaScriptならではの落とし穴や、メンテナンス性を下げるようなBad Practiceと、より良い実装のGood Practiceが紹介されています。
ExtJSでは、それら実装が難しい処理のいくつかについて、その実装を隠蔽しよりクリーンなインターフェイスとして、開発者がその実装を気にしなくても安全に使えるようなユーティリティクラス・メソッドを提供しています。
本記事では「メンテナブルJavaScript」で「それ、ExtJSでできるよ」というトピックについて、一部サンプルコードも交えてExtJSで提供されている機能を紹介していきます。(特に紹介していない章については、ExtJSでも通用する普遍的な内容でした。)
注意:本記事ではあくまでも、ExtJSが詳細を隠蔽してくれているクラス・メソッドの紹介までに留めています。隠蔽している実装が本当に妥当なものであるかどうかの検証はしていません。一応ExtJS 5.0のソースを軽く読んでみて、大体書籍にあるようなノウハウが組み込まれている「っぽい感じ」はしてますが、徹底した厳密な調査・検証・テストは実施できていません。そのため、実際に使ってみると「このケースだとExtJSの実装で不都合が出てしまうな~」というのも出てくるかもしれませんが、ご了承ください。
環境:
公式APIドキュメント:
目次:
本記事で紹介しているサンプルコードを動かしたい場合の準備について説明します。
まず、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.5 forループ」では配列をループさせるときのやり方について、「3.6 for-inループ」についてはオブジェクトのプロパティをiterateする方法が紹介されています。
hasOwnProperty()を使ったり、配列を間違ってfor-inループで使わないなどのノウハウが解説されていますが、ExtJSの提供するいくつかのユーティリティクラスを使うことで、そうしたプラクティスの詳細を意識しなくて済むようになります。
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()は内部で引数の型を見て、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 行目)
ExtJSで用意されているいくつかのユーティリティクラス、ユーティリティメソッドに関連するトピックがありますので、紹介します。
書籍の方ではプリミティブ値との比較で、自動変換がかかる場合など細かいノウハウが紹介されています。
ExtJSではプリミティブ値の比較については特にサポート用のクラスやメソッドはありません。ただし、オブジェクトインスタンスの比較については 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 行目)
書籍では setInterval(), setTimeout() での eval() の使用例が紹介されています。
ここではeval()それ自体のトピックではなく、ExtJSで setInterval(), setTimeout() を利用するためのお作法について紹介します。
指定したミリ秒後に処理を実行するには、通常の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 行目)
setInterval() については、特にラッパーメソッドなど無いようです。
以下のフォーラムでも、setInterval()をそのまま使うよう、Senchaの人から回答が来ています。
プリミティブラッパー型の話題とはあまり関係ありませんが、Extでは独自にExt.StringやExt.Numberといったユーティリティクラスを用意しています。
その中で便利そうなメソッドを幾つか紹介します。
ユーザ入力や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 行目)
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 行目) <b>Hello, "jon" & 'bob'.</b> str-utils.js (5 行目) <b>Hello, "jon" & 'bob'.</b> str-utils.js (7 行目) --/--/-- str-utils.js (9 行目)
デフォルトのイベントハンドラなどで、空っぽの関数を指定しておきたい場合があります。
Ext.emptyFnを指定しておけば、 "function(){}" をタイピングする手間が減ります。
この章でのトピックについては、特にExtJSでの開発で気にする必要はなさそうに思いました。
もともとExtJSでの開発では、ExtJSのクラスタシステム上でUIがJavaScriptで構築できるようになっており、HTMLのDOM要素やCSS設定について、Ext.dom.Elementなどで疎結合になるよう調整されています。
DOM要素の構築についてもExt.DomHelperクラスからJavaScriptのスキーム上でDOM要素を構築できるようになっています。
必要であれば、Ext.XTemplateを使ったテンプレート処理も可能です。
この章でのトピックについては、特にExtJSでの開発で気にする必要は無いと思います。
ExtJSのクラスシステムや、MVCアプリケーションのアーキテクチャでは名前空間を分離してクラスを構築する仕組みが組み込まれているため、開発者としては特に意識しない限りはグローバルスコープを操作することは無いと思います。
この章でのトピックについては、ExtJS側でも気にしておく必要はありそうです。
ExtJSでは、イベント設定こそ、各種ラッパークラス等でブラウザ間の細かい差異を気にせずに設定できるようになっていますが、イベントハンドラの引数は意識しておく必要があります。
そのため、特にイベントハンドラ設定が煩雑になりがちなControllerクラスやカスタムコンポーネントでこそ、引数で渡されるイベントオブジェクトを引き回さず、必要な情報だけを実際の処理に渡すような実装を心がけておいたほうが良さそうです。
ExtJSでは、nullやundefined周りや型情報を扱うときの細かい実装調整を隠蔽してくれる、便利なユーティリティメソッドを用意してくれていますので、いくつか紹介します。
(ドキュメントを確認したり、ソースを見ればすぐ分かるようなメソッドばかりですので、サンプルコードは割愛します。)
空かどうか、定義済みかどうか。
型情報を取得したい。
型を判定したい。
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); }
※ダンプログのコピペが大変なので、コンソールログ出力は省略します。
ExtJSではブラウザやOS判定用のユーティリティクラスやメソッドが用意されています。ExtJS 4.2 と ExtJS 5.0 で構成が変わっていましたので、簡単にクラス名とメソッド名だけそれぞれ紹介します。
ExtJS 5.0:
ExtJS 4.2:
ExtJSの場合は、Sencha Cmdと組み合わせることでJavaScriptのminifyやSass/Compassを使ったスタイルシート処理を統合してくれています。
ただ、ドキュメンテーションについてはExtJSの公式ドキュメントでも明記されてる箇所が見当たりませんでしたので、それについてだけメモしておきます。
ExtJSにおけるテストとその自動化については、重たい内容であり、自分自身勉強不足な分野でもあるため本記事では取り上げません。
ExtJSのクラスシステムでは、configurationやmixin, staticなどExtJS独自の概念が使われているため、ドキュメンテーションツールについてもそれらに対応したものが必要となります。
ExtJSでは、JSDuckというドキュメントジェネレータを使っています。
"@"によるアノテーションの種類が豊富なので、自在に使えるようになるまでは時間がかかるかもしれません。自分も、まだ満足に使いこなせてない状況です。
あとがき
ExtJSは年月を経て進化を続けている、割りと大きめなJavaScriptフレームワークです。そのため、ソースを覗いてみても即座に理解できるような単純な構成にはなっていません。
公式APIドキュメントやサンプルコードをコピペしてもある程度のものは作れますが、やはり実際の開発現場では、ドキュメントを読んだりサンプルを見ただけではすぐに実現方法が分からないような機能要求も出てきます。
そうした場合に、やはりソースを読んで理解を深めていかざるを得ません。
幸いなことに、公式APIドキュメントではクラスやメソッド名の近くに表示されている"view source"をクリックすれば該当するソースコードを閲覧できます。
本記事が、ExtJSが提供している便利なユーティリティクラス・メソッドを紹介するだけでなく、ソースを読んでより深くExtJSを理解するきっかけになれば幸いです。
冒頭にも記載しましたが、「メンテナブルJavaScript」で推奨されている方式がExtJSで実装されているのかどうか、そこまでは今回は検証していません。
ExtJSのユーザそれぞれが、本記事をヒントにしてそれぞれでExtJSの実装を調べて、改善提案などをSenchaに寄せていただければ、本記事の執筆者の喜びとしてこれに勝るものはありません。