JavaScriptを取り巻く環境がどんどん変化しています。新たなツールやフレームワークが生み出されているだけでなく、言語そのものがES2015(ES6)の登場で大きく変わりました。JavaScript開発の学習がいかに難しいか愚痴をこぼす記事がたくさんありますが、無理もないことです。
本記事ではモダンJavaScriptを紹介します。JavaScriptの進展を解説し、フロントエンドWebアプリケーションを作るために使われているツールや手法の全体像を確認します。JavaScriptを学び始めたばかりの人や、以前は使っていて数年間離れたため変化を知りたい人はぜひ読んでください。
Node.jsについて
Node.jsはサーバーサイドのプログラムをJavaScriptで書くためのランタイムです。Node.jsを使うと、アプリのフロントエンドとバックエンドをJavaScriptで書いたフルスタックJavaScriptアプリケーションを作れます。この記事はクライアントサイド開発に焦点を当てていますが、それでもNode.jsは重要な役割を担います。
Node.jsが登場し、npmパッケージマネージャーとCommonJSモジュールフォーマットが普及したことはJavaScriptを取り巻く環境に衝撃を与えました。Node.jsの登場以降、開発者たちはブラウザー、サーバー、ネイティブアプリケーション間の境界をなくし、斬新なツールや新たな手法を開発するようになりました。
JavaScript ES2015+
2015年、JavaScriptの言語仕様であるECMAScriptの第6版、ES2015がリリースされました(いまでもES6と呼ばれることがあります)。新バージョンでは機能が大幅に追加され、希望するWebアプリケーションの構築がより簡単に、より実現しやすくなりました。しかし、ES2015で進歩が止まったわけではありません。毎年、新たなバージョンがリリースされているのです。
変数の宣言
JavaScriptで変数を宣言する方法が2つ追加されました。letとconstです。
letはvarの後継となる機能です。ただし、varも引き続き利用できます。letは、関数内ではなく宣言したブロック内に変数のスコープが制限され、エラーの発生を減らせます。
// ES5
for (var i = 1; i < 5; i++) {
console.log(i);
}
// <-- logs the numbers 1 to 4
console.log(i);
// <-- 5 (variable i still exists outside the loop)
// ES2015
for (let j = 1; j < 5; j++) {
console.log(j);
}
console.log(j);
// <-- 'Uncaught ReferenceError: j is not defined'
constは、値の再代入を受け付けない変数を宣言できます。数値や文字列といったプリミティブ型では定数に似た働きをします。
const name = 'Bill';
name = 'Steve';
// <-- 'Uncaught TypeError: Assignment to constant variable.'
// Gotcha
const person = { name: 'Bill' };
person.name = 'Steve';
// person.name is now Steve.
// As we're not changing the object that person is bound to, JavaScript doesn't complain.
アロー関数
アロー関数は、簡潔な構文で無名関数(ラムダ式)を宣言できます。関数内に式が1つならfunctionキーワードとreturnキーワードを省略できて、より機能的な関数式が書けます。
// ES5
var add = function(a, b) {
return a + b;
}
// ES2015
const add = (a, b) => a + b;
定義されたコンテキスト内のthisの値を継承することも、アロー関数の重要な特徴です。
function Person(){
this.age = 0;
// ES5
setInterval(function() {
this.age++; // |this| refers to the global object
}, 1000);
// ES2015
setInterval(() => {
this.age++; // |this| properly refers to the person object
}, 1000);
}
var p = new Person();
クラス構文の機能が向上
オブジェクト指向プログラミングが好きなら、プロトタイプに基づく既存の仕組みにクラスが追加されたことは朗報でしょう。あくまで糖衣構文に過ぎませんが、プロトタイプを使って古典的なオブジェクト指向を模擬したい開発者には、より洗練された構文を提供します。
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
Promiseと非同期関数
JavaScriptの非同期処理は長い間、困難なものとされてきました。ある程度の機能があるアプリケーションなら、Ajaxリクエストなどを処理する際「コールバック地獄」に陥る危険性があったのです。
ES2015でPromiseのネイティブサポートが追加されました。Promiseは演算時には存在せず、あとで利用可能になる値のことです。Promiseを使うとコールバックのネストが深くならず、関数の非同期呼び出しを管理しやすくなります。
ES2017(今年公開予定)では非同期関数が導入されます(async/awaitと呼ばれることもある)。同期処理のように非同期処理のコードを書いて非同期処理の機能が向上されます。
async function doAsyncOp () {
const result = await asynchronousOperation();
console.log(result);
return result;
};
モジュール
ES2015で追加された特筆すべき機能にネイティブのモジュールフォーマットがあります。以前は、モジュールはサードパーティ製ライブラリーから読み込むしかありませんでしたが、モジュールの定義と使用が言語の一部になりました。
この記事で紹介していない機能もありますが、モダンJavaScriptの主な変更点はおおむね網羅しました。BabelのWebサイトにあるES2015を学ぶでは変更点一覧をサンプル付きで確認できます。JavaScriptの最新情報収集に便利です。変更点にはテンプレート文字列、イテレーター、ジェネレーター、MapやSetなどの新たなデータ構造などがあります。
Lintによるコードの検査
Linterはコードを構文解析して一連の規則と比較するツールで、構文エラー、フォーマット、グッドプラクティスを検査します。特に初心者におすすめです。コードエディターやIDEに合わせて適切に設定すれば即座にフィードバックが得られるようになり、新たな機能を学ぶとき構文エラーに長時間悩むこともなくなります。
人気が高く、ES2015+もサポートしているLinterであるESLintはJSプログラマーのイラッとする「クセ」はESLintを導入して対処しようで確認できます。
モジュール方式のコード
最近のWebアプリケーションは数千行(場合によっては数十万行)ものコードで書かれてるため、コードを小さなコンポーネントに分けて整理する仕組みが必要です。機能を限定し、単独で機能する個々のコードを書くことで、適切な管理のもと必要に応じて再利用できるのがモジュールです。
CommonJSモジュール
過去数年の間にモジュールフォーマットがいくつか登場しましたが、その中でもっとも人気なのがCommonJSです。CommonJSはNode.jsの標準モジュールフォーマットですが、モジュールバンドラーの助けを借りてクライアントサイドのコードにも使えます。モジュールバンドラーはあとで紹介します。
JavaScriptファイルから機能をエクスポートするにはmoduleオブジェクトを利用します。また、機能を必要な場所にインポートするにはrequire()関数を使います。
// lib/math.js
function sum(x, y) {
return x + y;
}
const pi = 3.141593
module.exports = {
sum: sum,
pi: pi
};
// app.js
const math = require("lib/math");
console.log("2π = " + math.sum(math.pi, math.pi));
ES2015モジュール
以前はサードパーティ製ライブラリーを使っていましたが、ES2015ではコンポーネントを定義、利用する方法が言語内に導入されました。必要な機能を格納したファイルを個別に用意し、そのファイルから特定のパーツだけをエクスポートさせアプリケーションで利用できます。
注記:ES2015のモジュールをネイティブサポートするブラウザーは開発中のため、現時点でES2015のモジュールを使うには追加のツールが必要です。
例えば以下の通りです。
// lib/math.js
export function sum(x, y) {
return x + y;
}
export let pi = 3.141593;
関数と変数をエクスポートするモジュールです。別のファイルにこのファイルをインクルードすればエクスポートした関数を利用できます。
// app.js
import * as math from "lib/math";
console.log("2π = " + math.sum(math.pi, math.pi));
必要なものだけ指定してインポートできます。
// otherApp.js
import {sum, pi} from "lib/math";
console.log("2π = " + sum(pi, pi));
これらの例はBabelのWebサイトから引用しました。より詳しく知りたいならES6のモジュールを理解するを見てください。
パッケージ管理
ほかのプログラミング言語はかなり前から、サードパーティ製ライブラリーやコンポーネントの検索とインストールを簡単にするために専用のパッケージリポジトリやパッケージマネージャーがありました。Node.jsには専用のパッケージマネージャーおよびリポジトリであるnpmがあります。npmはJavaScriptのデファクト・パッケージマネージャーです。また、世界最大のパッケージレジストリと言われています。
npmリポジトリでサードパーティ製のモジュールを探すことができます。見つけたモジュールはnpm install <package>コマンドで簡単にダウンロード、利用できます。パッケージはローカルのnode_modulesディレクトリにダウンロードされます。すべてのモジュールと依存オブジェクトがこのディレクトリに格納されます。
ダウンロードしたパッケージはプロジェクトの依存オブジェクトとしてプロジェクトやモジュールの情報を含めpackage.jsonファイルに記載できます(プロジェクトやモジュールはパッケージとしてnpmに公開できます)。
開発用と製品用に別々に依存オブジェクトを定義できます。製品用の依存オブジェクトはパッケージの動作に必要ですが、開発用はパッケージの開発者にのみ必要です。
package.jsonファイルの例です。
{
"name": "demo",
"version": "1.0.0",
"description": "Demo package.json",
"main": "main.js",
"dependencies": {
"mkdirp": "^0.5.1",
"underscore": "^1.8.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sitepoint",
"license": "ISC"
}
ビルドツール
最近のWebアプリケーションは、開発時のコードと実際の製品に使われるコードが異なります。開発者はブラウザーにサポートされていない可能性がある最新バージョンのJavaScriptでコードを書いたり、node_modulesフォルダー内のサードパーティ製パッケージとその依存オブジェクトを多用したり、静的解析ツールや圧縮ツール(Minifier)などを使ったりして開発します。これらをすべて変換、効果的にデプロイして、大半のWebブラウザーが理解できる形にするのがビルドツールです。
モジュールのバンドル
HTMLにscriptタグを大量に書くのは、本格的なアプリケーションでは管理しきれなくなる上にscriptタグによるHTTPリクエストがパフォーマンス低下を引き起こすので、現実的ではありません。
きれいで再利用できるコードをES2015モジュールやCommonJSモジュールで書くには、ブラウザーがES2015モジュールの読み込みをネイティブサポートするまでは「モジュールを読み込む方法」が必要です。
ES2015のimport文(CommonJSではrequire)を使えば必要なモジュールをすべてインクルードできます。モジュールバンドラーを使えばすべてのコードが少数のファイル(バンドル)にまとまります。このファイルをサーバーにアップロードし、HTMLにインクルードさせます。ファイルにはインポートしたモジュールやそのモジュールが必要な依存オブジェクトがすべて含まれます。
人気のあるツールはいくつかありますが、Webpack、Browserify、Rollup.jsがおすすめです。ニーズに合わせて選んでください。
モジュールバンドラーについてより詳しく学びたい、ふかんてきな見地でモジュールバンドラーについて学びたいならいまどきなフロントエンド開発者になる!JSのモジュール管理ってこういうことがおすすめです。
トランスパイラー
最新のブラウザーはES2015をある程度サポートしていますが、アプリを利用するユーザーがES2015を部分的にしかサポートしていない、あるいはまったくサポートしていないレガシーブラウザー、レガシーデバイスを使っている可能性もあります。
最新のJavaScriptを動かすには、書いたコードを前のバージョン(たいていはES5)のコードに変換する必要があります。この作業の標準的なツールがBabelです。Babelはコードをほとんどのブラウザーで使える互換コードに変換するコンパイラーです。この機能のおかげで、ベンダーの実装を待たずにJavaScriptの最新機能をすべて利用できるのです。
構文の変換ではない作業が必要な機能もあります。Babelに含まれているPolyfillはpromseなどの複雑な機能を実行する仕組みをエミュレートします。
ビルドシステム、タスクランナー
モジュールバンドラーとトランスパイラーは、プロジェクトで必要なビルドプロセスの一部に過ぎません。そのほかのプロセスにはコードの圧縮(ファイルサイズを減らす)、解析用ツールの使用、場合によっては画像最適化やCSS/HTMLプリプロセッサーの使用などJavaScriptが関係しないタスクが含まれます。
タスク管理は面倒な作業です。そこで、タスク管理を自動化し、どんな操作も単純なコマンドで実行できる仕組みがあります。人気があるツールはGrunt.jsとGulp.jsです。タスクをグループにまとめて管理できます。
たとえば、コマンドgulp buildを使えばLinter、Babelによるトランスパイル、Browserifyによるモジュールのバンドルを実行できます。3つのコマンドとそれに対応する引数を順序も含めて覚えておく必要はなく、コマンドを1つ実行すればすべて自動で処理されます。
プロジェクトで手作業が発生したら、タスクランナーで自動化できないか考えます。
さらに詳しく知りたいなら面倒な作業も発狂しない!Web制作を超効率化するgulp.jsの始め方(2017年版)を読んでください。
アプリケーションのアーキテクチャー
Webアプリケーションの要件はWebサイトとは異なります。たとえば、ブログならページの再読み込みが許容されますが、Google Docsのようなアプリケーションではそうはいきません。Webアプリケーションはできるかぎりデスクトップアプリケーションに近い動きを目指すべきです。さもなければユーザビリティが損なわれます。
従来のWebアプリケーションは、Webサーバーから複数のページを送信するなど、多くの動的な処理が必要になるとユーザーの操作に合わせてAjaxでHTMLを置き換えてコンテンツを読み込む方法が一般的でした。この方法は、動的なWebアプリケーションの実現には大きな前進でしたが、とても複雑でした。ユーザーが操作するたびにHTMLの断片、またはページ全体を送信するのはリソースの無駄使いです。特にユーザーにとって時間の無駄使いでした。これでは、デスクトップアプリケーションにはかないません。
この状況を脱する新たな手法は、ユーザーにアプリケーションを表示するのではなく、クライアントがサーバーとコミュニケーションをとることです。必要なJavaScriptの量は一気に増えましたが、クリックのたびに再読み込みや長い待ち時間が必要になることもなく、ネイティブアプリケーションとほとんど変わらない動作が可能になりました。
シングルページアプリケーション(SPA)
一般的なWebアプリケーションでもっとも高度なアーキテクチャーはSingle Page Application、SPAです。SPAとは、アプリケーションの動作に必要なあらゆるものが含まれたJavaScriptの大きな塊です。ユーザーインターフェイスはすべてクライアントサイドで描画されるので、再読み込みの必要はなく、唯一変化するものがアプリケーション内のデータです。通常はAjaxなどの非同期通信でリモートAPIで処理します。
この手法の欠点は、最初の読み込み時間が長いことです。しかし、一度読み込めば、クライアントとサーバーの間はデータの送受信だけなので、ビュー(ページ)間を高速で移動できます。
Universal / Isomorphicアプリケーション
SPAはすばらしいユーザーエクスペリエンスをもたらしてくれますが、場合によっては最適解にならないこともあります。最初の読み込み時により高速な反応が求められる場合や検索エンジンのインデックス最適化が必要な場合です。
こうした問題点を解決するために最近登場した手法がIsomorphic(またはUniversal)JavaScriptアプリケーションです。コードの大部分をサーバー側とクライアント側の両方で実行できるので、最初のページ読み込みでは高速化のためにサーバー側で必要なものを描画し、そのあとでユーザーがアプリとやりとりする際はクライアント側による描画に切り替えます。Webページは最初にサーバー側で描画されるので、検索エンジンはこれらのWebページを適切にインデックスします。
デプロイ
モダンJavaScriptアプリケーションは、本番環境にビルドプロセスの結果だけをデプロイするので、開発者が書くコードは本番環境のコードと同一ではありません。作業のワークフローはプロジェクトの規模や開発者の人数、使うツールやライブラリーによって左右されます。
たとえば、単純なプロジェクトに1人で携わっているなら、デプロイ準備が整うたびにビルドプロセスを実行し出力ファイルをWebサーバーにアップロードします。アップロードが必要なのはビルドプロセス(トランスパイル、モジュールのバンドル、圧縮など)によって生成された出力ファイルだけです。この出力ファイルはアプリケーションと依存オブジェクトをすべて含んだ単一の.jsファイルです。
ディレクトリ構造は次のようにします。
├── dist
│ ├── app.js
│ └── index.html
├── node_modules
├── src
│ ├── lib
│ │ ├── login.js
│ │ └── user.js
│ ├── app.js
│ └── index.html
├── gulpfile.js
├── package.json
└── README
ES2015で書いたすべてのアプリケーションファイルをsrcディレクトリに配置し、npmでインストールしたパッケージと、作成したモジュールをlibディレクトリからインポートします。
次にGulpを実行します。Gulpはgulpfile.jsにある命令を実行しプロジェクトをビルドします。ここで、すべてのモジュール(npmでインストールしたモジュールも含む)を1つのファイルにバンドルして、ES2015+をES5にトランスパイルし、出力ファイルを圧縮します。結果をdistディレクトリに出力すると便利です。
補足:プロセスを必要としないファイルは、srcディレクトリからdistディレクトリにコピーできます。ビルドシステムでそのタスクを設定できます。
distディレクトリからWebサーバーにファイルをアップロードします。残りのファイルは開発にのみ使うものなので気にする必要はありません。
チーム開発
ほかの開発者と一緒に作業しているなら、GitHubなどのコードリポジトリを使ってプロジェクトを保存しているはずです。ビルドプロセスをコミット前に実行してビルド結果をほかのファイルとともにGitリポジトリに保存し、あとで本番環境用のサーバーにダウンロードします。
しかし、複数の開発者が一緒に作業すると、ビルドしたファイルをリポジトリに保存するときにエラーが起こります。このような場合でも、ビルドしたファイルを正しい状態に保ちたいと思う人が多いはずです。プロセスにJenkins、Travis CI、CircleCIといったサービスを組み込み、コミットがリポジトリにプッシュされるたびにプロジェクトを自動的にビルドすれば、この問題に対処できます。開発者はプロジェクトをビルドする必要がなく、コードの変更をプッシュすることだけに集中できます。また、リポジトリは自動的に生成されたファイルによって常に正しい状態に保たれます。ビルドしたファイルはもちろんデプロイに利用できます。
最後に
単純なWebページからモダンなJavaScriptアプリケーションへの移行は、ここ数年間Web開発から遠ざかっていた人にとっては面倒で意欲を削がれることかもしれません。しかし、この記事が出発地点になれば幸いです。各トピックにはより詳細な記事を可能な限りリンクしたので参考にしてください。
すべて見たあと、量の多さに圧倒され混乱するかもしれません。そんなときはKISSの原則を心掛け、全部使おうとするのではなく必要なものだけ使ってください。結局、重要なのは最新技術をすべて使うことではなく、問題を解決することです。
(原文:The Anatomy of a Modern JavaScript Application)
[翻訳:薮田佳佑/編集:Livit]