エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

React.js, Vue.jsが使えない状況でメンテナンス性の高いJavaScriptを書く3つのポイント

エムスリー エンジニアの岩本です。 この記事は エムスリー Advent Calendar 2018 の23日目の記事です。

React.jsやVue.jsを使えれば、開発のベストプラクティスなどがあるので、メンテナンス性の高いプログラムはずいぶんと書きやすくなったと思います。本当に仮想DOMの功績は大きいですね。

しかし、世の中にはそういったライブラリを使うことができないプロジェクトもあるわけです。古すぎて、一部分だけ最新のソースコードにすることが憚られたり、サイズの問題でライブラリを入れることができなかったり。。。

その場合どのように書けばメンテナンス性の高いプログラムを書くことができるのでしょうか。そこでIE6時代からJavaScriptをもりもりと書いている私なりのベストプラクティスを紹介します。

そもそもなぜメンテナンス性の悪いコードとなってしまうのか

jQueryではセレクタで要素にアクセスできるという素晴らしいAPIで簡単に要素にアクセスできるようになりました。「ちょっとこの要素消してほしい」「この要素に直前に追加したいアイコンがあるんだ」とかアドホックな対応に簡単に対応することができます。

ただ、そのように作ったアプリってちゃんとした設計がされていないので、最初の数個のHTML変更やイベント追加までは良いのだけども、ページ自体に機能が増えてくるとどこのイベントでなにをしているのかがわからなくなってしまう。その結果メンテナンス性の低いコードとなるのです。

さらにクライアントサイドのプログラミングはバックエンドとはかなり性質が異なり複雑になりがちです。

  • HTMLで実現されたUIがある
  • UIが動作起点となるイベント駆動である
  • Ajax, setTimeoutなどの非同期での動作がある

これらのせいで上から順番に実行すれば良いサーバサイドのプログラミングとは違った複雑さが生まれてしまいます。

どのようにアプローチするのか

結局React.jsやVue.jsで行っているように下記の3つの原則に則ってコードを書くことでかなり改善されると考えています。

  • コンポーネント志向で設計する
  • ViewModelを定義する
  • 画面の更新処理は1箇所で行う

コンポーネント志向で設計する

コンポーネント指向で設計しないと、どこからどこまでがスコープなのかわからず、見通しが悪くなります。React.jsやVue.jsでも必ずコンポーネント単位に区切られているので同じように管理しましょう。コンポーネントはClassを使って表現します。コンストラクタにroot要素を受け取り、コンポーネントはそのroot要素の配下しか変更してはいけないルールとします。

React.jsやVue.jsは言わずもがなコンポーネント指向ですね。

ViewModelを定義する

次に、Viewの状態を表すモデル(ViewModel)を定義します。ViewModelがあるとコンポーネントの状態が一目瞭然となり、Viewの更新と処理を切り分けることも可能となります。

React.jsだとpropsやstate、Vue.jsだとpropsやdataがこれに当たります。

画面の更新処理は1箇所で行う

これが一番大事ではないかと考えています。画面の更新を1箇所で行うことでViewの状態とViewModelの状態が一致させることができます。React.jsもVue.jsも同じように画面の更新は一箇所で行っています。同様に1箇所で画面の更新処理を行いましょう。

React.jsもVue.jsもrenderメソッドがこれに当たります。

また同じようにイベントの割当、要素の構築もメソッドを分けて定義しておきましょう。

その結果下記のような雛形となります。

// これがコンストラクタ
// IEをサポートしないならclassが使えます。
function RegisterForm(rootEl, options) {
  // rootElはthisに紐つけておきます。
  this.rootEl = rootEl;
  // 必要であればデフォルトオプションをここで定義しておきます。
  this.options = Object.assign({someOption1: true}, options);

  this.initViewModel();
  this.buildElements();
  this.attachEvents();
  this.updateView();
}
// Viewモデルを初期化します。
RegisterForm.prototype.initViewModel() {}
// このコンポーネントで使用する要素を構築したり、子コンポーネントを作ったりします。
RegisterForm.prototype.buildElements() {}
// 必要なイベントを割り当てます。
RegisterForm.prototype.attachEvents() {}
// Viewの更新を行います。
RegisterForm.prototype.updateView() {}

実際にダメなコードをリファクタしてみる

つぎのソースコードを見てください。jQueryを使った比較的ありがちなプログラムです。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>sample</title>
  <style>
    #register-form label.field-label {
      display: inline-block;
      width: 150px;
      text-align: right;
      padding-right: 10px
    }
    .checkbox-block {
      vertical-align: top;
      display: inline-block;
    }
    .block {
      display: block;
    }
    .block.child {
      padding-left: 20px;
    }
  </style>
</head>
<body>
<form id="register-form">
  <div>
    <label class="field-label">メルマガ登録</label>
    <label>
      <input name="mail-magazine" type="radio" value="yes"/>
      希望する
    </label>
    <label>
      <input name="mail-magazine" type="radio" value="no"/>
      希望しない
    </label>
  </div>
  <div id="magazines-container" style="display: none;">
    <label class="field-label">登録するメルマガ</label>
    <div class="checkbox-block">
      <label class="block">
        <input name="magazines" type="checkbox" value="1"/>
        メルマガ1
      </label>
      <label class="block">
        <input name="magazines" type="checkbox" value="2"/>
        メルマガ2
      </label>
      <label class="block">
        <input name="magazines" type="checkbox" value="3"/>
        メルマガ3
      </label>
      <label
        id="magazine3_1-container"
        class="block child"
        style="display: none;"
      >
        <input name="magazines" type="checkbox" value="3-1"/>
        メルマガ3-1(メルマガ3を選択したときのみ表示)
      </label>
    </div>
  </div>
</form>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script>
$('#register-form [name="mail-magazine"]').change(function(evt) {
  var value = evt.target.value;
  if (value === 'yes') {
    $('#magazines-container').show();
  } else {
    $('#magazines-container').hide();
  }
});

$('#register-form [name="magazines"][value="3"]').change(function(evt) {
  if (evt.target.checked) {
    $('#magazine3_1-container').show();
  } else {
    $('#magazine3_1-container').hide();
  }
});
</script>
</body>
</html>

いけてない部分がたくさんありますね。今は動的に変更される部分が少ないので見通しがよく見えますが、あと少し動的な処理が増えるとすぐに、触りたくないコードの出来上がりです。とりあえずHTMLの部分は置いておいて、JavaScriptの部分のみを見てみましょう。

// コンポーネント指向になっていないので、
// どこからどこまでをスコープとして考えればよいかわからない

// 要素の表示状態をモデルとして持っていないため、何が表示・非表示されるかが簡単にわからない
$('#register-form [name="mail-magazine"]').change(function(evt) {
  var value = evt.target.value;
  if (value === 'yes') {
    $('#magazines-container').show();
  } else {
    $('#magazines-container').hide();
  }
});

// メルマガの表示状態と、メルマガ3-1の表示状態が独立したメソッド内で更新されてしまいます
// Viewの状態としてどのようなものがあるのかが簡単にはわかりません。
$('#register-form [name="magazines"][value="3"]').change(function(evt) {
  if (evt.target.checked) {
    $('#magazine3_1-container').show();
  } else {
    $('#magazine3_1-container').hide();
  }
});

これを次のようにします。

// クラスでコンポーネントを定義する
// 最近だとclassが使えますね。ターゲットブラウザに合わせてください。
function RegisterForm(rootEl) {
  this.$rootEl = $(rootEl);

  this.initViewModel();
  this.buildElements();
  this.registerEvents();
}
RegisterForm.prototype.initViewModel = function() {
  // 使用する状態はプロパティとして定義しておく
  this.showMagazinesContainer = false;
  this.showMagazine3_1 = false;
}
RegisterForm.prototype.buildElements = function() {
  // 使用する要素は予め変数にセットしておく
  this.$magazinesContainer = this.$rootEl.find('#magazines-container');
  this.$magazine3_1Container = this.$rootEL.find('#magazine3_1-container');

  // もう少し大きなアプリだとこのメソッド内で要素を動的に作成する
}

// イベントを割り当てる
RegisterForm.prototype.registerEvents = function() {
  // thisを退避
  var _this = this;
  this.$rootEl.on('change', '[name="mail-magazine"]', function(evt) {
    _this.showMagazinesContainer = evt.target.value === 'yes';
    // viewの変更は必ずupdateViewで行う
    _this.updateView();
  });

  this.$rootEl.on('change', '[name="magazines"][value="3"]', function(evt) {
    _this.showMagazine3_1 = evt.target.checked;
    // viewの変更は必ずupdateViewで行う
    _this.updateView();
  });
}

// ビューの更新処理は必ずこのメソッドで行う
RegisterForm.prototype.updateView = function() {
  // magazinesContainerの更新
  if (this.showMagazinesContainer) this.$magazinesContainer.show();
  else this.$magazinesContainer.hide();

  // magazine3_1の更新
  if (this.showMagazine3_1) this.$magazine3_1Container.show();
  else this.$magazine3_1Container.hide();
}

new RegisterForm(document.querySelector('#register-form'));

このような実装にすると、画面が比較的に複雑になっても

  • コンポーネント化することスコープを区切ることができる
  • モデルとビューが分かれることで、状態が見えやすくなる
  • ビューの更新が1箇所にまとまることで、複雑な状態をビューに持たなくて良くなる

ので、メンテナンス性の高いプログラムにすることができます。なお、React.jsやVue.jsは上記のことを仮想DOMを利用することで効率よく実行できるようにしています。React.jsやVue.jsを使わない環境であれば、今回紹介したようにJavaScriptを記載してみてください。

エンジニア募集

エムスリーでは自身で手を動かし、技術で医療の課題を解決するエンジニアを募集しています。 この記事(or 他の記事も)を読んで興味を持った方はぜひ下記リンクよりご応募ください!