はじめに
jsライブラリを作成するにあたりwebpack周りを調べたメモです。
webpackの記事はググれば山のように見つかるのですが、
webpackは少し前に触って以来で、いつの間にかwebpack2がリリースされ記述方法が若干変わっており、それまで参考にした記事はwebpack1の書き方だったり、入門者にはなかなか戸惑う点がありました。
というわけで、またそういったノイズ記事を増やしてしまうことについてご了承ください。
2017年4月時点の内容ですので、近い将来また変わってくると思います。
ゴール
今回、やりたいことは以下です。
- jsはBabelでes2015ベースで書く
- jsはminifyさせる
- cssはSassで書いて、jsにバンドルさせる
- unitテストを書く
- lintを入れる
環境
OS: macOS v10.12.4
node: v6.10.2
npm: v4.6.1
サンプル
最終的に出来たサンプルプロジェクトはこちらです。
https://github.com/darquro/webpack_sample
プロジェクト構造は以下のようになりました。
.
├── dist
│ ├── webpack_sample.js
│ ├── webpack_sample.js.map
│ └── webpack_sample.min.js
├── html
│ └── index.html
├── src
│ ├── css
│ │ └── style.scss
│ └── js
│ ├── friend.js
│ ├── main.js
│ └── person.js
├── test
│ ├── friend_test.js
│ └── mocha.opts
├── .babelrc
├── .eslintrc
├── package.json
├── postcss.config.js
└── webpack.config.js
動作確認はこちらで確認できます。
https://darquro.github.io/webpack_sample
ボタンをクリックイベント(js)とボタンのスタイル変更(css)だけ確認できるページです。
各npmパッケージのインストール手順の説明は省いていますので、 package.json
から必要なものを確認してください。
"devDependencies": {
"autoprefixer": "^6.7.7",
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.0.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-power-assert": "^1.0.0",
"babel-register": "^6.24.1",
"concurrently": "^3.4.0",
"css-loader": "^0.28.0",
"eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.1.3",
"eslint-plugin-import": "^2.2.0",
"mocha": "^3.3.0",
"postcss-loader": "^1.3.3",
"postcss-smart-import": "^0.6.12",
"power-assert": "^1.4.2",
"precss": "^1.4.0",
"style-loader": "^0.16.1",
"webpack": "^2.4.1",
"webpack-dev-server": "^2.4.4"
}
webpack
jsやcss、イメージなどをビルドしてまとめてくれるツール。デファクトスタンダードとなっている。
https://webpack.js.org/
webpack.config.js
に色々設定を記載します。
今回webpack 2.4.1を使用していますので、書き方がwebpack 1とは一部変わってます。
作成した設定はこのようになりました。
const Webpack = require('webpack');
const path = require('path');
const MINIFY = process.env.NODE_MINIFY === '1';
module.exports = {
context: path.join(__dirname, '/src/js'),
entry: {
webpack_sample: './main.js',
},
output: {
path: path.join(__dirname, '/dist'),
filename: MINIFY ? '[name].min.js' : '[name].js',
publicPath: '/assets/',
},
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules',
],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {},
},
},
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
'postcss-loader',
],
},
],
},
devtool: 'source-map',
plugins: MINIFY ? [
new Webpack.optimize.UglifyJsPlugin(),
] : [],
devServer: {
contentBase: 'html',
port: 8080,
},
};
順番に設定内容を説明していきたいと思います。
公式ドキュメントはこちら
context
context: path.join(__dirname, '/src/js'),
contextを指定すると、ビルドする対象のルートディレクトリを指定できます。
今回では src/js
が起点となるディレクトリなので、このように設定しました。
entry
entry: {
webpack_sample: './main.js',
},
entryには起点となるファイルの名前とパスを指定します。
今回のプロジェクトは webpack_sample
という名前にしましたので、keyにwebpack_sample
を指定し、パスはcontextで指定した場所から相対パスで起点となる main.js
を指定しています。
output
output: {
path: path.join(__dirname, '/dist'),
filename: MINIFY ? '[name].min.js' : '[name].js',
publicPath: '/assets/',
},
outputはビルドして出力先を指定します。
パスとしては dist
ディレクトリに出力し、ファイル名は、 webpack_sample.js
とminifyした webpack_sample.min.js
を出力するようにしました。
MINIFY
の変数については、npmのコマンドに、NODE_MINIFY=1 webpack
を指定させ、一つの設定を流用しつつ、minifyする箇所だけ設定を分岐するようにしています。
publicPathについては、テスト時にローカルサーバーを立てて確認する際、htmlから参照できるパスを assets/webpack_sample.js
というパスで参照出来るようにしています。
resolve
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules',
],
},
resolveを指定すると、jsでimportする際、相対パスで ../../foo/bar.js
といったような面倒な指定に陥ることを防げます。
今回では src
ディレクトリが起点となっていますので、jsからは src
配下のディレクトリからのパスで指定することができるようになります。
node_modules
は消してしまうとnpmライブラリが見れなくなるらしいので必ず指定します。
module.rules (js)
ここにloader系を指定します。
webpack1では、rulesではなくloadersという書き方ですので、webpack1の記事を見てるとハマります。
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {},
},
},
jsは今回Babelだけですので、babel-loaderを指定します。
書き方はbabel-loaderのドキュメントそのままです。
https://github.com/babel/babel-loader
optionに何も書いていないのは .babelrc
に書いているからです。
module.rules (scss)
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
'postcss-loader',
],
},
css(scss)側は、 style-loader
、 css-loader
、 postcss-loader
の3つを使用しています。
書き方については、postcss-loaderのドキュメントのままです。
https://github.com/postcss/postcss-loader
webpackでSassのコンパイルを検索すると sass-loader
を使用する記事が幾つか見つかりますが、
ベンダープレフィックスをつける autoprefixer
が postcss-loader
を使用する必要があり、移行したという記事がありましたので、 postcss-loader
を使用しました。
devtool
devtool: 'source-map',
source-mapファイルを出力する設定です。
source-map
以外にも色々と指定があります。
https://webpack.js.org/configuration/devtool/#devtool
plugins
plugins: MINIFY ? [
new Webpack.optimize.UglifyJsPlugin(),
] : [],
minifyする設定です。 MINIFY
のフラグでそのままビルドしたjsとminifyしたjsの両方を出力するようにしています。
minifyするだけならこの指定をしなくても $ webpack -p
の指定だけでminifyしてくれます。
将来的にminifyのオプションが何か増えるかもしれないという気もしたので、この指定にしました。
devServer
devServer: {
contentBase: 'html',
port: 8080,
},
webpack-dev-server
コマンドでローカルサーバーを立ち上げるときの設定です。
ルートでhtmlディレクトリを指定しました。
Babel
Babelの設定は webpack.config.js
にまとめることもできますが、 .babelrc
ファイルに記載するようにしました。
{
"presets": ["es2015", "babel-preset-power-assert"]
}
presetsで es2015
と babel-preset-power-assert
を指定しています。
babel-preset-power-assert
についてはunitテストで後述します。
PostCSS
PostCSSの設定もBabelと同じく webpack.config.js
にまとめることもできますが、 postcss.config.js
に記載しました。
/* eslint global-require: off */
module.exports = {
plugins: [
require('postcss-smart-import')({ /* ...options */ }),
require('precss')({ /* ...options */ }),
require('autoprefixer')({ /* ...options */ }),
],
};
Sassの記法を書けるようにするために、3つのプラグインを使用しています。
それぞれの説明については割愛します。
-
postcss-smart-import
: PostCSSで@import記法を使うためのプラグイン -
precss
: PostCSSで変数定義などを使うためのプラグイン -
autoprefixer
: PostCSSでベンダープレフィックスを使うプラグイン
sampleコード
さて、jsライブラリとしての基本的な設定周りが済んだので、今回のサンプルプロジェクトのコードについて説明します。
まずはPersonクラスとそれを継承したFriendクラスを用意しました。
class Person {
constructor() {
this.name = 'Person';
}
getName() {
return `This is ${this.name}`;
}
}
export default Person;
import Person from 'js/person';
class Friend extends Person {
constructor(name) {
super();
this.name = name;
}
callName() {
alert(`This is ${this.name}`);
}
getName() {
return `This is ${this.name}`;
}
}
export default Friend;
起点となるmain.jsは以下です。
import 'css/style.scss';
import Friend from 'js/friend';
window.Friend = Friend;
また、Sassファイルは以下です。
$btn-width: 300px;
$btn-hight: 150px;
button {
&.btn {
width: $btn-width;
height: $btn-hight;
}
}
テストのindex.htmlには、以下のようにボタンを配置して、そのクリックイベントとスタイルに今回のライブラリを使用するようになっています。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="assets/webpack_sample.js"></script>
</head>
<body>
<button class="btn" id="btn">click me</button>
<script>
var friend = new Friend('Taro');
document.getElementById('btn').onclick = function(){
friend.callName();
};
</script>
</body>
</html>
ビルドは、 webpack
コマンドで、サーバー起動は webpack-dev-server
を使用します。
そのままターミナルから実行するには、webpackのパスが通ってないので、
$ `npm bin`/webpack
とする必要がありますが、package.jsonのscriptにコマンドを登録しているので、それぞれ、
$ npm run build
$ npm run start
で出来るようにしました。
"scripts": {
"build": "webpack && NODE_MINIFY=1 webpack",
"start": "webpack-dev-server",
},
webpack-dev-server
はファイルの更新も検知してくれます。
unitテスト
unitテストはテストフレームワークにmocha、アサーションライブラリにpower-assertを使用しました。
testディレクトリ配下がテストコードです。
まず mocha.opts
を作成し、設定を記載します。
--recursive
--compilers js:babel-register
recursive
の指定をすると再帰的にテストファイルを検索してくれるようです。
compilers js:babel-register
はmochaでes2015で書くためのパッケージです。
また、power-assert側もes2015を解釈させるために、 babel-preset-power-assert
が必要で、presetsを .babelrc
で記載していました。
import assert from 'assert';
import Friend from 'js/friend';
describe('test Friend', () => {
it('getName()', () => {
const friend = new Friend('Taro');
assert.equal('This is Taro', friend.getName());
});
});
テストコードはFriendクラスのテストを一つ書きました。
package.jsonにtestコマンドを追加し、mocha側からのパスを解決させています。
"test": "NODE_PATH=$NODE_PATH:$PWD/src mocha"
lint
lintは以下のパッケージを使用しています。
- eslint
- eslint-plugin-import
- eslint-config-airbnb-base
eslint-config-airbnb-baseはAirbnbのスタイルガイドをベースに少しカスタマイズをしました。
eslintの設定は .eslintrc
ファイルに記述します。
{
"extends": "airbnb-base",
"env": {
"es6": true,
"node": true,
"browser": true
},
"rules": {
"import/no-unresolved": [2, { ignore: ["^js|^css"] }],
"import/no-extraneous-dependencies": [0],
"import/extensions": [0],
},
"globals": {
/* MOCHA */
"describe": false,
"it": false,
"before": false,
"beforeEach": false,
"after": false,
"afterEach": false
}
}
"browser": true
とすることで、ブラウザのもつAPIをエラーにさせないようにしたり、
npmパッケージにないものもimportしてもエラーにならないように、今回のsrcディレクトリ配下は除外するようにしています。
また、mochaのAPIも除外しています。
ここだけでは拾いきれないファイル個別の設定は、ファイルの先頭にコメントを入れるなどで、eslintの固有設定もできます。
postcss.config.jsはglobal-requireをエラーにしないようにしています。
/* eslint global-require: off */
Atom
私はAtomをメインに使ってるので、Atom側のパッケージも入れます。
- Linter
- linter-eslint
をインストールすればeslintが動いてくれます。
さいごに
ゼロから環境構築まで色々調べながらでとても疲れました。
以上です!