CodeSnippetと共にするJavaScriptプログラミング

Webサービスにおいて、JavaScriptの依存度は増加傾向にあります。
GitHubに登録されたプロジェクト(2018年度調査資料)を見ても、JavaScriptの割合が最も高いですね。皆さんもこのような経験はありませんか?

1.他の開発者がすでに作ってあるコードがあったが、知らずに重複して作業していた。
 trimなどのユーティリティメソッドでは特に発生しやすい。
2.同じ用途のフレームワークを複数、またはバージョンごとに追加していた。(jQuery, prototype…)
3.新規に追加しようとしたコードが既存コードに影響して問題が発生した。
4.メンテナンス時、すでに開発されているHTMLページのJavaScriptを、どこから手をつければよいか分からない。
(進入ページと同時に実行されるコード把握が難しい)

もちろん、JavaScriptに限った問題ではありませんが、特にJavaScriptで顕著に現れる問題です。

現在は、JavaScriptも管理しなければならない時代です。

ポイントは、外部に露出されているJavaScriptコードをきちんと整理することにあります。windowに変数や関数を無造作に追加することなく、どのコードがどこに入るべきか明確に整理して、無駄な重複を避けましょう。他のコードとこじれることがないように、出入口は1つだけ作成します。

まず、ライブラリを使わずにコードを整理する方法を調べてみよう。整理されたコードがもたらすメンテナンスの利便性は、皆さんも経験されていることでしょう(少なくとも上記のような問題は予防できます)。では、CodeSnippetのユーティリティメソッドdefineNamespacedefineModuleを使って簡単に整理してみましょう。

従来の方式と問題点

グローバルを汚染させる問題として、実行関数を使用しなければならないということは、誰もが知っていることでしょう。

<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.myNotene.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>

defineNamespacedefineModuleは、ページの読み込み時点で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のdefineNamespacedefineModuledefineClassを使うと、サービスのJavaScriptコードを簡単に計画的に構造化することができます。

  • defineNamespace:例外処理なしでwindowのオブジェクト形態のネームスペースを簡単に作成できる
  • defineModule:ページの読み込み時点で、initializeメソッドを実行する以外は、defineNamespaceと同じ
  • defineClass:クラスシミュレーションのユーティリティメソッド

サービス開発初期にNamespaceリストとその用途をまとめて共有する場合、すでに存在するロジックを重複して追加したり、同様の機能をする他のベンダーのフレームワークを重複して追加したりする問題を防止できます。

JavaScriptの曖昧なタイプチェックを回避できるユーティリティメソッドから、使い勝手が悪かったwindow.openを使ったポップアップを使いやすく管理できるメソッドまで、CodeSnippetは様々な便利な機能があります。機能リストは、CodeSnippet Github Repositoryからご確認いただけます。

最後に、スクリプト全体の容量は23KB、GZIP圧縮後は約6.94KBと、負担のないサイズです。また、必要な機能のファイルのみ個別に使用することもできます。使用中に発生する問題は、GitHubにレポートすると、大きな問題でない限りすぐにフィードバックを得ることができるでしょう。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop