いまどきなフロントエンド開発者になる!JSのモジュール管理ってこういうこと

2016/05/18

Mark Brown

48

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Browserify、Webpackなど、ここ数年耳にするようになったフロントエンド開発ツール。結局何をやってるの? いまどきのフロントエンド開発者になるために欠かせない、JavaScriptのモジュール管理についての少し長いまとめ。

本記事はDan PrinceRavi Kiranが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

開発者はモジュール、依存関係の管理、最新のプログラミング言語の基本的要件の動的ロードについて考えています。重要ないくつかの機能は、2015年にJavaScriptに追加されたものです。

モジュールはNode.jsで広く使われていますが、この記事ではモジュールをブラウザーで使う方法について焦点をあてていきます。少し過去を振り返り、混乱しがちな現在の状況までの歩みを紹介しながら、将来の見通しを立て、BrowserifyやWebpack、jspmなど重要になってきているJavaScript向けのモジュール群について学んでいきましょう。

最後に、これらのツールをCoffeeScripやTypeScript、Babelなどのトランスパイラー(トランスコンパイラー)と組み合わせて使う方法を説明します。

モジュールの歩み

JavaScriptは1995年に登場し、モジュールをネイティブでサポートしているブラウザーはいまだありません。Node.jsとCommonJSは2009年に開発され、大半のnpmパッケージでCommonJSが使われています。

Browserifyは2011年にリリースされた、すべての必要なオブジェクトを1つのJavaScriptファイルにまとめるツールです。クライアントサイドのJavaScriptでnpmパッケージをrequireで読み込ませるために、CommonJSを採用しました。

従来の方法

jQueryなどのライブラリーはグローバルスコープかwindow$を追加します。

window.$ = function() { ... };

ライブラリーを指定するスクリプトを記述し、グローバルオブジェクトを使います。

<script src="jquery.js"></script>
<script>
$(function() { ... });
</script>

それぞれのアプリケーションコードはグローバルスコープが汚くなるのを防ぐために、Appのようなグローバル領域で名前空間が定義されるのが一般的でした。そうしないと、名前が衝突し動作しなくなる以前に、名前が長すぎるのです。

var App = {};
App.Models = {};
App.Models.Note = function() {};

これからの方法

ライブラリーは共通のモジュール形式(ES6モジュール)でオブジェクトをエクスポートします。

export default function $() { ... }

モジュールをローカルスコープにインポートして使います。

import $ from 'jquery';

$(function() { ... });
  • グローバル領域が不要
  • 独立性の高いソースコード
  • npmへのアクセス
  • アプリケーションコード特有の名前空間は不要
  • 必要なときにいつでもモジュールを動的にロード

現在の状況

本当に複雑です。あちこちで以下のようなさまざまなモジュールが使われています。

以下のようなさまざまな形、サイズのアセットをまとめる(バンドリング)ツールがあります。

それから、多くの人が使いたいと思っているトランスパイラもあります。

さらに、モジュールの動的ロードができる、さまざまなライブラリーもあります。

書き連ねたものは、現在使われている人気ツールのほんの一部です。初心者だけでなく専門家でもまだ分からないことが多々あります。またトランスパイルの代価として、たくさんのツールを組み合わせて使うと、結果が異なるという点も注目されています。

2016年のツールをまとめて整理しよう

フロントエンドの開発者はかなり長い間ビルドツールを使用してきましたが、ビルドステップが正規に使われ始めたのはこの2、3年です。SassやCoffeeScriptなどのツールは前処理を主流にしましたが、今やES6には回りを巻き込む勢いがあります。

このツイートの通りです。

ここ数年、GulpとGruntが人気を集めているのは、アセットを流し込むときに一連の変換ができるからです。多くの人々はnpmのツールをそのまま使っていますが、GulpやGruntは高く評価されていて人気があります。詳しくは「Why I Left Gulp and Grunt for npm Scripts(npmスクリプトにGulpとGruntを使う理由)」と「Give Grunt the Boot! A Guide to Using npm as a Build Tool(ビルドツールとして使うnpmガイド)」をご覧ください。

個人的には、アセットパイプラインの構築が長くなることについては気にしていません。私が求めているのは、必要なときに最新のツールを使うことができる最小限の構成ツールです。たとえばSassやAutoprefixer、Babel、Coffeescriptなどの、実装、構成、トランスパイル中も安心できる、適切なモジュールシステムやローダーです。要するに、個々の開発者がアセットパイプラインの作成に何年もかけて、たくさんの人が同時に改造に挑んでいるという状況には、無駄な時間が多く費やされていると思うのです。

コミュニティはBrowserify、Webpack、jspm、Sprockets、Gulpなどツールごとに別れています。別れていることが問題ではありませんが、全員が前に向かって進もうとするときに混乱するのです。

スタート地点を明確にする

以下が、共通認識です。

  • ES2015モジュールは未来にふさわしいJavaScriptのモジュール形式です。
  • Babelは現在、最適なES2015コンパイラです。
  • ネイティブのローダーがブラウザーで使えるようになるにはまだ時間がかかります。Telerikのレポート「The Future of JavaScript:2016 and Beyond(JavaScriptの今後)」ではES2015がモジュールローディングのハードルをクリアして完全にサポートするには2年以上かかると示唆しています。
  • もし今モジュールを使いたいなら、どこかの時点でCommonJSを使う可能性がとても高いです。

最新の動向を知るのに欠かせないJavaScriptバンドラー、BrowserifyやWebpack、jspmを使った最小構成の設定についてみてみましょう。

新しいプロジェクト

mkdir modules-app
cd modules-app
npm init -y
npm install --save-dev browserify webpack jspm
mkdir src
touch src/{entry,lib}.js index.html

自分が使っているテキストエディターでindex.htmlを更新します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

コードを動かすサーバーも必要になります。たとえばlive-serverはライブリロード機能も備えた優れた小さなゼロコンフィグHTTPサーバーです。npm install -g live-serverと記述してグローバルにインストールし、プロジェクトのルートからlive-serverを実行して開始します。

Browserify

すべての関係するオブジェクトをバンドルして、ブラウザーでrequire('modules') を使えるようにするのがBrowserifyです。

src/lib.jsを開き、一番はじめのモジュールを加えます。

var double = function(number) {
  return number * 2;
}

module.exports = {
  double: double
}

src/entry.jsを開き、モジュールをrequireで読み込んで使います。

var lib = require('./lib.js');
console.log(lib.double(2));

package.jsonscripts部分を更新します。

"scripts": {
  "browserify": "browserify ./src/entry.js -o ./bundle.js"
},

このスクリプトをnpm run browserifyで実行します。

Browserifyでプロジェクトルートの中にbundle.jsが作成され、コンソールに4つのファイルが出力されているのを確認してください。Browserifyで何ができるのか、どのようにバンドルが作成されるのかについてもっと知りたい人は、egghead.ioの Browserify入門をお勧めします。

おめでとうございます。これでブラウザー上にモジュールができました。

Browserifyのメリットのもう1つは、モジュールへのアクセスと同様に、作成者がnpmにもアクセスできるという点です。lodashをインストールしてみてみましょう。

npm install lodash --save-dev

src/lib.jsを編集します。

var sum = require('lodash/sum');

var double = function(number) {
  return number * 2;
}
var addFive = function(number) {
  return sum([number, 5]);
}

module.exports = {
  double: double,
  addFive: addFive
}

src/entry.jsを編集し新しい関数addFiveを呼び出します。

var lib = require('./lib.js');
console.log(lib.double(2));
console.log(lib.addFive(2));

もう一度npm run browserifyでバンドルを作成して、ブラウザーで47が表示されているのを確認します。表示されていれば、インポートが成功しlodashのsum関数が使用されたと分かります。

ここまでくれば、すぐにブラウザーでモジュールを使えます。最初に説明したように、以下のような多くのメリットがあります。

  • グローバル領域が不要
  • 独立性の高いソースコード
  • npmへのアクセス
  • アプリケーションコード特有の名前空間は不要

あとで、実行時にモジュールの動的ローディングをしてみましょう。

Webpack

Webpackはモジュールバンドラーです。Webpackは関係するオブジェクトでモジュールを受け取り、静的アセットを生成します。

webpackを呼び出すために、package.jsonに新しいスクリプトを追記します。

"webpack": "webpack ./src/entry.js bundle.js"

npm run webpackでwebpackを実行します。

Webpackはbundle.jsでリライトされて、ブラウザーでの出力はまったく同じになるはずです。

npm run browserifynpm run webpackを実行して、コンパイルされたbundle.jsファイルの違いを調べてみてください。重要なのは、これらのツールが内部でどのような働きをしたかではありません。実装方法は異なりますが、CommonJSで同じコードをどのブラウザーでも表示できる標準のJavaScriptにコンパイルするという、本質的には同じ働きをしていることです。各モジュールはbundle.js内の関数に入れられて、IDが指定され、必要な時に指定されたIDで読み込まれます。

Webpackの機能はまだまだあり、本当に万能なモジュールバンドラーです。Webpackは開発用の優れたツールも提供しています。たとえば、変更されたときに自動で個々のモジュールをリロードしてくれるhot module replacementです。このツールはLiveReloadと似ていますが、ページを更新しなくてモジュールが差し替えられます。

異なるアセットタイプにも使えるローダーは増えています。CSS用のcss-loaderstyle-loaderまであります。CSSをJavaScriptのバンドルにコンパイルし、実行時にページに挿入します。本題から外れましたが、もっと知りたい人は「Webpackを使ってみよう」をご覧ください。

JavaScriptトランスパイラー

今日使われているもっとも人気のあるトランスパイラーを3つ紹介します。「JSにコンパイルする言語」にはとても多くの言語がリストされていますが、紹介する3つでも使いたいものがあるかもしれません。

モジュールバンドラーと合わせたトランスパイラーの使い方の前に、ツールの使い方です。

npm install --save-dev coffee-script typescript babel-cli babel-preset-es2015
touch src/{coffee-lib.coffee,ts-lib.ts,es6-lib.js}

CoffeeScript

coffee-lib.coffeeを編集します。

sum = require 'lodash/sum'

double = (number)-> number * 2
addFive = (number)-> sum([number, 5])

module.exports =
  double: double
  addFive: addFive

備考:CoffeeScriptはモジュールを書く際、CommonJS構文を使います。

coffeeを実行するためにpackage.jsonにスクリプトを追加します。

"coffee": "coffee --output ./dist ./src/coffee-lib.coffee"

npm run coffeeで実行します。

TypeScript

ts-lib.tsを編集します。

/// <reference path="lodash.d.ts" />
import * as _ from 'lodash';

const double = (value: number)=> value * 2
const addFive = (value: number)=> _.sum([value, 5])

export = {
  double,
  addFive
}

備考:TypeScriptは、ES2015モジュール構文とCommonJSを合わせたような、特有のモジュール構文を使います。

tscを実行するために、package.jsonにスクリプトを追記します。

"tsc": "tsc --outDir ./dist ./src/ts-lib.ts"

npm run tscで実行します。

コンパイラーは、TypeScriptファイルではない外部モジュールでの動かし方を把握するのに型の定義(type definition)を必要とするので、lodashが見つからないというエラーメッセージを表示します。そこで定義ファイルを次のように呼び出します。

cd src
curl -O https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/lodash/lodash.d.ts
cd ..
npm run tsc

Babel

es6-lib.jsを編集します。

import sum from 'lodash/sum';

const double = (number)=> number * 2
const addFive = (number)=> sum([number, 5])

export {
  double,
  addFive
}

備考:Babelでは素晴らしいことに、新しいES2015のモジュール構文を使えます。

Babelは使用するプリセットを指定する構成ファイルが必要になります。

echo '{ "presets": ["es2015"] }' > .babelrc

babelの実行のためにスクリプトにpackage.jsonを追加します。

"babel": "babel ./src/es6-lib.js -o ./dist/es6-lib.js"

npm run babelで実行します。

/dist内のファイルには、CommonJSモジュール形式のES5コードが含まれていますが、前述のようにBrowserifyかWebpackとなら完全にうまく動きます。CommonJSでES5にトランスパイルしてからバンドルするか、各段階でその両方を実行するために他のパッケージも使えます。

Browserify向けのトランスパイルとバンドルのプラグインにはcoffeeifytsifybabelifyがあります。

Webpack向けのローダーには異なる言語のモジュールをrequireして読み込むcoffee-loaderts-loaderbabel-loaderがあります。

jspm

jspmはSystemJSを利用した一般的なモジュールローダー用のパッケージマネージャーで、動的ES6モジュールローダーをベースに構築されています。

jspmのアプローチは他と異なり、モジュールローダーのSystem.jsから始まります。System.jsは、ローダーのスペックに依存します。

jspmを使ったプロジェクトをインストールして初期化しましょう。

npm install -g jspm
jspm init

すべての初期設定はそのままで、Babelをトランスパイラーとして使います。ES6形式のモジュールを実行するとBabelを使うようにSystem.jsを設定します。

index.htmlを更新し、System.jsをロードして設定します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="jspm_packages/system.js"></script>
  <script src="config.js"></script>
  <!--<script src="bundle.js"></script>-->
  <script>
   System.import('src/entry.js');
  </script>
</body>
</html>

ブラウザーが少数のリクエストとlodashに404を表示するのは、jspmは初期設定でjspm_packagessディレクトリーからパッケージをロードするためです。

ディレクトリーにlodashをインストールするためにjspm install lodashを実行し、コンソールに47が出力されているか確認しましょう。ここまでに起きていることを整理します。

  • entry.jsファイルがSystem.import('src/entry.js');で動的にロードされます。
  • System.jsはentry.jsをロードし、libを実行時に呼び出します。
  • System.jsはlib.jsをロードし、lodash/sumも呼び出します。

System.jsは直接ES6で動かせるので、ES6を動的に要求して、実行中にコンパイルするためにentry.jsを更新します。

import lib from './es6-lib';
// import lib from '../dist/coffee-lib';
// import lib from '../dist/ts-lib';

console.log(lib.double(2));
console.log(lib.addFive(2));

1つずつ上記のコメントを解除して、CoffeeScriptやTypeScriptのES5にコンパイルされたバージョンをロードする方法もあります。その他に、プリコンパイルされたES5コードを読み込む代わりに、コードをトランスパイルするSystem.jsプラグインを使う方法もあります。

jspmでバンドルを作成するために、package.jsonに最後のスクリプトを追記します。

"jspm": "jspm bundle src/entry bundle.js"

npm run jspmで実行します。

最後に、index.htmlbundle.jsのスクリプトタグのコメントを解除します。ブラウザーは余計なhttpをリクエストすることなく、利用可能なバンドルをロードします。

<script src="bundle.js"></script>

再びWebpackについて

先のWebpackの例は初期設定オプションを使ったもっとも簡単なもので、CommonJSモジュールでentry.jsを1つのバンドルにコンパイルしました。Webpackでもっと特別なことをする場合は、すべてのローダーに対応したカスタム構成ファイルを作成する必要があります。

プロジェクトのルート内にwebpack.config.jsを作成します。

module.exports = {
  context: __dirname + "/src",
  entry: "./entry",
  output: {
    path: __dirname,
    filename: "bundle.js"
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel-loader',
      query: {
        presets: ['es2015']
      }
    },{
      test: /\.coffee$/,
      loader: 'coffee-loader'
    },{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  }
}

バンドルされたファイルのみをロードするためにindex.htmlをもう一度更新します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

Babel、CoffeeScript 、TypeScriptでトランスパイルするためのローダーをインストールします。

npm install --save-dev babel-loader coffee-loader ts-loader

webpackをグローバルにインストールし、引数なしで実行して構成ファイルからバンドルを作成します。

npm install -g webpack
webpack

Webpackはローダーがファイル拡張子で区別するため、entry.jsからES6、CoffeeScript、TypeScriptを自由に使えるので、1つずつコメント解除して試せます。

import lib from './es6-lib.js';
// import lib from './coffee-lib.coffee';
// import lib from './ts-lib.ts';

Webpackでできることはまだまだたくさんありますが、ここで説明した簡単な設定は出発地点としてふさわしいでしょう。

最後に

モジュールについてもっと詳しくなったら、(もしツールの使用が妨げになってなければ)モジュールを使うことで多くの課題が解決でき、アプリケーションが複雑化するのをかなり防げます。もしモジュールを使っていなければ、早速始めてみましょう。アセットパイプラインを構築するのに無駄な時間を費やすことはありません。代わりにちゃんと動く簡単なツール“Just Work”を使いましょう。

Webpackは今のところ圧倒的な破壊力があって、ほとんど何をするにもWebpackの設定でできるでしょう。jspmはバンドルのニーズをすべて満たす素晴らしいツールで、さまざまな形式で動き、開発をスムーズに進められます。Browserifyは依然として確実なオプションです。現代のモジュールビルダーの原型のような存在で、Webpackのかなり優れた特徴(バンドル分割やホットリローディングなど)を取り入れたビジネスモデルが発達しています。System.jsは実行時に特定のモジュールをロードするときには最適です。

1つのプロジェクトで、これまで紹介したツールをすべてを使いたいとは思わないでしょう。必要なときにトランスパイルを使えるように、これらの人気のある3つのオプションを理解しておくことは大切です。単にモジュールを使いたいのであれば、初期設定オプションで、Browserifyかjspm、Webpackを使えばうまくいくでしょう。

ツールはシンプルに、構成は軽く、です。さあ、プログラミングを楽しみましょう。

(原文:Understanding JavaScript Modules: Bundling & Transpiling

[翻訳:和田麻紀子
[編集:Livit

Copyright © 2016, Mark Brown All Rights Reserved.

Mark Brown

Mark Brown

オーストラリア・メルボルン在住。フロントエンドのWeb開発者。有能な人達に囲まれて自分自身が成長しながら、Web設計に喜びを感じています。特にビジュアルプログラミングに興味があり、SVGとCanvasを使ったプログラミングに夢中。

Loading...