ES2015(ES6)の対応が進みつつある今日この頃。ただブラウザや環境によって実装状況にバラつきがあって、使用できる機能・そうでない機能はcompatibility tableを確認しないと分からない状況です。
環境を気にせずに完全なES2015でコーディングしつつES5にトランスパイルして配布すれば、スムーズに作業が進むのではないかと思って環境構築を試してみました。
参考 ECMAScript6 compatibility table
http://kangax.github.io/compat-table/es6/
こういう環境を作りたい
- 依存はnpmでインストール
- JavaScript(ES2015)でコーディング
- webpackでビルドする
- ビルドプロセスでbabelが呼ばれてES5にトランスパイルされる
- テストはkarma & jasmine
- ソースコードを監視してローカルでブラウザをリロードして欲しい
- ソースコードを監視してテストを実行して欲しい
npm https://www.npmjs.com/
webpack https://webpack.github.io/
babel https://babeljs.io/
karma https://karma-runner.github.io/1.0/index.html
jasmine http://jasmine.github.io/
前提
node & npmがインストールされているものとします。
使用したバージョンはnode v6.0.0
npm v3.8.6
です。
また動作確認のブラウザはChrome v53.0
を使用しています。
1) package.jsonを作る
$ mkdir my_project; cd $_;
$ npm init
2) webpackを動かす
インストール
ローカルサーバーも一緒にインストールしておきます。
$ npm i -D webpack webpack-dev-server
補足 -Dは
devDependencies
に保存するオプションです。
必要なファイルの準備
エントリポイント(最初に実行されるファイル)を作ります。
$ mkdir src
$ echo 'console.log("test");' > src/index.js
設定ファイルを作ります。
// webpack.conf.js
module.exports = {
entry: './src/index.js',
output: {
path: './dest',
filename: 'bundles.js'
}
};
補足 この設定では
index.js
をエントリポイントとして読み込み、require
などで参照された別モジュールを全てまとめてdest/bundles.js
として出力します。
ブラウザで動作確認するためのhtmlファイルを作ります。
// index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>test</title>
<script src="dest/bundles.js"></script>
<!--webpackで出力したファイルを参照-->
</head>
<body>
<h3>test</h3>
</body>
</html>
ローカルサーバーを起動するためのスクリプトを追加します。
// package.json
"scripts": {
"build": "webpack",
"server": "npm run build; webpack-dev-server"
...
スクリプトの実行
現在のフォルダ構成はこのようになっています。
├── index.html
├── package.json
├── node_modules
├── src
│ └── index.js
└── webpack.config.js
スクリプトを実行してみましょう。
$ npm run server
> test@1.0.0 server /Users/.../test
> npm run build; webpack-dev-server
> test@1.0.0 build /Users/.../test
> webpack
Hash: ...
Version: ...
Time: ...
http://localhost:8080/webpack-dev-server/
スクリプトを実行するとビルド結果が表示され、URLが記載された部分があります。ここをコピーしましょう。
ブラウザで開く
ビルド結果のURLは自分でブラウザに入力して開きます。
動作を確認
ブラウザのコンソールウインドウを開くとtest
が出力されます。これはindex.js
に書いたconsole.log('test');
が実行された結果です。
Sourcesタブを開くとビルドした後のファイルを見る事ができます。
index.js
には1行しか書いていませんが、webpackは依存を含めた全てのJavaScriptをbundles.js
として出力するため、いろいろな処理が追加されます。
ブラウザでの動作を確認したらnpm run
コマンドはCtrl+Cで終了しましょう。
3) ES2015の構文を書いてみる
サンプルで書いたconsole.log()
ではトランスパイルしても何も変わらないので、ES2015の構文を使ったサンプルに書き換えてみます。
// src/index.js
let greet = require('./greet');
console.log(greet.greeting('hoge'));
// src/greet.js
class Greet {
greeting(name) {
return `Hello ${name}`;
}
}
module.exports = new Greet();
再度ローカルサーバーを実行します。
$ npm run server
今回はコンソールウインドウにHello hoge
が出力されます。
Sourcesタブでビルドされたファイルを見ると、ES2015の構文がそのまま出力されている事が分かります。
たまたま今見ているブラウザではES2015のclass構文に対応しているようですが、対応していないブラウザで同じページを開くとエラーになりますよね?
4) babel
ES2015からES5にトランスパイルするためにbabelをインストールします。
インストール
$ npm i -D babel-core babel-loader babel-preset-es2015
$ npm i babel-polyfill
補足 polyfillはES2015をサポートしていない環境で代替となるものに置き換えてくれる機能です。ビルドしたbundles.jsとは別に必要になるか判断が付かなかったのでdependenciesに入れました。
設定
webpackのモジュールローダーとしてbabelを指定します。
// webpack.config.js
module.exports = {
...
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
}
.babelrc
を書いておくとbabelの実行時に読み込んでくれます。
どのフォーマットのファイルをトランスパイルするかを指定します。
$ echo '{ "presets": [ "es2015" ] }' > .babelrc
スクリプトの実行
現在のフォルダ構成はこのようになっています。
├── .babelrc
├── dest
│ └── bundles.js
├── index.html
├── package.json
├── node_modules
├── src
│ └── index.js
└── webpack.config.js
スクリプトを実行してみましょう。
$ npm run server
動作確認
再びブラウザのSourcesを見ると、今度はES5の構文に置き換わっています。これで(相当古いものでない限り)ほとんどのブラウザで実行できそうです。
5) ローカルサーバーをもうちょっと便利に
sourcemap
// package.json
"scripts": {
"build": "webpack -d"
webpackのオプションに-d(debug)
を指定すると、ビルド時にソースマップのファイルを出力してくれます。するとコンソールウインドウのSourcesタブにwebpack://
が現れ、ビルド前の元のソースを見る事ができます。(ブレークポイントも設定できます、便利。)
ソースコードを監視してローカルでブラウザをリロードして欲しい
これをwebpack-dev-server
でやるのが難しかったのでbrowser-sync-webpack-plugin
に変更します。
$ npm i -D browser-sync-webpack-plugin browser-sync
webpackを-w(watch)
モードで呼び出す必要があるのでserver
のスクリプトを以下のように書き換えます。
// package.json
"scripts": {
"server": "npm run build -- -w"
webpackのプラグインとして動作するように設定を追加します。
// webpack.config.js
var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
module.exports = {
...
plugins: [
new BrowserSyncPlugin({
host: 'localhost',
port: 3000,
server: {
baseDir: ['./']
}
})
]
もう一度スクリプトを実行してみます。
今度はブラウザのURLを自分で入力する事なく、自動的にページが開くはずです。
$ npm run server
この状態でソースを何か変更してみましょう。
// src/index.js
console.log(greet.greeting('fuga'));
ビルドが再実行され、ブラウザも自動的にリロードされます。
browser-sync-webpack-plugin
にはブラウザのリロード以外にも、ChromeとFireFoxを開いて片方のsubmitボタンを押したらもう一方のブラウザも同期した動作をさせる、などの便利な機能があるようです。
browser-sync-webpack-plugin
https://www.npmjs.com/package/browser-sync-webpack-plugin
6) テスト
インストール
$ npm i -D jasmine-core karma karma-chrome-launcher karma-jasmine karma-webpack karma-mocha-reporter karma-phantomjs-launcher
スクリプトの追加
npm run test
でテストが実行されるようにスクリプトを追加します。
// package.json
"scripts": {
...
"test", "karma start"
}
webpackの設定を分割
テスト実行時も開発時と同じように、webpackからbabelを呼び出してES2015からES5にトランスパイルします。ただ現在のwebpack.conf.js
はbrowser-sync-webpack-plugin
の設定を書いているため、これだとテストの度にブラウザが起動してしまいます。
そこでwebpackの設定をテスト用・開発用それぞれに分ける事にします。
- webpack.config.js --- 共通の設定
- webpack.test.config.js --- テスト用
- webpack.dev.config.js --- 開発用
共通の設定を読み込んでからそれぞれの環境に合わせた設定をマージするためwebpack-merge
を使用します。
$ npm i -D webpack-merge
webpack.conf.js(共通の設定)
module.exports = {
entry: './src/index.js',
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
}
};
webpack.test.config.js(テスト用)
const webpackMerge = require('webpack-merge');
const config = require('./webpack.config.js');
module.exports = webpackMerge(config, {
});
webpack.dev.config.js(開発用)
var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const webpackMerge = require('webpack-merge');
const config = require('./webpack.config.js');
module.exports = webpackMerge(config, {
output: {
path: './dest',
filename: 'bundles.js'
},
plugins: [
new BrowserSyncPlugin({
host: 'localhost',
port: 3000,
server: {
baseDir: ['./']
}
})
]
});
karmaの設定
webpackでビルドしてからテストを実行するように設定します。
// karma.conf.js
var webpackConfig = require('./webpack.test.config.js');
module.exports = function(config) {
config.set({
basePath: './src',
frameworks: ['jasmine'],
files: [ { pattern: './spec.js', watched: false } ],
exclude: [],
preprocessors: {
'./spec.js': ['webpack']
},
webpack: webpackConfig,
webpackServer: { noInfo: true },
reporters: ['mocha'],
mochaReporter: {
output: 'minimal'
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['PhantomJS'],
singleRun: true,
concurrency: Infinity
})
}
files
で指定したspec.js
はまだ存在しません。このファイルはテスト実行時に*.spec.js
というネーミングを持つファイルを全て読み込むために作成します。
// src/spec.js
var testsContext = require.context("./", true, /\.spec\.js$/);
testsContext.keys().forEach(testsContext);
補足
context()
の2番目の引数はサブディレクトリ内も再帰的に検索する事を意味します。
テストを書く
簡単なテストを書いてみましょう。src/greet.js
に挨拶を返すGreet
クラスが存在するので、挨拶の結果を検証します。
// src/greet.spec.js
describe('greet', () => {
var greet = require('./greet');
it('test', () => {
expect(greet.greeting('abc')).toEqual('Hello abc');
});
});
現在のフォルダ構成はこのようになっています。
├── .babelrc
├── dest
│ └── bundles.js
├── index.html
├── karma.conf.js
├── node_modules
├── package.json
├── src
│ ├── greet.js
│ ├── greet.spec.js
│ ├── index.js
│ └── spec.js
├── webpack.config.js
├── webpack.dev.config.js
└── webpack.test.config.js
テストの実行
テスト用のスクリプトを実行してみましょう。
$ npm run test
> test@1.0.0 test /Users/.../test
> karma start
START:
23 10 2016 22:28:52.592:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
...
Finished in 0.006 secs / 0.002 secs
SUMMARY:
✔ 1 test completed
1つのテストが成功しkarmaが終了します。
ソースコードを監視してテストを実行して欲しい
karmaのオプションで指定します。
// package.json
"scripts": {
"test": "karma start --auto-watch --no-single-run"
補足 karma.confにもオプションと同じ設定がありますが、必要でない時にもファイル監視が実行されてしまう可能性があります。監視したくない時、例えば
package.json
でtest:nowatch
スクリプトを定義すれば柔軟に対応ができます。
sourcemap
ローカルサーバーでも出てきたsourcemapですが、karmaでも使用する事ができます。
$ npm i -D karma-sourcemap-loader
karmaのpreprocessors
にsourcemap
を指定します。
// karma.conf.js
module.exports = function (config) {
...
preprocessors: {
'./spec.js': ['webpack', 'sourcemap'] // sourcemapを追加
},
webpackのビルドにsourcemap
の情報を含めます。
// webpack.test.config.js
module.exports = webpackMerge(config, {
devtool: 'inline-source-map' // この行を追加
});
テストが落ちるようにして結果を確認しましょう。
// src/greet.spec.ts
it('test', () => {
expect(greet.greeting('unknown')).toEqual('Hello abc'); // 'unknown'に変更
});
greet.spec.js
の行数が出力されるようになりました。
$ npm run test
FAILED TESTS:
greet
✖ test
PhantomJS 2.1.1 (Mac OS X 0.0.0)
Expected 'Hello unknown' to equal 'Hello abc'.
webpack:///src/greet.spec.js:5:46 <- spec.js:110:47 <--- これ
loaded@http://localhost:9876/context.js:151:17
おわり
webpack自体あまり使った事がないので、試行錯誤で書いてみた設定です。プロダクション用やCSSを含めたビルドについては全く書いていないので、不完全な状態だと思います。
ここ数年JavaScriptをとりまく環境は進歩が激しいですね。私と同じように時代について行けず何となくwebpackとかbabelとか億劫になっている方がチュートリアル的に一歩ずつ進められて、終わったあとに何となく「分かったような、気がする...」の気分を感じていただけたらと思います。
今回使用したソース:
https://github.com/ringtail003/babel-with-webpack