はじめに
本書の想定読者・サンプルコード・バージョン情報
第1章 改善の前に
第2章 レガシーコードを理解する
第3章 パッケージ管理
第4章 テストコードを用意する
第5章 ESLint/Prettier
第6章 TypeScript
第7章 モジュール分割
第8章 Vue.js(セットアップ)
第9章 Vue.js(移行の予備知識)
第10章 Vue.js(移行編)
第11章 リリースまでを安全に
第12章 改善できた、次はどうする?
あとがき
フロントエンド開発には悩みがつきものです。それがレガシーコードが絡むものであれば尚更でしょう。
完全な新規の開発現場であれば、自分の好きなアーキテクチャーを採用した、モダンな環境を整えることができます。しかし現実はそう簡単ではありません。既存のコードを流用・修正をしていかねばならないことも多いのが現実です。
DOM依存べったりのコード、いたるところで実行されるグローバル関数、テストコードが無いのはあたりまえ……。私達に立ちはだかる障壁の数々、とてもつらいですね。
問題を解決するため、昨今ではさまざまなツールやライブラリー、フレームワークが登場して多くのプロダクトで利用されています。そしてこれらをゼロから始めるために必要な情報は、豊富に存在します。しかし、現実に存在する目の前の巨大なレガシーコードに導入しようと思った時点で「ゼロから」のスタートではないことに気付くこともあるでしょう。そういった「すでに動いているレガシーコードにどうやって導入していけばよいのか?」という情報は、あまり多くありません。このような理由でレガシーコードの改善作業は着手することは、それ自体のハードルがとても高く、何よりも動いているコードを壊すリスクから不安で勇気が出ないものです。
さらに、レガシーコードの改善は泥臭く地味な作業になりがちで、やらなくていいのであれば誰もやりたくないものでしょう。そんな作業をするよりも転職などで環境を変えたほうがいい、という意見を聞くこともあります。
しかし、現実にはそれぞれの事情で「やらなくてはいけない」というシチュエーションは存在します。筆者はこれまで「闇」と称されているようなレガシーフロントエンドコードと向き合い、まだまだ道半ばではありますがさまざまな改善を行ってきました。うまくいったこともあれば、見事に本番で不具合を発生させてしまったこともあります。その中で得られた多くの学びがあり、それを簡単ではありますが本書にまとめました。
もしもあなたが同じように困っているのであれば、この本が少しでもその助けになれば嬉しく思います。
本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
本書籍は、技術系同人誌即売会「技術書典6」で頒布されたものを底本としています。
本書におけるレガシーフロントエンドとは、次にいずれかに該当するものを指します。
・仕様が正しく把握されていない
・テストコードが存在しない
・モジュール管理が導入されていない
・DOM操作APIやjQueryでDOMを操作する
・長い期間手を加えられていない
・その他さまざまな理由により修正が困難である
本書では次のような読者を想定しています。
・レガシーフロントエンドから脱却する現実的な方法を知りたい
・モダンなツール・ライブラリーのメリットや導入方法を知りたい
・改善作業における心構えやノウハウを知りたい
・改善作業をしたいが何から手をつけたらよいかわからない
・実践的に手を動かしてモダンな技術要素を学びたい
なお、次の前提知識が必要となります。本書内では詳細な説明は省いていますのでご注意ください。
・プログラミングに関する基礎知識がある
・JavaScriptの基本的な用語・コードが理解できる
・実践編 : GitHubからコードを取得できる
本書では章の末尾に「実践編」という形で、jQueryで作られたTODO(タスク管理)アプリケーションをサンプルとして、章ごとに段階を追って適用し改善する例を掲載しています。このサンプルアプリケーションは単一のHTMLページ上で完結しており、簡易的なSPA(Single Page Application)として動作します。主な仕様は次のとおりです。
・追加ボタンをクリックするとTODOが追加される
・削除ボタンをクリックするとTODOが削除される
・一番上のTODOが次のタスクとして表示される
・タスクの総件数が表示される
・タスクが0件の場合には専用の表示となる
改善前の初期段階で、アプリケーションを構成する技術要素は次の内容です。
・jQuery
・JavaScript(ECMAScript5)
・テストコードなし
・モジュール管理なし
すべての実践編を完了すると、次の内容に置きかわります。
・Vue.js
・TypeScript
・ESLint/Prettier
・webpack(モジュール管理)
・Jest(テストコード)
・URL: https://github.com/mugi-uno/anzen-kaizen-guide/
ブランチによって内容に差異があります。実践編に沿ってコードを書いていく場合は、masterブランチをチェックアウトしてください。afterブランチでは改善完了後のコードを確認することができ、コミットログでは章に沿って修正を積み上げています。もし書いたコードがうまく動作しない場合などには参考にしてみてください。
・master : 改善前のレガシーフロントエンドコード
・after : 改善完了後のコード
本書で紹介するツール・ライブラリーは、次のバージョンで動作確認をしています。
・GoogleChrome バージョン: 72.0.3626.121(Official Build)(64 ビット)
・Node.js v11.9.0
・npm 6.5.0
その他のnpmパッケージのバージョンに関しては、リポジトリーのpackage.jsonとpackage-lock.jsonを参考にしてください。
・https://github.com/mugi-uno/anzen-kaizen-guide/blob/after/package.json
・https://github.com/mugi-uno/anzen-kaizen-guide/blob/after/package-lock.json
改善前のサンプルTODOアプリケーションのコード内容から、コアとなるレガシーコードを一部抜粋して紹介します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="./css/style.css">
<title>TODO</title>
</head>
<body>
<button id="addTodo">タスクを追加する</button>
<div id="todoList"></div>
<div id="todoEmpty">タスクがありません</div>
<div>
<span id='nextTodo'></span>
<span id='todoCount'></span>
</div>
<script src="./js/jquery-3.3.1.min.js"></script>
<script src="./js/script.js"></script>
</body>
</html>
// 「次のTODO」「総件数」「タスク有無による表示」を更新する関数
function updateAll() {
var count = $('.todo').length;
var next = $('.todo input').first();
var nextTodoText = count ? next.val() : '(未登録)'
$('#nextTodo').text('次のTODO: ' + nextTodoText);
$('#todoCount').text('(全' + count + '件)');
if (count) {
$('#todoList').show();
$('#todoEmpty').hide();
} else {
$('#todoList').hide();
$('#todoEmpty').show();
}
}
// タスクを追加する関数
function addTodo() {
var wrapper = $('<div>');
wrapper.addClass('todo');
var input = $('<input>');
input.attr('type', 'text');
var deleteButton = $('<button>');
deleteButton.addClass('delete').text('削除');
wrapper.append(input);
wrapper.append(deleteButton);
$('#todoList').append(wrapper);
}
// jQuery#readyで初期ロード時の処理を登録
$(function() {
// 追加ボタンクリック時のイベントハンドリング
$('#addTodo').on('click', function() {
addTodo();
updateAll();
});
// TODO入力時のイベントハンドリング
$('#todoList').on('input', '.todo:eq(0)', function() {
updateAll();
});
// 削除ボタン追加時のイベントハンドリング
$('#todoList').on('click', '.delete', function() {
$(this).closest('.todo').remove();
updateAll();
});
updateAll();
});
* {
font-size: 16px;
}
body {
padding: 20px;
}
#addTodo {
font-size: 1.2rem;
}
#todoList, #todoEmpty {
border-radius: 5px;
border: 1px dashed gray;
margin: 20px 0px;
padding: 20px;
text-align: center;
width: 400px;
}
.todo input {
width: 80%;
}
#nextTodo {
font-weight: bold;
font-size: 1.2rem;
}
#todoCount {
font-size: 0.8em;
}
最新のフレームワークはクールに見えます。SNSやWebの技術記事では、自分達が現役で使っている技術を名指しで「◯◯ is dead」と書かれ、ツラい気持ちになることもあるかもしれません。その上で技術的な負債とも呼べるコードを目の当たりにすると、「全部書き換えるべきだ!」という気持ちにもなるのもわかります。
しかしフロントエンドに限らず、アーキテクチャーの刷新といった改善作業を実施する際、いきなり無計画にコードを書き換え始めるのはオススメできません。今動作しているコードは、たとえレガシーであったとしても「動いている」という価値があります。これは無視してはならない重要なことです。古い技術で負債と呼べるようなものだとしても、プロダクトを支えているコードであることには変わりなく、書き換えていく際にはその部分は守って受け継がなければいけません。
一度冷静に深呼吸をして、
・「どのようなリスクがあるのか?」
・「なんのために・誰のためにやるのか?」
・「現実的なのか?」
・「本当に移行すべきなのか?」
・「安全とはなにか?」
といった点を考えてみましょう。
改善作業にはさまざまなリスクが伴います。メリットとデメリットを天秤にかけた上で着手すべきでしょう。そのために、まずはどのようなリスクがあるかを考えてみましょう。
改善作業の結果、稼働しているシステムの挙動を壊してしまう可能性があります。いわゆる、リグレッション/デグレードです。これは多くのエンジニアが恐れるものです。
エンドユーザーや関係者に影響が出ることも勿論ですが、なにより一度壊してしまうと「次」の改善作業への心理的な障壁が高くなりがちです。リスクばかりが目について、身動きが取りづらくなってしまいます。
改善作業は長い道のりになる可能性もあります。勢いよく最初の一歩を踏み出したものの、途中で心が折れてしまうかもしれません。メンバーの入れ替えなどの外部的な要因で、続行が困難となってしまうこともあります。
その結果、改善の前と後のコードが入り混じった状態で残ってしまうことになり、メンテナンス対象が増えて複雑化してしまいます。頓挫した場合には、改善の着手前より状況が悪化する可能性がある、ということを知っておくべきでしょう。
アーキテクチャーの部分を書き換えると、それだけエンジニアは新たに必要な知識や覚えるべきことが増えます。人数が多ければ多いほど、そのコストは大きくなることでしょう。
改善を行うのであれば「現状の何が問題か」「どのように変更するか」「何を使うか」といった情報を、適切に関係者に展開していかねばなりません。地味で手間な部分ですが、これを怠った結果、チームメンバーの負担が大きくなり「リファクタリングをしたら開発コストが上がった」といった結果を招きかねません。
リファクタリングそのものが、エンドユーザーに直接的に価値を与えることはほぼありません。それでも改善作業をやりたいということは、何か得たいものがあるはずです。
・開発速度を上げたい
・メンテナンスコストを下げたい
・モダンな環境を整えてエンジニアのモチベーションを上げたい
・テストコードを書きやすくしたい
これらはほんの一例です。自分達がなんのために改善作業を行うのかをきちんと明文化しましょう。これによって、作業の中で何を優先すべきかや、いざとなった場合に何を諦めるかが見えてきます。そして何より、改善作業を続ける上でのモチベーションに繋がります。
では、「安全に改善を進める」とは一体どういうことでしょうか。少し考えてみましょう。
安全に進めるうえにおいて重要なのが、とにかく小さくすることです。
作業のボリューム量とリスクの大きさは比例します。大きければ大きいほど、トラブル発生時のロールバックにも時間がかかったり、調査にも手間取るかもしれません。
改善作業は、すべてがうまくいくとは限りません。一度で修正するコード量を、できるだけ小さくしましょう。また、コード量だけでなくひとつひとつの作業サイクルも短いものとし、小さいスコープでリリースできるようにしましょう。ありとあらゆる改善に広く浅く手をつけるのではなく、ある特定の狭く小さい改善作業を完全に終わらせ、それを積み上げて大きな改善としていくことが、安全に進めるためのコツです。
改善作業は往々にして、着手してからその山の大きさに気付きます。軽い気持ちで手をつけると、途中で「これ全然終わらないのでは……?」と思うことも多いでしょう。そうならないために、事前の入念な準備が重要です。
まったく終わる見込みがないのであれば、最初から「やらない」のも価値のある決断です。中途半端に投げ出されて、より難解なコードを生み出してしまうよりは遥かによいでしょう。そのためには、着手する前の段階で全体像をしっかり把握しておく必要があるのです。
小さく進めることや準備を入念にしていくこと根本的な目的は、作業をするあなたが「改善に自信をもつ」ことにあります。
改善作業ではすでに動いているコードを直していくため、つねに不安や恐怖との戦いになります。これを払拭するために、資料化などを経て自分の理解を確認し、テストコードを書いて機会的に動作を保証する必要があります。そしてできるだけ小さく進めることで影響範囲を掌握しながら進めていくのです。
安全とは改善対象を壊さないという意味もありますが、何よりも作業するあなた自身が「心理的に安全」であることがもっとも重要なのです。
さて、散々脅すようなことを述べましたが、技術的な負債を返済することには大きな価値があります。先に説明したさまざまなリスクは、完全に打ち消したり無視することはできません。しかし、うまく進めることで軽減していくことは可能です。
本書では、ひとつひとつの改善作業のコスト自体は高くなる可能性はあるものの、可能な限りリスクを減らしながら、一歩ずつ「石橋を叩いて渡り」改善を進めることにフォーカスします。注意点として、すべてをそのまま実践していくと時間がかかりすぎる可能性もありますので、実際に改善作業と向き合った際に、もっともリスクと感じる部分にうまく取捨選択して適用していただければと思います。
「彼を知り己を知れば百戦殆うからず」という故事成語があります。安全に書き換えるためには、対象となるコードを誰よりも理解していなければなりません。
もしかすると、想像を遥かに超えて整理や分類が困難なほどに複雑なコードかもしれません。はたまた、ボリュームが巨大で到底終わりそうもない修正量かもしれません。これら自体は仕方のないことですが、先に状況を把握して、正しく知ることが重要です。
到底無理であるなら対象範囲を削る、やることを絞るなど、現実的な落とし所を探すことができます。新しいフレームワークを導入する前に一度リファクタリングを実施する、といった戦略も考えられます。
理解しないまま、とりあえずコードを触りはじめるのは大きなリスクです。戦う前に、まずはコードを正しく知るところから始めてみましょう。
レガシーなフロントエンドコードを読み解く場合、まずはDOMをどのように取り扱っているかを把握するとよいでしょう。理解しづらい大きい要因のひとつは、DOMと処理の関係性が見えづらいことにあります。最終的にVue.jsやReactといったフレームワークに書き換えるときも、DOMとそれに関連する処理のまとまりごとに書き換えていきます。つまり、逆に考えれば意味のあるまとまりを把握できていないと、書き換えもまた困難になるのです。
DOM操作コードは大きく分けて次に分類していくことができます。
DOMへ破壊的な値の書き込みを行う処理です。HTMLやテキストを直接書き換えているものが代表的でしょう。
// jQueryによるHTML・テキストの書き込み
$(".el").text("新しいテキスト");
$(".el").html("<span>コンテンツ</span>");
// DOM APIによるHTML・テキストの書き込み
document.querySelector(".el").innerText = "新しいテキスト";
document.querySelector(".el").innerHTML = "<div>コンテンツ</div>";
DOM要素自体の追加や削除も該当します。
// jQueryによるDOMの追加・削除
$(".el").append($("<div>"));
$(".el").remove();
// DOM APIによるDOMの追加・削除
document.querySelector(".el").append(document.createElement("div"));
document.querySelector(".el").remove();
クラス・属性・スタイルの更新もDOMを書き換えています。
// jQueryによるクラス・属性・スタイルの書き換え
$(".el").addClass("myclass");
$(".el").removeClass("myclass");
$(".el").attr("myattr", "abc");
$(".el").css("color", "red");
// DOM APIによるクラス・属性・スタイルの書き換え
document.querySelector(".el").classList.add("myclass");
document.querySelector(".el").classList.remove("myclass");
document.querySelector(".el").setAttribute("myattr", "abc");
document.querySelector(".el").style.color = "red";
jQueryなどのライブラリー利用時には「実はDOMを操作している」というAPIもあるので注意が必要です。
// 実はstyleに"display: none;"を付与している
$(".el").hide();
// 実はstyleに"height: 100px;"を付与している
$(".el").height(100);
DOMから何らかの値の読み込みを行う処理です。書き込みと同様、HTMLやテキストの読み込みが代表例でしょう。
// jQueryによるHTML・テキストの読み込み
var text = $(".el").text();
var html = $(".el").html();
// DOM APIによるHTML・テキストの読み込み
var domHtml = document.querySelector(".el").innerHTML;
var domText = document.querySelector(".el").innerText;
クラス・属性・スタイルの参照も該当します。
// jQueryによるクラス・属性・スタイルの読み込み
var myclass = $(".el").attr("myattr");
var hasclass = $(".el").hasClass("myclass");
var height = $(".el").css("height");
// DOM APIによるクラス・属性・スタイルの読み込み
var classList = document.querySelector(".el").classList;
var domAttr = document.querySelector(".el").getAttribute("myattr");
var domHeight = document.querySelector(".el").style.height;
要素数自体を取得している場合も読み込み処理に分類されるでしょう。
// jQueryによる要素数の取得
var length = $(".el").length;
// DOM APIによる要素数の取得
var domLength = document.querySelectorAll(".el").length;
クリック・入力といったイベントに応じて他の処理に繋げる処理です。
// jQueryによるイベントハンドリング
$(".el").click(function() {
eventHandler();
});
$(".el").on("click", function() {
eventHandler();
});
// DOM APIによるイベントハンドリング
document.querySelector(".el").addEventListener("click", function(){
eventHandler();
});