NHN Cloud Meetup 編集部
CodeSnippetと共にするJavaScriptプログラミング
2016.04.25
502
Webサービスにおいて、JavaScriptの依存度は増加傾向にあります。
GitHubに登録されたプロジェクト(2018年度調査資料)を見ても、JavaScriptの割合が最も高いですね。皆さんもこのような経験はありませんか?
trimなどのユーティリティメソッドでは特に発生しやすい。
2.同じ用途のフレームワークを複数、またはバージョンごとに追加していた。(jQuery, prototype…)
3.新規に追加しようとしたコードが既存コードに影響して問題が発生した。
4.メンテナンス時、すでに開発されているHTMLページのJavaScriptを、どこから手をつければよいか分からない。
(進入ページと同時に実行されるコード把握が難しい)
もちろん、JavaScriptに限った問題ではありませんが、特にJavaScriptで顕著に現れる問題です。
現在は、JavaScriptも管理しなければならない時代です。
ポイントは、外部に露出されているJavaScriptコードをきちんと整理することにあります。windowに変数や関数を無造作に追加することなく、どのコードがどこに入るべきか明確に整理して、無駄な重複を避けましょう。他のコードとこじれることがないように、出入口は1つだけ作成します。
まず、ライブラリを使わずにコードを整理する方法を調べてみよう。整理されたコードがもたらすメンテナンスの利便性は、皆さんも経験されていることでしょう(少なくとも上記のような問題は予防できます)。では、CodeSnippetのユーティリティメソッドdefineNamespace、defineModuleを使って簡単に整理してみましょう。
従来の方式と問題点
グローバルを汚染させる問題として、実行関数を使用しなければならないということは、誰もが知っていることでしょう。
<body> <script> function login() {/* ... */} </script> </body>
上のコードは、login()で実行することもありますが、window.login()
でも使用することができ、他のJavaScriptと混ざって問題を発生させる余地があります。(このようなコードを「グローバルを汚染させるコード」と言います。)
そのため、次のように実行関数(IIFE)を使ってグローバル汚染を防ぎます。
<body> <script> (function() { function login() {/* ... */} })(); login(); // ReferenceError </script> </body>
問題は、login関数が無名関数(名前のない関数)内に存在するため、外部からアクセスができないということです。
そのため、windowに外部からアクセスできるように手動で追加する必要があります。windowにプロジェクト名でオブジェクトを作成し、ここに整理することにしましょう。
<body> <script> (function(w) { function login() {/* ... */} w.myProject = { login: login }; })(window); w.myProject.login(); // OK </script> </body>
loginとメソッドがwindow.myProjectと呼ばれるオブジェクトに含まれています。しかし不便な点があります。Namespaceは、複数のdepthを構成しているはずですが、無名関数内で例外処理をする必要があります。例えば、myProject.common
を作成するとき、myProject
オブジェクトがあるかどうか、例外処理が必要です。
<body> <script> (function(w) { function saveData() {/* ... */} // myProjectがあるか? if (w.myProject) { // myProject.userがあるか? if (w.myProject.user) { w.myProject.user.saveData = saveData; } else { w.myProject.user = { saveData: saveData }; } } })(window); w.myProject.user.saveData(); </script> </body>
defineNamespace
CodeSnippetのdefindNamespaceは、windowにJavaScript機能を簡単に体系的に構造化できる機能を提供しています。その名の通り、Namespaceを定義する機能を提供するユーティリティメソッドです。
このメソッドを使うと、前述した不便な問題もなくなり、計画的に機能を追加できます。
<body> <script> var common = tui.util.defineNamespace('ne.myNote.common', { trim: function() {/* ... */} }); tui.util.defineNamespace('ne.myNote', { login: function() {/* ... */} }); ne.myNote.login(); ne.myNode.common.trim('test'); common.trim('test'); // 変数に割り振って使用できる </script> </body>
ne.myNoteとne.myNote.commonでネームスペースを定義しました。さらにne.myNote.commonを最初に宣言しても問題なく動作します。
defineNamspaceを使って、JavaScriptのメソッドを構造的に露出させるだけでも、非常にすっきりとした成果物を作成できます。
さらに進めて、スクリプトがロードされた時点で初期化メソッドを実行したい場合は、defineModuleメソッドを使用することができます。
<body> <script> tui.util.defineModule('ne.myNote.settings', { memberID: '<%= memberID %>', initialize: function() { // ページロードと同時に非同期通信を行うこと(自動実行) $.ajax(/* ... */); } }); ne.myNote.settings.memberID; // ユーザーID </script> </body>
defineNamespaceとdefineModuleは、ページの読み込み時点でinitialize
という名前のメソッドの実行をするかどうかの違いです。defineModule
は、ページ単位のJavaScriptファイルを作成するのに利用できます。initializeは、ページの読み込みと実行スクリプトを実装する形で使用できます。(実務サンプルのチャプターで扱います。)
ここまでで十分ですが、もしクラスのシミュレーションが必要であれば、defineClassを使用できます。defineClassは結果がコンストラクタ関数のため、インスタンス化して使用できます。
<body> <script> var Comment = tui.util.defineClass({ init: function(content) { this.content = content; this.like = 0; }, likeIt: function() { this.like += 1; } }); var comment1 = new Comment('I like it!'); var comment2 = new Comment('I hate it!'); </script> </body>
defineClassは内部的にprototypeパターンを利用してクラスをシミュレートします。したがって、プロパティはインスタンスに、メソッドはprototypeオブジェクトに追加されるので、ブラウザ基盤の限定的なリソースで動作するJavaScriptを効率的に扱うことができます。もちろん継承も可能です。
<body> <script> var PhotoComment = tui.util.defineClass(Comment, { init: function(content) { Comment.call(this, content); this.photoUrl = ''; // CommentクラスにphotoUrlプロパティを追加した } }); var comment1 = new PhotoComment('I like it!'); </script> </body>
実務サンプル
ログインページで使用するモジュールを作ってみよう。emailにtrimを適用する場合を想定します。
// util.js tui.util.defineNamespace('ne.myNote.util', { trim: function(str) { if (str.trim) { return str.trim(); } return str.replace(/^[\s]+|[\s]+$/g, ''); }, getElement: function(selector) { return document.querySelector(selector); } });
util.jsは、プロジェクト全体で使用するユーティリティメソッドを集めたモジュールです。getElementはjQueryの$()のようなものに見做すことができます。
<body> <form> <input type="text" name="email" placeholder="Enter email address" /> <input type="password" name="password" placeholder="Enter password" /> <input type="submit" value="login" /> </form> <script src="./util.js"></script> <!-- ne.myNote.util --> <script> tui.util.defineModule('ne.myNote.page.login', { $email: ne.myNote.util.getElement('input[name=email]'), $form: ne.myNote.util.getElement('form'), initialize: function() { this.$form.addEventListener('submit', this.onSubmit.bind(this)); }, onSubmit: function(e) { this.$email.value = ne.myNote.util.trim(this.$email.value); } }); </script> </body>
各ページで使用するエレメントに対して、モジュールのプロパティで定義した($email, $form)JavaScriptが使用するページ内の要素を一目で見ることができて、管理しやすくなりました。
initializeでページ初期に実行されるスクリプトを実装しました。これでページ全体のファイルに目を通さなくてもロード時のスクリプトを管理できるようになりました。
JavaScriptでドキュメントにバインドするフォーム要素のイベントをonSubmitで設定しました。このようにイベントメソッドのコンベンションを定義すると、ページ内でどのようなイベントを実装するか、容易に見分けることができます。
要約と結論
CodeSnippetのdefineNamespace、defineModule、defineClassを使うと、サービスのJavaScriptコードを簡単に計画的に構造化することができます。
- defineNamespace:例外処理なしでwindowのオブジェクト形態のネームスペースを簡単に作成できる
- defineModule
:ページの読み込み時点で、initializeメソッドを実行する以外は、defineNamespaceと同じ
- defineClass
:クラスシミュレーションのユーティリティメソッド
サービス開発初期にNamespaceリストとその用途をまとめて共有する場合、すでに存在するロジックを重複して追加したり、同様の機能をする他のベンダーのフレームワークを重複して追加したりする問題を防止できます。
JavaScriptの曖昧なタイプチェックを回避できるユーティリティメソッドから、使い勝手が悪かったwindow.openを使ったポップアップを使いやすく管理できるメソッドまで、CodeSnippetは様々な便利な機能があります。機能リストは、CodeSnippet Github Repositoryからご確認いただけます。
最後に、スクリプト全体の容量は23KB、GZIP圧縮後は約6.94KBと、負担のないサイズです。また、必要な機能のファイルのみ個別に使用することもできます。使用中に発生する問題は、GitHubにレポートすると、大きな問題でない限りすぐにフィードバックを得ることができるでしょう。