エムスリー エンジニアの岩本です。この記事はエムスリー Advent Calendar 2018 の23日目の記事です。
React.jsやVue.jsを使えれば、開発のベストプラクティスなどがあるので、メンテナンス性の高いプログラムはずいぶんと書きやすくなったと思います。本当に仮想DOMの功績は大きいですね。
しかし、世の中にはそういったライブラリを使うことができないプロジェクトもあるわけです。古すぎて、一部分だけ最新のソースコードにすることが憚られたり、サイズの問題でライブラリを入れることができなかったり。。。
その場合どのように書けばメンテナンス性の高いプログラムを書くことができるのでしょうか。そこでIE6時代からJavaScriptをもりもりと書いている私なりのベストプラクティスを紹介します。
jQueryではセレクタで要素にアクセスできるという素晴らしいAPIで簡単に要素にアクセスできるようになりました。「ちょっとこの要素消してほしい」「この要素に直前に追加したいアイコンがあるんだ」とかアドホックな対応に簡単に対応することができます。
ただ、そのように作ったアプリってちゃんとした設計がされていないので、最初の数個のHTML変更やイベント追加までは良いのだけども、ページ自体に機能が増えてくるとどこのイベントでなにをしているのかがわからなくなってしまう。その結果メンテナンス性の低いコードとなるのです。
さらにクライアントサイドのプログラミングはバックエンドとはかなり性質が異なり複雑になりがちです。
これらのせいで上から順番に実行すれば良いサーバサイドのプログラミングとは違った複雑さが生まれてしまいます。
結局React.jsやVue.jsで行っているように下記の3つの原則に則ってコードを書くことでかなり改善されると考えています。
コンポーネント指向で設計しないと、どこからどこまでがスコープなのかわからず、見通しが悪くなります。React.jsやVue.jsでも必ずコンポーネント単位に区切られているので同じように管理しましょう。コンポーネントはClassを使って表現します。コンストラクタにroot要素を受け取り、コンポーネントはそのroot要素の配下しか変更してはいけないルールとします。
React.jsやVue.jsは言わずもがなコンポーネント指向ですね。
次に、Viewの状態を表すモデル(ViewModel)を定義します。ViewModelがあるとコンポーネントの状態が一目瞭然となり、Viewの更新と処理を切り分けることも可能となります。
React.jsだとpropsやstate、Vue.jsだとpropsやdataがこれに当たります。
これが一番大事ではないかと考えています。画面の更新を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><metacharset="utf-8"><metaname="viewport"content="width=device-width"><title>sample</title><style>#register-formlabel.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><formid="register-form"><div><labelclass="field-label">メルマガ登録</label><label><inputname="mail-magazine"type="radio"value="yes"/> 希望する</label><label><inputname="mail-magazine"type="radio"value="no"/> 希望しない</label></div><divid="magazines-container"style="display: none;"><labelclass="field-label">登録するメルマガ</label><divclass="checkbox-block"><labelclass="block"><inputname="magazines"type="checkbox"value="1"/> メルマガ1</label><labelclass="block"><inputname="magazines"type="checkbox"value="2"/> メルマガ2</label><labelclass="block"><inputname="magazines"type="checkbox"value="3"/> メルマガ3</label><labelid="magazine3_1-container"class="block child"style="display: none;" ><inputname="magazines"type="checkbox"value="3-1"/> メルマガ3-1(メルマガ3を選択したときのみ表示)</label></div></div></form><scriptsrc="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();elsethis.$magazinesContainer.hide();// magazine3_1の更新if (this.showMagazine3_1)this.$magazine3_1Container.show();elsethis.$magazine3_1Container.hide();}new RegisterForm(document.querySelector('#register-form'));
このような実装にすると、画面が比較的に複雑になっても
ので、メンテナンス性の高いプログラムにすることができます。なお、React.jsやVue.jsは上記のことを仮想DOMを利用することで効率よく実行できるようにしています。React.jsやVue.jsを使わない環境であれば、今回紹介したようにJavaScriptを記載してみてください。
エムスリーでは自身で手を動かし、技術で医療の課題を解決するエンジニアを募集しています。この記事(or 他の記事も)を読んで興味を持った方はぜひ下記リンクよりご応募ください!
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。