Ext JSの開発でやってはいけない10のこと
こんにちは、ゼノフィnakamuraです。
Guest Blog Post
CNX では、ほとんどのExt JSで行う開発作業はゼロから新しいアプリケーションを生成する事ですが、時々我々のお客様からパフォーマンス問題、バグ、構造問題で既存の内部作業を取り扱う依頼がきます。 この「清掃人」の作業を長い間を関わって来たので、検査しているアプリケーションによく表れるいけないコーディング習慣に気づいてきました。 この10年間を渡って行って来た作業をレビューして、Ext JSのアプリケーション開発でやってはいけない10のことのリストを作成しました。
1. コンポーネント構造の異常や余計な入れ子
開発者がよくする間違いは意味なくコンポーネントをネストすることです。 これを行うとパフォーマンスも傷つきますし、枠が二重になったり意外なレイアウト挙動をしめすなど、アプリケーションにおかしなことが発生する場合もあります。次の1Aの例では、一つのグリッドが含まれているパネルがあります。 この場合では、そのパネルは不必要です。1Bの例で示すように、その余分なパネルを省く事ができます。パネルからフォーム、ツリー、タブパネル、グリッドが派生していることを覚えておいてください、そしてこのコンポーネントを利用する時には不必要なネストをしないように注意するべきです。
1 2 3 4 5 6 7 8 9 10 | items: [{ xtype : 'panel', title: ‘My Cool Grid’, layout: ‘fit’, items : [{ xtype : 'grid', store : 'MyStore', columns : [{...}] }] }] |
サンプル 1A 悪い見本: ‘panel’は必要ない
1 2 3 4 5 6 7 | layout: ‘fit’, items: [{ xtype : 'grid', title: ‘My Cool Grid’, store : 'MyStore', columns : [{...}] }] |
サンプル 1B 良い見本: グリッドは既にパネルなので、グリッドではパネルプロパティを直接利用できます。
2. 利用されてないコンポーネントの浄化不足の為に行うメモリリーク。
大勢の開発者はアプリケーションを使うにつれて、動作が段々遅くなっていく事を不思議に思います。 その大きな理由は、ユーザーがアプリケーションをナビゲートしたときに、利用されてないコンポーネントを処理しないことです。 以下の見本2Aでは、ユーザーがグリッド列を右クリックする度に、新しいコンテキストメニューが生成されます。もしユーザーがこのメニューを開いたまま、その列を何百回も右クリックすると、永遠に破棄されない何百個のコンテキストメニューが生成されます。 開発者とユーザーにとっては、最後に生成されたコンテキストメニューがページで表示されますから、アプリケーションの見た目は正確に見えます。 他のメニューは隠れています。以前のメニューが処理されず、新しいメニューが生成されていくと、アプリケーションの利用メモリが増加していきます。 最終的にこれはより遅くなるか、ブラウザがクラッシュするということになってしまいます。
見本2Bの方が良いです。 その理由はグリッドが初期化された時にコンテキストメニューが生成されるので、ユーザーが列を右クリックする度に、それが単純に再利用されます。 しかし、もしそのグリッドが破棄された場合、もう必要ではないのにコンテキストメニューは存在します。 最高のシナリオは見本2Cで、コンテキストメニューはグリッドが破棄された時に同時に破棄されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', columns : [{...}], store: ‘MyStore’, initComponent : function(){ this.callParent(arguments); this.on({ scope : this, itemcontextmenu : this.onItemContextMenu }); }, onItemContextMenu : function(view,rec,item,index,event){ event.stopEvent(); Ext.create('Ext.menu.Menu',{ items : [{ text : 'Do Something' }] }).showAt(event.getXY()); } }); |
サンプル 2A 悪い見本: 右クリックするたびにメニューが生成され、破棄される事はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', store : 'MyStore', columns : [{...}], initComponent : function(){ this.menu = this.buildMenu(); this.callParent(arguments); this.on({ scope : this, itemcontextmenu : this.onItemContextMenu }); }, buildMenu : function(){ return Ext.create('Ext.menu.Menu',{ items : [{ text : 'Do Something' }] }); }, onItemContextMenu : function(view,rec,item,index,event){ event.stopEvent(); this.menu.showAt(event.getXY()); } }); |
サンプル 2B より良い: グリッドが生成された時にメニューが生成されますので、毎回再利用されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', store : 'MyStore', columns : [{...}], initComponent : function(){ this.menu = this.buildMenu(); this.callParent(arguments); this.on({ scope : this, itemcontextmenu : this.onItemContextMenu }); }, buildMenu : function(){ return Ext.create('Ext.menu.Menu',{ items : [{ text : 'Do Something' }] }); }, onDestroy : function(){ this.menu.destroy(); this.callParent(arguments); }, onItemContextMenu : function(view,rec,item,index,event){ event.stopEvent(); this.menu.showAt(event.getXY()); } }); |
サンプル 2C 最高: グリッドが破棄されると、コンテキストメニューも破棄されます。
3. モンスターコントローラー
何千行ものコードがあるびっくりする程巨大なコントローラーが一つあるアプリケーションを何度も目撃しました。 我々はアプリケーションの機能でコントローラーを分けて行くのが好みです。 例えば、注文処理のアプリケーションはラインアイテム、出荷り、お客探し、などをそれぞれ優先のコントローラーがあるかもしれないです。こうすると、もっと単純にコードのナビゲーションやメンテナンスができます。
何人かの開発者はビューごとにコントローラーを分けるのが好みです。 例えば、もしアプリケーションにグリッドとフォームがあるとしたら、グリッドを管理するコントローラーもあれば、フォームを管理するコントローラーもあります。 首尾一貫であれば、コントローラーロジックを分解する一つの「正しい」方法はありません。ただコントローラーは他のコントローラーと通信できることは忘れないで下さい。3Aの見本で他のコントローラーから参照を貰い、そのメソッドを一つ呼び出す方法を見ることができます。
1 | this.getController('SomeOtherController').runSomeFunction(myParm); |
サンプル 3A: 他のコントローラーへの参照を取得して、メソッドを呼び出す。
かわりにどのコントローラーでもリスンできるアプリケーション上のイベントを発火できます。サンプル3Bと3Cでは、一つのコントローラーがアプリケーション上のイベントを発火して、他のコントローラーがリスンする様が見えます。
1 | MyApp.getApplication().fireEvent('myevent'); |
サンプル 3B: アプリケーションレベルのイベントを発火する
1 2 3 | MyApp.getApplication().on({ myevent : doSomething }); |
サンプル 3C: 他のコントローラーがアプリケーションレベルのイベントをリスンします。
注:Ext JS 4.2からは、複数のコントローラーを利用するのがより簡単になりました — 他のコントローラーが直接リスンできるイベントを発火できます。
4. 貧弱なソースコードのフォルダー構造
これはパフォーマンスや操作には影響しませんが、アプリケーションの構造を追いかけにくい状況になります。アプリケーションが発展してゆく際に、もしソースコードが整理されていたら、機能を追加する事やソースコードを探し出すことがより簡単になります。 見本4Aで示しているように、大勢の開発者が(大きいアプリケーションの場合でも)全てのビューを一つのフォルダに入れる事をよく目撃します。見本4Bで表示されているように、論理的な機能でビューを整理するする事をおすすめします。
サンプル 4A 悪い見本: 全てのビューが一つのレベルにあります。
サンプル 4B 良い例: ビューは論理的な機能で整理されています。
5. グローバル変数の利用
グローバル変数が悪いことはよく知られていますが、最近レビューしたアプリケーション内でもまだ利用されていました。 グローバル変数を利用するアプリケーションには、名前が競合したりデバッグが難しくなるなどの重大な問題が起こる可能性があります。 グローバル変数を利用するかわりに、クラス内に「プロパティ」をもって、そのプロパティをゲッターとセッターで参照するようにします。
例えば、あなたのアプリケーションが最後に選択された顧客を覚えておく必要があるとしましょう。サンプル5Aで示すようにアプリケーション内に変数を定義したくなるかもしれません。それは簡単ですがその値はアプリケーションのどの部分からでも勝手に使うことができてしまいます。
1 | myLastCustomer = 123456; |
サンプル 5A 悪い例: 最後の顧客番号を保管する為に生成されたグローバル変数
その代わりに「広域で」で利用されるプロパティを持つクラスを生成するのが、より良い作業スタイルです。 この場合では、Runtime.jsというファイルを生成し、実行時のプロパティを保持させ、アプリケーションがそれを利用します。 サンプル5Bは、ソースコード構造内でのRuntime.jsの場所を示しています。
サンプル 5B: Runtime.jsファイルのロケーション。
サンプル5CはRuntime.jsの内容を示しています。 サンプル5Dはそれをapp.jsで “require” する方法を示します。 そうすると、5Eと5Fで示すように、アプリケーションのどの部分からでもプロパティを “set” と “get” できます。
1 2 3 4 5 6 7 8 9 | Ext.define(‘MyApp.config.Runtime’,{ singleton : true, config : { myLastCustomer : 0 // initialize to 0 }, constructor : function(config){ this.initConfig(config); } }); |
サンプル 5C: アプリケーションのグローバルプロパティを保持するサンプルRuntime.jsファイル
1 2 3 4 5 | Ext.application({ name : ‘MyApp’, requires : [‘MyApp.config.Runtime’], ... }); |
サンプル 5D: app.jsファイルでRuntimeクラスをrequireする
1 | MyApp.config.setMyLastCustomer(12345); |
サンプル 5E: 最後の顧客をセットする方法。
1 | MyApp.config.getMyLastCustomer(); |
サンプル 5F: 最後の顧客を取得する方法。
6. “id” を使うこと
コンポーネントにidを使うことは推奨しません。その理由は各idはユニークである必要があるからです。 単に間違えて同じidを一回以上使ってしまう場合があり、そうするとDOMのidをダブることになります(名前の競合)。 その代わりに、idの生成をフレームワークに任せてあげて下さい。 Ext JS ComponentQueryがあるので、もうExt JSのコンポーネントにidを指定する必要はありません。 サンプル6Aはあるアプリケーションで二つの異なる保存ボタンが生成されているところのコードセグメントを表示しています。 両方ともidが “savebutton” のidで指定されていて名前の競合がおこりました。 もちろん以下のコードでは明らかになっていますが、大規模のアプリケーション内で名前の競合を見つけるのは難しい時もあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // here we define the first save button xtype : 'toolbar', items : [{ text : ‘Save Picture’, id : 'savebutton' }] // somewhere else in the code we have another component with an id of ‘savebutton’ xtype : 'toolbar', items : [{ text : ‘Save Order’, id : 'savebutton' }] |
サンプル 6A 悪い見本: コンポーネントに重複している “id” を与えると名前の競合が発生します。
代わりに、手動で各コンポーネントを識別したい場合は、サンプル6Bで示すように、単に “id” を “itemId” に切り替える事ができます。 これで名前の競合が解決できますし、itemIdによって、コンポーネントに参照を与える事ができます。itemId経由でコンポーネントの参照を取得する方法は様々です。いくつかのメソッドはサンプル6Cで表示されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | xtype : 'toolbar', itemId : ‘picturetoolbar’, items : [{ text : 'Save Picture', itemId : 'savebutton' }] // somewhere else in the code we have another component with an itemId of ‘savebutton’ xtype : 'toolbar', itemId: ‘ordertoolbar’, items : [{ text : ‘Save Order’, itemId: ‘savebutton’ }] |
サンプル 6B 良い例: “itemId” を使ってコンポーネントを生成する
1 2 3 4 5 6 | var pictureSaveButton = Ext.ComponentQuery.query('#picturetoolbar > #savebutton')[0]; var orderSaveButton = Ext.ComponentQuery.query('#ordertoolbar > #savebutton')[0]; // assuming we have a reference to the “picturetoolbar” as picToolbar picToolbar.down(‘#savebutton’); |
サンプル 6C 良い例: “itemId” でコンポーネントを参照する
7. あてにならないコンポーネントの参照
時々、コンポーネントの位置をもとに参照を取得しているコードを見かけます。これは避けるべきです。その理由はもし項目が追加されたり、削除されたり、別のコンポーネント内に入れ子されたりすると、そのコードが簡単に破綻します。サンプル7Aにはよく存在する二つのケースを示しています。
1 2 3 | var mySaveButton = myToolbar.items.getAt(2); var myWindow = myToolbar.ownerCt; |
サンプル 7A 悪い例: コンポーネント位置に基づいてコンポーネント参照を取得するのは避けて下さい。
代わりに、ComponentQueryまたはコンポーネントの “up” と “down” メソッドを利用して、サンプル7Bで示されているように参照を取得して下さい。このテクニックに従うと、あとでコンポーネントの構造、または順番が変更されても、コードが破綻する可能性が減少します。
1 2 3 | var mySaveButton = myToolbar.down(‘#savebutton’); // searching against itemId var myWindow = myToolbar.up(‘window’); |
サンプル 7B 良い例: 相対参照を取得するにはComponentQueryを利用する。
8. 大文字・小文字の名前付け規則に従わない失敗
コンポーネント、プロパティ、xtypeなどを名前付ける時に、Senchaはいくつかの大文字・小文字規則に従います。混乱しないで、コードもきれいにしておく為に、同じ基準に従うべきです。サンプル8Aはいくつかの正しく名いシナリオを表示しています。サンプル8Bでは同じシナリオを正しい大文字・小文字名前付け規則に従った場合を表示しています。
1 2 3 4 5 6 7 8 9 10 11 12 | Ext.define(‘MyApp.view.customerlist’,{ // should be capitalized and then camelCase extend : ‘Ext.grid.Panel’, alias : ‘widget.Customerlist’, // should be lowercase MyCustomConfig : ‘xyz’, // should be camelCase initComponent : function(){ Ext.apply(this,{ store : ‘Customers’, …. }); this.callParent(arguments); } }); |
サンプル 8A 悪い例: 太文字は正しくない大文字・小文字で名前付けられています。
1 2 3 4 5 6 7 8 9 10 11 12 | Ext.define(‘MyApp.view.CustomerList’,{ extend : ‘Ext.grid.Panel’, alias : ‘widget.customerlist’, myCustomConfig : ‘xyz’, initComponent : function(){ Ext.apply(this,{ store : ‘Customers’, …. }); this.callParent(arguments); } }); |
サンプル 8B 良い例: 太文字は正しい大文字・小文字の規則に従っています
加えて、もしカスタムイベントを発火している場合、イベントの名前は小文字になっているべきです。もちろん、この規則に従わなくても全ては動作しますが、基準に従わずにより混乱したコードを書く理由はありません。
9. 親コンポーネントのレイアウトにコンポーネントを押し込める
サンプル9Aでは、パネルはいつも “region:center” プロパティをもつ事になりますので、例えばコンポーネントを再利用して “west” の部分に設置したら動作しなくなります。
1 2 3 4 5 6 7 8 9 10 11 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', initComponent : function(){ Ext.apply(this,{ store : ‘MyStore’, region : 'center', ...... }); this.callParent(arguments); } }); |
サンプル 9A 悪い例: “center” リージョンはここで指定しない方が良い。
その代わりに、サンプル9Bで示されているように、コンポーネントを生成する時にlayoutコンフィグを指定して下さい。こうするとlayoutコンフィグに制約されず、どこでもコンポーネントを再利用できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', initComponent : function(){ Ext.apply(this,{ store : ‘MyStore’, ...... }); } }); // specify the region when the component is created... Ext.create('MyApp.view.MyGrid',{ region : 'center' }); |
サンプル 9B 良い例: コンポーネントを生成する時にリージョンを指定する
9Cのサンプルで表示されているように、コンポーネントのデフォルトリージョンを提供して、必要であればあとでオーバーライドするようにもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Ext.define('MyApp.view.MyGrid',{ extend : 'Ext.grid.Panel', region : 'center', // default region initComponent : function(){ Ext.apply(this,{ store : ‘MyStore’, ...... }); } }); Ext.create(‘MyApp.view.MyGrid’,{ region : ‘north’, // overridden region height : 400 }); |
サンプル 9C これも良い例: デフォルトのリージョンを指定して、必要であればオーバーライドする
10. 必要以上にコードを複雑にする
よく必要以上にコードが複雑になっていることを目撃します。通常これは各コンポーネントで利用できるメソッドをよくご存知でない結果です。最もよく見るケースは、一つひとつのデータレコードから各フォームフィールドをロードするコードです。サンプル10Aではこの例を示しています。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // suppose the following fields exist within a form items : [{ fieldLabel : ‘User’, itemId : ‘username’ },{ fieldLabel : ‘Email’, itemId : ‘email’ },{ fieldLabel : ‘Home Address’, itemId : ‘address’ }]; // you could load the values from a record into each form field individually myForm.down(‘#username’).setValue(record.get(‘UserName’)); myForm.down(‘#email’).setValue(record.get(‘Email’)); myForm.down(‘#address’).setValue(record.get(‘Address’)); |
サンプル 10A 悪い例:一つひとつのレコードからフォームフィールドをロードする
一つひとつ各値をロードするより、loadRecordメソッドを使って、1行のコードで対応するフォームフィールドにレコードから全てのフィールドをロードして下さい。大事なポイントはサンプル10Bで表示されているようにフォームフィールドの「name」プロパティがレコードのフィールド名と揃っていることを確認することです。。
1 2 3 4 5 6 7 8 9 10 11 12 | items : [{ fieldLabel : ‘User’, name : ‘UserName’ },{ fieldLabel : ‘Email’, name : ‘Email’ },{ fieldLabel : ‘Home Address’, name : ‘Address’ }]; myForm.loadRecord(record); |
サンプル 10B 良い例: loadRecordを利用して、1行のコードで全てのフォームフィールドをロードする。
これはコードが必要以上に複雑になる可能性の例です。 ポイントは全てのコンポーネントのメソッドを見直して、シンプルで適当なテクニックを利用していることを確認することです。
CNX Corporation はSencha Certified Select Partnerです。Sencha Partner NetworkはSencha Professional Servicesチームの価値ある拡張です。
CNXは1996年からカスタムビジネスアプリケーション開発の最先端にいます。CNXは2008年にExt JSのブラウザベーズのユーザーインタフェース開発を標準化して、2010年にモバイル開発のスタンダードとしてSencha Touchを追加しました。CNXは世界中の教育、金融、食物、法律、物流、生産、出版、販売、などの様々な業界のお客様にワールドクラスのウェブアプリケーションを作成しました。CNXの開発チームはシカゴ事務所を拠点として、どんな規模のプロジェクトでも扱えます。CNXは独立で作業するか、お客様のチームと協力して、良いコストパフォーマンスの方法でプロジェクトを実現できます。 詳しくは http://www.cnxcorp.com ヘ。