NHN Cloud Meetup 編集部
CodeSnippetと共にするJavaScriptプログラミング
2016.04.25
545

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にレポートすると、大きな問題でない限りすぐにフィードバックを得ることができるでしょう。