おはこんばんちは!! 尾藤 a.k.a. BTOです。
UUUMでは業務の効率改善に kintone を導入しようとしているのですが、kintone の機能を拡張するには Javascript でプログラムを書く必要があります。 そこで、Javascript の開発環境を構築したので、何をやったのかをまとめてみました。
具体的には、 BabelでES6で書いて、webpackでビルドして、mochaでテスト書いて、power-assertでassertの出力を見やすくして、karmaで複数ブラウザのテストを自動化して、カバレッジを出力するようにしました。 お前は何を言っているんだと思うかもしれませんが、僕も何を言っているのかわかりません。
Babel
言わずと知れた ES6 用のトランスパイルツールです。 ご存知の方も多いと思うので、詳細は割愛しますね。
Babel の導入は npm でインストールするだけ
% npm install babel babel-preset-es2015 --save-dev
babel-preset-es2015 は ES6(別名ES2015)用のプリセットです。 これでES6で記述できるようになります。
Babelは .babelrc
というファイルがあると、その設定を読み込んでくれます。
ここに es2015
のプリセットを使用するように設定します。
{ "preset": ["2015"] }
Polyfill
Babel でトランスパイルすることで、古いブラウザでも動作する Javascript を出力できるようになったのですが、古いブラウザの標準オブジェクトの中には ES6 に対応していないものがあります。
そのために標準オブジェクトを拡張する Polyfill
を導入します。
% npm install babel-polyfill --save-dev
わざわざ Polyfill が別ライブラリに分かれているのには、ちゃんと意味があります。 標準オブジェクトを拡張することは、既存の環境と互換性を失うことになります。 互換性を失うのは危険を伴うので、別ライブラリに分けているのです。 Polyfill を導入するときは、標準オブジェクトを拡張していることをちゃんと認識したうえで使う必要があります。
babel-runtime
上記のように Polyfill には標準オブジェクトを拡張するという問題があるので、それをしないようにする実装もあります。
それが babel-runtime
です。
babel-runtime は標準オブジェクトの対応していない機能を、その場で別のコードにトランスパイルして解決します。
Polyfill を使うのか babel-runtime を使うのかは、決めの問題だと思うので、どちらかを導入すれば良いと思います。 Polyfill を導入しているケースが多いように思われますので、個人的には Polyfill でいいんじゃないかと思っています。
webpack
ブラウザで実行するには、複数に分けられたjsファイルを1つにして読み込まないといけません。 複数ファイルを読み込むことももちろんできますが、依存関係の解決とか大変ですよね。 そのためにビルドツールを使うわけですが、今回は webpack を採用しました。
webpack だと browserify と違って複数ファイルを生成するのが簡単にできるそうです。 (browserify使ったことないので、詳細はわかりません)
% npm install webpack babel-loader --save-dev
インストールすれば、すぐに webpack が使えます。
簡単ですね。
webpack は様々形式のデータを扱うのに、プラグイン形式で対応しており、babel を使う場合は babel-loader
を使います。
これだけだと webpack がどのようにファイルをビルドするかがわからないので、設定ファイルを書きます。
webpack.config.js
webpack は設定ファイルを webpack.config.js で書くのですが、どうせなら設定ファイルも ES6 で書きたいですよね。
これをやるのはとても簡単で、bable-register
をインストールすれば、 webpack.config.babel.js
というファイルで設定を ES6 で書くことができます。
% npm install babel-register --save-dev
interpret
babel-register をインストールするだけで、なぜ ES6 で設定ファイルを書くことができるようになるのでしょうか。
そこには interpret
というライブラリの存在があります。
interpret の仕組みはとても簡単で、設定された拡張子に対して動的な前処理を適用するだけです。
interpret の方で、 .babel.js
という拡張子に対しては、 babel-register
で変換して処理するという風に定義されていますので、 webpack.conf.babel.js
でも問題なく処理できるというわけです。
つまり、interpret に対応しているツールに関しては、babel-register をインストールすれば、もれなく ES6 にも対応できるようになります。
JSのように逐次解釈して実行する言語をインタプリタ言語と言いますが、 interpret
はインタプリタと同じ意味(品詞は違う)の単語で、とてもわかりやすい名前ですね。
webpack.config.babel.js
webpack.config.babel.js には次のような設定を書きました。
export default { entry: { main: './src/main.js', kintone: './src/kintone.js', }, output: { filename: '[name].js', path: `${__dirname}/web`, }, module: { loaders: [{ test: /\.js$/, include: [ `${__dirname}/src`, `${__dirname}/test`, ], loader: 'babel', }], }, };
entry
には、どのファイルをビルドするのかを指定します。
ここでは、2つ(main, kintone)を指定しています。
このように、複数のファイルを指定できるのが、webpackの良いところですね。
output
には、どのように出力するかを定義します。
ここでは web ディレクトリにビルド結果を出力するようにしています。
module.loader
でローダの指定をします。
ここでは、 .js
のファイルに対して、 babel-loader
を使用して babel でトランスパイルするように指定しています。
include
ではローダの対象となるファイルを指定しています。
相対パス地獄を解決する
相対パス地獄は ES6 だけでなく他の言語でも見られますが、みなさんも
require('../../../../foo.js');
のような相対パスでのモジュールの読み込みで苦い思いをした経験があるのではないでしょうか。 これは webpack の機能を使うことで解決できます。
webpack の設定の resolve.root
でディレクトリを指定すれば、そのパスを起点としたモジュールが読み込めるようになります。
webpack.config.babel.js に記述を追加します。
resolve: { root: [ `${__dirname}/src`, ], },
この設定だと、例えば src/uuum/foo.js
は、
import from 'uuum/foo';
のような記述で読み込みができるようになります。
ここで注意しないといけないのが、この機能は webpackが提供している機能
だというところです。
なので、 babel 単体ではうまく処理することができませんので、 webpack を通さない処理だと当然使用することができません。
例えば、 webpack.config.babel.js は、 webpack を通さずに直接 babel でトランスパイルするので、もし src/
以下のファイルを読み込んでいた場合は、import に失敗します。(NODE_PATHを設定すれば大丈夫)
ソースマップ
Babel で ES6 で記述できるようになったのは良いのですが、ブラウザ上で実際に実行されるのは、トランスパイルされた Javascript なので、ブラウザ上でエラーが出た場合に元のソースコードのどこでエラーが発生したのかがわかりません。 それがわかるようにするためにソースマップを生成しておくと、オリジナルのソースコードのどこに対応するのかがわかるようになるので、デバッグが楽になります。
ソースマップを追加するのは devtool
に inline-source-map
を設定するだけです。
devtool: 'inline-source-map',
名前の通り、この設定を追加すると生成されるファイルの中にソースマップが設定されます。 他にもソースマップの生成方法はいろいろありますが、詳しくはマニュアルをご参照ください。
ここでは1つ課題があって、本番環境ではソースマップは必要ないんですが、現状だと本番用でもソースマップが追加されてしまっています。
なので、本番環境と開発環境でわけないといけないのですが、 webpack.config.prod.babel.js
の用に環境ごとに設定ファイルを分けるのが一般的なようですね。
これはおいおい解決していこうと思ってます。
Mocha
テストツールには Mocha
を採用しました。
Mocha はシンプルな TDD/BDD 対応のテストツールです。
テストに必要な基本的な部分のみ実装していて、 assertion ですら外部のライブラリを使用します。
そのため時流に乗って適切なライブラリと組み合わせることができ、その点が気に入っています。
Mocha はインストールするだけで、すぐに使えるようになります。
% npm install mocha --save-dev
テストディレクトリは標準の test/
にします。
test/mocha.opts
に Mocha のコマンドラインのオプションを書いておくと、自動的に適用されるようになります。
こうしておけば、 mocha
コマンドを叩くだけでテストが実行できるようになります。
mocha.opts には次のような設定を書きました。
--compilers js:babel-register --recursive --require babel-polyfill --require src/kintone
1行目の --compilers js:babel-register
を指定しておくと、テストファイルを読み込む時に、 babel-register
で前処理してくれるようになります。
つまり、テストコードも ES6 で記述できるようになります。
--recursive
はその名の通り。
あとは Polyfill が必要なので読み込んでいるので、このプロジェクトは kintone ようなので、それ用のライブラリを読み込んでいます。
mocha コマンドは次のように実行します。
NODE_ENV=test NODE_PATH=$NODE_PATH:$PWD/src mocha
テストなので、 NODE_ENV に test をセットします。
NODE_PATH は先程の webpack のところでやった、相対パス地獄を回避するためにライブラリの読み込みパスを指定しないといけません。
power-assert
Mocha は assertion のライブラリを持っていないと書きましたが、 assertion には power-assert を採用しました。 power-assert を使うと、 assert で失敗した時の出力が超絶にわかりやすくなります。
どのような出力になるかは本家をご参照ください。(power-assert)
power-assert をインストールします。
% npm install power-assert babel-plugin-espower --save-dev
power-assert はそのままでは babel に対応していないので、 babel-plugin-espower
も導入します。
.babelrc に次の設定を追加します。
"env": { "test": { "plugins": [ "espower", ] } }
power-assert はテスト環境だけで、動作すれば十分なので、テスト環境のみ espower を追加するようにしています。
Karma
ごめん、書くの疲れた。 また今度書きます。
感想
相変わらず JS 界はツール類が多くて大変というのが正直な感想です。 ブラウザの違い、バージョンの違い、サーバ・クライアントの違いと複雑な条件が絡み合っているので仕方がないとは思うのですが。。。
しかし、このあたりの仕組をちゃんと把握したうえで使いこなすのは、本当に大変だと思うし、今の環境も1年後には古くなっているのを考えると少し憂鬱な気持ちになってしまいます。
とはいえ、現環境ではまずまずの状態を作れたのではないかと思うので、しばらくはこの環境で開発を進めていこうと思っています。
まとめ
UUUMでは富士登山(任意参加!!)ができます。
エンジニアで参加したのは僕だけでしたがw