フレームワークなくとも秩序は成り立つ ~ フロントエンドJavaScript ~

はじめに

いま開発しているWebアプリケーションでフロントエンドのJSを書こうとしているときに、以前の開発でAngularJSを使用していた経験があることもあり、自然にどのフレームワークを採用しようかと色々物色しておりました。

AngularJSの他にもEmberやKnockout, Backbornなど調査してみたんですが、どのフレームワークも「SPA」(シングルページアプリケーション)を作ることを前提に作られているようでした。

自分が開発中のアプリケーションはSPAではないし、AngularJSを採用したプロジェクトの時もバックエンドで採用しているフレームワークとの相性とかで結構苦労したのでどうしようかな〜と考えて、っていうかフロントエンドMV*フレームワークいる?って所から考えなおしてみました。

フロントエンドMV*フレームワークを使いたい理由

もちろんチームやプロダクトの性質によると思いますが、僕が考えた上では

  1. ビジネスロジックとプレゼンテーションを分離したい。
  2. コードに構造を持たせて使い回ししやすく、見通しよくしたい。
  3. フレームワークやライブラリの力で効率化したい。

のような感じかな〜と思いました。

適当にJSでゴリゴリ書いちゃうとどのファイルに何があるかがわからずに見通しが悪くなるし、JSはUIをいじることに使われる特性上デザインの変更に対して対応出来ないといけないので変更ができるだけ簡単に行えるようにしておかないといけない。 さらにあわよくばデータバインディングやフィルタやらの力を借りて効率よく開発したいって感じです。

忘れちゃならないトレードオフ

ただ、フレームワークの採用にはもちろん良いことだけでなく、以下のようなトレードオフも存在します。

  1. 学習コスト
  2. フレームワークへの依存

学習コスト

そのフレームワークを使いこなせるようになるとどんどん開発効率が上がっていきます。

ただそのフレームワークを使いこなすまでにはそのフレームワーク独特の概念やAPIを学習するのにそれなりの時間がかかり、さらにそれを使いこなすためにも時間がかかります。

さらにフレームワークを使うと必ずそのフレームワーク独特の問題などぶつかることも多く、それを解決するのに余計な時間がかかってしまう可能性も考慮する必要があります。

フレームワークへの依存

フレームワークはあらゆるアプリケーションに使えるわけではなく、そのフレームワークが想定しているユースケースがあります。 そのユースケースに沿って使えば効率良く開発が行えますが、そこから外れたことをやろうとすると余計なHackが必要になってしまいます。

そのためフレームワークの選定を間違えると余計なコストがかかったり、わざわざフレームワークに合わせたアプリケーションになってしまうという恐れもあります。

必要ないなら使わない方が良い

今回私が行っているWebアプリケーション開発の状況は以下のような感じでした。

  • このアプリケーション開発以外のプロジェクトもやってる
  • アプリケーションはSPAじゃない
  • インフラ、バックエンド、フロントエンド、ネイティブアプリまで全てを担当するためフロントエンドの専任エンジニアはいない

ざっくり言うと、「やることはたくさんあるからフレームワークの選定ミスで余計なコストを掛けたくない!」っていう状況です。

その状況のなかで考えたときに

  • フレームワークの機能での効率化は今回の場合学習コスト + 相性の悪さで多分チャラになる
  • でも見境なくJS書いてカオスにもしたくない。
  • 自分である程度JSの構造化をすればいいんじゃね?

っていうことになりました。

おれおれのフロントエンドJS設計

フロントエンドJavaScriptで書くコードをざっくり以下の様に分離します。

  • Model: ビジネスロジックの管理
  • Template: HTMLの生成
  • ViewModel: イベントハンドリングやデータフロー

ディレクトリ構造は以下のような感じになります。

├── javascripts
│   ├── models
│   │   └── user.js
│   ├── templates
│   │   └── errors_message_template.js
│   │   └── day_options_template.js
│   └── viewmodels
│       ├── profile_viewmodel.js
│       └── top_viewmodel.js

Model (ビジネスロジック)

MVCフレームワークでもモデルと呼ばれ 、アプリケーション特有のロジックを入れるレイヤーです。 たとえばユーザープロフィールの登録ページを実装する場合、Userというモデルオブジェクトを作り、そこにバリデーションの条件やバックエンドのAPIとの通信などのコードを書きます。

ここにあるコードはデザインとの結びつきがなく、どのページでも使いまわせるようにしておきます。

参考コードとして下記のUserモデルをのせておきます。

function User(name, email, job) {
   var self = this;
   self.name = name:
   self.email = email;
   self.job    = job

   self.nameIsBlank = function(){
      self.name.length === 0;
   }

}

Template(HTMLの生成)

入力フォームをチェックしてエラーメッセージを表示したり、Ajaxやユーザーのアクションに反応して変化するHTMLを生成するコードです。 例えば日付に関する入力フォームで年、月、日のセレクトボックスがある場合、年、月が変更されると日のセレクトボックスのoptionタグはその月にある日の数に合わせて変化しないといけません(8月なら31個、2月なら28個のように)。

それを実現するために年と月の値からoptionタグを必要な数だけ生成するコードを書きます。

ここは同じようなHTMLが必要な箇所があれば上手く使いまわせるように引数によって動的な値を管理します。

以下に年と月を引数に受け取って必要なoptionタグを生成するTemplateのコードを記載します。

/*
 * 引数のyearとmonthからその月の開始日と終了日までのoptionタグを生成する。
 * selectedDayに指定した日付のoptionタグにはselected="selected"の属性を設定する。
 * 例:
 * <option value="1">1</option>
 * <option value="2">2</option>
 * <option value="3" selected="selected">3</option>
 * <option value="4">4</option>
 * <option value="5">5</option>
 * .
 * .
 * <option value="31">31</option>
 *
 */
function DayOptionsTemplate(year, month, selectedDay){
    var self = this;
    var jsMonth = month - 1;
    var monthFirstDate = new Date(year, jsMonth, 1);
    var monthLastDate  = new Date(year, jsMonth + 1, 0);
    self.template = generateTemplate(monthFirstDate.getDate(),
                                     monthLastDate.getDate(),
                                     selectedDay);

    function generateTemplate(firstday, lastDay, selectedDay){

        function a_day_option_template(day, selected){
            var attr = '';
            if (selected === true){
                attr += ' selected="selected"';
            }
            var template = '<option value="' + String(day) + '"' + attr + '>' +
                String(day) +
                '</option>';
            return template;
        }

        var template = '';

        for(var i=firstday;
            i<=lastDay;
            i++) {
            var selected = i === selectedDay ? true : false;
            template += a_day_option_template(i, selected);
        }
        return template;
    }
}

ViewModel(イベントハンドリング・データフロー)

HTMLの要素に対してイベントをつけたり、モデルの変更をキャッチした結果生成したHTMLを実際のDOMに差し込んだりする所です。

ここの部分が一番DOMに結びつきやすく、使い回しも効きにくいため、Webアプリケーションのページ毎に作成するイメージです。

DOMと結びついた面倒な部分をここに押し込めることで他のModelやTemplateの独立性を高めています。

以下にプロフィール登録ページのViewModelのサンプルコードを記載します。

ProfileViewModel = function(){
  var self = this;
   
  $(document).ready(function(){
        
       // イベントハンドリング  
       $('form[name="profile" button.submit]').click(function(){

          // データフロー
          // Formの値をモデルに与える
          var name = $('input[name="user[name]"').val();
          var email = $('input[name="user[email]"').val();
          var job     = $('input[name="user[job]"').val();

          var user = new User(name, email, job);
          if (user.nameIsBlank === true)
             alert('名前を入力して下さい');
             return false
          end          
       });

       // イベントハンドリング
       $('select[name='month']').change(function(){
          // データフロー
          // 選択されている値を元にテンプレートを作成する
          var year    = $('select[name='year']').val();
          var month = $('select[name='month']').val();
          var day     = $('select[name='day']').val();
          var dayOptions = DayOptionsTemplate(year, month, day);

          // テンプレートの差し込み
          if ($("select[name='day']").children().length > 0){
              $("select[name='day']").empty();
          }
          $("select[name='day']").prepend(dayOptions.template);
          
       })
  });

}

このViewModelを使いたいページで実行すれば、必要な処理がすべて用意されるという感じです。

ViewModelの実行はscriptタグで行っても良いですが、dispatcherを作ってURLで必要なVIewModelをセットすると より見通しさらによくなります。

dispatcher("^/profile", function(){
    ProfileViewModel();
});

function dispatcher(path, func){
    dispatcher.path_func = dispatcher.path_func = dispatcher.path_func || []
    if (func) return dispatcher.path_func.push([path, func]);
    for(var i = 0, l = dispatcher.path_func.length; i<l; ++i ){
        var func = dispatcher.path_func[i];
        var match = path.match(func[0]);
        match && func[1](match)
    };
};

dispatcher(location.pathname);

終わりに

とりあえずJSがカオスにならずに秩序をもって構造化でき、ある程度使い回しが可能なコードが作れるような設計を、フレームワークなしで実現できるように取り組んでみました。

いままさに開発中のアプリケーションでは結構気持ちよくこの設計が使えていますが、まだまだ解決したい問題は多々あります。

たとえば分離したJSファイルの依存性を解決したり、Templateが純粋なJSなのでデザイナーが触りづらいなど色々あります。

その解決方法も目処は立っている(依存性解決はWebpackを使ったり、Templateもそれを解決してくれそうなライブラリがある)のですが、いまは自分達のプロジェクトでは回っているので問題が顕在化しそうになってから取り組めば良いかな〜という感じです。

必要なコストが払えるのであればフレームワークを導入するのも良いですが、 自分達のプロジェクトに最適化した設計を行なうことが出来るのであればその方が効率は上がるし、プログラミングをしていてもかなり楽しくなるのでおすすめします^^