読者からときどき聞こえてくるのは、自分自身でJavaScriptのプロジェクトを進める方法が分からないということです。
理由の1つは、記事では具体的な手順が示されるので、手順そのものを自分で考えることがないからです。またプロジェクトに手こずるもう1つの理由は、まだ作りかけの状態なのに他人の完成作品と比べて落胆することです。
実際のプロジェクトの進め方は記事(私のも含めて)で説明しているほど単純ではありません。現実には、プロジェクトは完璧なコードをガンガン書くことではなく、たくさんの試行錯誤とリファレンスを幾度も参照して少しずつできあがるものなのです。
本記事では、自分でJavaScriptプロジェクトを進めていく方法を説明します。
重要な注意事項:記事にはいくつかのサンプルコードを掲載しています。なじみのないコードなら、ひとまず飛ばしてしまって構いません。記事の主旨はプロジェクトへの取り組み方の全体像を理解することであり、技術的な詳細ではありません。
最初に基本を身に着けよう
少なくともJavaScript(そしてプログラミング全般)の基本を身に着ける必要があります。変数、関数、if文、繰り返し文、配列、オブジェクト、DOM操作メソッド(getElementById、querySelectorAll、innerHTMLなど)が含まれます。必要ならばこの記事を読み終えてからGoogle検索するかMDNで探してください。
一度こうした概念に慣れたら、もうif文をどう書くかに悩まず製作に没頭できるので、作業はぐっと速くなるでしょう。
多くの人がこの手順をおろそかにするので、結果的に遠回りをしています。まるでゲームをするときに、まだレベル1もできていないのにレベル3でプレイするようなものです。こうすればイライラは避けられます。
計画を立てる
いきなりプロジェクトに飛びついて一直線の手順でこなすのではなく、全体像を眺める時間を作ります。計画するのです。どのようなことが必要なのでしょうか。 たとえば、カウントダウン時計を作るなら、時間の計測、データの保持、数字の表示場所、時計の操作などの手段を考える必要があります。
この段階ではまだ技術的な泥沼にはまらずに、なにがしたいのかをしっかり考えてください。全体の計画という道しるべがあれば、迷走することはありません。ソフトウェア設計の世界ではよくこのことをユースケース分析と呼んでいます。
コード無しで書いていく
計画ができあがったら、詳細を考えます。個人的に最良だと思う方法は、プロジェクトの各パートにおいてなにが求められるのかを具体的に書き出すことです。このとき、まだコードを書かずに言葉だけで記述するのがカギです。これはpseudocode(疑似コード)と呼ばれます。pseudocodeなら構文を気にすることなく、プロジェクトの動作だけに思考を集中できます。
カウントダウン時計の場合なら、書き出した内容は次のようになるでしょう。
- 現在時刻を取得する
- 終了時刻を決定する
- 現在時刻と終了時刻の差から、残り時間を求める
- カウントダウン中には残り時間の取得処理を繰り返す
- カウントダウン中には画面上に残り時間を表示する
内容を書き出したら、次は以下のように、各パートごとに小さく分解していきます。
- カウントダウン中には画面に残り時間を表示する
- 時刻を「時・分・秒」に分ける
- 1つのコンテナに「時」を表示する
- 「分」「秒」も同様にする
いったんロジックを書き出してしまえば、コードを書くのが格段に楽になります。なぜなら、いきなり「カウントダウン時計を作れ」と言われて全コードを書くよりも、「終了時刻から現在時刻を引き算する」といった具体的な手順にしたがってコードを書くほうが簡単だからです。
ただし、このとき最初から完璧に全手順を書き出す必要はありません。書き出した手順は流動的ですから、手順を加えたり、取り除いたり、間違えたりして、試行錯誤しながら改良すればよいのです。
小さな部分に分けて製作する
手順ができあがったら、小さな部分に分けてコードを書き始めます。カウントダウン時計の場合、現在時刻の取得から始めるでしょう。
const currentTime = new Date().getTime();
console.log(currentTime);
次に、カウントダウン終了時刻を取得します。
const endTime = new Date(2017, 4, 4, 7, 30).getTime();
console.log(endTime);
自分の時計を作るときは上のサンプルのように特定の日付を終了日にできますが、本記事のサンプルがある日で停止しては困るので、終了日を10日後としています(JavaScriptで使う単位がミリ秒なので、10日間はミリ秒に変換されます)。
const endTime = new Date().getTime() + 10*24*60*60*1000;
console.log(endTime);
コードを小さな部分に分けて書いていくほうが良い理由は、次のようなことです。
- 次の手順に進む前に、作った機能が正常動作するかを確認できる
- ほかのたくさんの関連要素に悩まされず、考えやすい
- 一度に大量のことを気にかける必要がないため、作業が速い
- エラーの発見や防止がしやすい
- 必要なときにテストできる
- 往々にして、部品として使い回せるコードが書ける
各パーツを結合する
各部分の準備ができたら、結合します。この段階の一番大切なことは、単独では動作する各パートが、結合後も動作するか確認することです。少し変更が必要な場合もあります。
以下は、カウントダウン時計で残り時間の計算のために開始時刻と終了時刻を結合するときの例です。
// set our end time
const endTime = new Date().getTime() + 10*24*60*60*1000;
// calculate remaining time from now until deadline
function getRemainingTime(deadline){
const currentTime = new Date().getTime();
return deadline - currentTime;
}
// plug endTime into function to output remaining time
console.log(getRemainingTime(endTime));
小さな部品に分けてから結合する方法だと、頭の中で同時にたくさんのことに注意を払う必要がないため、一度に全体を作ろうとするよりもずっと簡単です。
残り時間を取得する関数ができたので、繰り返し実行して表示時間を更新し続けるようにします。
このときのHTMLは以下です。
<div id="clock"></div>
JavaScriptは次のようになります。
// set our end time
const endTime = new Date().getTime() + 10*24*60*60*1000;
// calculate remaining time from now until deadline
function getRemainingTime(deadline){
const currentTime = new Date().getTime();
return deadline - currentTime;
}
// store clock div to avoid repeatedly querying the DOM
const clock = document.getElementById('clock');
// show time repeatedly
function showTime(){
const remainingTime = getRemainingTime(endTime);
clock.innerHTML = remainingTime;
requestAnimationFrame(showTime);
}
requestAnimationFrame(showTime);
上の例では画面に残り時間を表示するためにshowTime関数を加えました。関数の最後に加えたrequestAnimationFrame(showTime)は、ブラウザー側が準備でき次第showTimeを再実行します。これでパフォーマンス面で有利な方法を使って表示時刻を更新し続けられます。
知っての通りカウントダウンはすべてミリ秒単位です。次の手順はミリ秒を日・時・分・秒に変換します。
ここまでに説明してきた方法(小さな手順に分解するなど)を使って、最初にミリ秒を秒に変換し、どうなるかを確認してから関数に組み入れます。同じ手順で分・時・日を計算できます。最終的な結果は次のようになります。
function showTime(){
const remainingTime = getRemainingTime(endTime);
const seconds = Math.floor((remainingTime/1000) % 60);
const minutes = Math.floor((remainingTime/(60*1000)) % 60);
const hours = Math.floor((remainingTime/(60*60*1000)) % 24);
const days = Math.floor(remainingTime/(24*60*60*1000));
clock.innerHTML = `${days}:${hours}:${minutes}:${seconds}`;
requestAnimationFrame(showTime);
}
requestAnimationFrame(showTime);
実験とテスト
この段階ですでに、たくさんの実験とテストを繰り返して動作確認をしてきました。きちんと動作するならば、いろいろ試して不具合がないか確認してください。たとえば、もしユーザーがここやあそこをクリックしたら? もし入力値が想定外の値だったら? もし画面サイズが狭かったら? もし想定するブラウザーで動作しなかったら? 各手順でもっと効率良くできるところは?
カウントダウン時計の例に戻り、もしタイマーが0になったらどうなるでしょうか? if文を加えて、時計が0で止まるようにします。
function showTime(){
...
// ensure clock only updates if a second or more is remaining
if(remainingTime >= 1000){
requestAnimationFrame(showTime);
}
}
値に1000ミリ秒(1秒)を使う理由は、もし0を使うと時計が行き過ぎて-1まで進んでしまう可能性があるからです。時計が秒よりも小さい単位で動いている場合、終了条件式を1秒よりも小さい値にしてください。
この問題は記事の執筆中に友人が指摘してくれたことで、これもまた最初のコードが完璧ではないことを示す良い例です。
ここまでの説明は、まさに次に述べるポイントにつながります。
外部の助けを求める
外部に助けを求めることは、プロジェクトのどの時点においても大切なことです。「助け」は参考文献でも、ほかの人でもかまいません。わざわざこの話を持ち出したのは、開発者はなにも見ず誰の手も借りずに座って完璧なコードを書ける…という、よくある誤解が蔓延しているからです。
ベテラン開発者が頻繁に調べものをするのを見て、新人の開発者が驚いたという話をよく耳にします。実際のところすべてを知り尽くすのは不可能なので、情報を探し当てる力というのは一番大切な技術なのです。
ツールや技術が変化しても、「学ぶ技術」は風化しません。
コードのリファクタリング(再構築)
プロジェクトを完成する前に、コードのリファクタリング(見直し・再構築)を検討してください。以下は、改良のために自分自身に問いかけたい質問です。
コードは簡潔で読みやすいか?
もし簡潔さと読みやすさのどちらか片方を選ばなければならないとしたら、よほどパフォーマンスの問題がない限り、通常は読みやすさを優先します。読みやすければ、メンテナンス、更新、修正も楽になるからです。
コードは効率的か?
たとえば、もしコード内で同じ要素を何度も繰り返し参照するなら、その要素は変数に格納してコードの負担を減らしたほうが良いでしょう。カウントダウン時計のサンプルでも以下の部分を次のように処理しました。
// store clock div to avoid repeatedly querying the DOM
const clock = document.getElementById('clock');
関数や変数の名前は分かりやすいものになっているか?
たとえば、showTimeのような関数名は、stとするよりもずっと分かりやすいです。これはとても大切な点です。ときに開発者はそのときピッタリだと思って略称を付けたために、あとからなにを略したのか思い出せず途方に暮れることがあります。名前が分かりやすいかどうかを知るには、コードを書かない人に名前の由来を簡単に説明できるか考えてみると良いでしょう。
名前の衝突の可能性はないか?
たとえば、「container」のような名前だと、どこかほかの部分で同じ名前が使われている確率が高そうです。
また、スコープをよく考えずにグローバル変数を使いすぎてはいませんか?
グローバルスコープを保護する簡単な方法は、カウントダウン時計のコードを即時関数(immediately-invoked function expression、IIFE)に入れることです。これなら時計のコードはすべての変数にアクセスできますが、ほかのコードからはアクセスできなくなります。
(function(){
// code goes here
})();
編集過程でエラー原因を生んでいないか?
たとえば、ある場所で変数名を変更しながら、ほかの場所では名前をそのままにしていたりしませんか? オブジェクトになにかを追加したときにカンマを加えるのを忘れてはいませんか?
表示は適切になっているか?
カウントダウン時計のサンプルで、1桁の表示に0が付いたら見やすくなります(10:9ではなく10:09と表示)。1つの方法は値が9以下なら先頭に0を加えることですが、少し面倒です。別のサンプルコードで見つけた賢いワザは、最初に先頭に0を追加して、次にslice(-2)で、値がなんであれ最後の2桁を取得するという方法です。コードは次のようになります。
function showTime(){
const remainingTime = getRemainingTime(endTime);
const seconds = ('0' + Math.floor((remainingTime/1000) % 60)).slice(-2);
const minutes = ('0' + Math.floor((remainingTime/(60*1000)) % 60)).slice(-2);
const hours = ('0' + Math.floor((remainingTime/(60*60*1000)) % 24)).slice(-2);
const days = ('0' + Math.floor(remainingTime/(24*60*60*1000))).slice(-2);
clock.innerHTML = `${days}:${hours}:${minutes}:${seconds}`;
// ensure clock only updates if a second or more is remaining
if(remainingTime >= 1000){
requestAnimationFrame(showTime);
}
}
requestAnimationFrame(showTime);
コードが無駄に冗長になっていないか?
本来なら関数やループでまとめられるところを、繰り返し同じコードを書いてはいませんか? 先の処理では、出力にゼロを追加するコードは単独の関数にまとめられます。これで重複が減り、コードも読みやすくなります。
function pad(value){
return ('0' + Math.floor(value)).slice(-2);
}
const seconds = pad((remainingTime/1000) % 60);
プロジェクトを新鮮な目で見るとどう見えるか?
数日置いてから、自分のコードを見直してください。新鮮な目で見ると、もっとすっきり書ける箇所や効率化できる箇所が発見できるでしょう。
手直しすると、コードはますますエレガントになっていきます。それを納品すれば、コードを見た人びとは「いったいどうやったらこんなに完璧に書けるのか」と驚くでしょう。
数カ月後にあらためて見直すと、もっとうまくできたはずなのにと気がつくでしょう。私の友人の1人は、進歩している証だから良いことなんだと言ってくれました。
もし興味があれば、時計のサンプルのデモを見てください(若干のスタイルを追加しています)。
最後に
プログラミングのプロジェクトが単純に順を追ってまっすぐ進展することはまずありません。この記事で覚えておいてほしいもっとも重要なことは「一度に全体を作ろうとするよりも、小分けにして試しながら作るほうがずっと良い」という点です。
以前にJavaScriptのプロジェクトで苦しんだ経験があるならば、この記事が役に立つことを祈ります。
※本記事はVildan Softic、Matt Burnettが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:What Tutorials Don’t Tell You: How to Approach Projects)
[翻訳:西尾健史/編集:Livit]