webpackを使っていたプロジェクトをFuseBoxに移行してみました。
バンドル時間が超速くなり、バンドルファイルサイズが超小さくなりました。
package.json
package.jsonの比較です。
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.23.1",
"babel-eslint": "^8.0.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.22.0",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.23.0",
"css-loader": "^0.28.7",
"eslint": "^4.6.1",
"eslint-loader": "^1.6.3",
"eslint-plugin-flowtype": "^2.34.1",
"eslint-plugin-react": "^7.3.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.2",
"flow-bin": "^0.54.1",
"node-sass": "^4.5.0",
"sass-loader": "^6.0.3",
"style-loader": "^0.18.2",
"url-loader": "^0.5.8",
"webpack": "^3.4.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.23.1",
"babel-eslint": "^8.0.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.23.0",
"eslint": "^4.6.1",
"eslint-plugin-flowtype": "^2.34.1",
"eslint-plugin-react": "^7.3.0",
"flow-bin": "^0.54.1",
"fuse-box": "^2.2.31",
"node-sass": "^4.5.0",
"uglify-js": "^3.1.1"
},
FuseBoxは自パッケージ内のプラグインで大体のバンドルを済ませることができます。
webpackのようになんとかloader系をいちいちインストールしなくてもよいので、package.jsonはかなりスッキリしました。
設定ファイル
もともとのwebpackの設定webpack.config.dev.jsと、FuseBoxの設定fuse.jsです。
だいたい同じことを実現できていると思いますが、ESLint関連だけはnpm scriptsからの実行に切り替えました。
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
module.exports = {
devtool: 'inline-source-map',
entry: {
bundle: './src/javascripts/index.js',
},
output: {
path: path.join(__dirname, '../../public/javascripts'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['eslint-loader'],
enforce: 'pre'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['react', 'es2015', 'flow'],
}
},
{
test: /\.(css|scss)$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(jpg|png)$/,
use: ['url-loader']
}
]
},
resolve: {
extensions: ['.js', '.jsx']
},
plugins: [
new ExtractTextPlugin('[name].css'),
new webpack.LoaderOptionsPlugin({
options: {
eslint: {
test: /\.jsx?$/,
configFile: path.join(__dirname, '../../.eslintrc'),
cache: false
}
}
})
],
};
const {
FuseBox,
CSSPlugin,
SassPlugin,
BabelPlugin,
QuantumPlugin,
WebIndexPlugin,
ImageBase64Plugin,
JSONPlugin,
Sparky
} = require('fuse-box')
let fuse, app, vendor, isProduction
Sparky.task('config', () => {
fuse = new FuseBox({
homeDir: 'src/',
sourceMaps: !isProduction,
hash: isProduction,
output: 'public/$name.js',
target: 'browser',
experimentalFeatures: true,
plugins: [
BabelPlugin({
sourceMaps: !isProduction,
presets: ['es2015', 'react', 'flow'],
}),
CSSPlugin(),
[SassPlugin(), CSSPlugin()],
ImageBase64Plugin(),
JSONPlugin(),
WebIndexPlugin({
template: 'src/index.html'
}),
isProduction && QuantumPlugin({
bakeApiIntoBundle: 'bundle',
treeshake: true,
uglify: true,
}),
],
})
app = fuse.bundle('bundle').instructions('> javascripts/index.js')
})
Sparky.task('clean', () => Sparky.src('public/').clean('public/'))
Sparky.task('copy', () => Sparky.src('index.html', {base: 'public/'}).dest('./'))
Sparky.task('prod-env', () => { isProduction = true })
Sparky.task('build', ['prod-env', 'clean', 'config'], () => fuse.run())
Sparky.task('release', ['build', 'copy'], () => {})
Sparky.task('default', ['clean', 'config'], () => {
fuse.dev({
port: 8000,
})
app.watch().hmr()
return fuse.run()
})
fuse.jsは、fuse-box/react-exampleを参考にして作りました。
FuseBox付属のタスクランナーSparkyを使用しています。
詳しい使い方は参考サイトに譲ります。
リリース用にビルドするときのみprod-envタスクを噛ませてisProductionフラグを立てることにより、sourceMapsを消したりuglifyしたりできて、かなり気持ちいいです。
知見
fuse.jsを書くときにいろいろと迷ったので、個人的なポイントやハマったところを挙げていきます。
デフォルトのトランスパイラがTypeScript
README.mdに
FuseBox loves typescript, and does not require any additional configuration.
とある通り、特に何も指定しなければソースコードをTypeScriptと解釈してトランスパイルします。
既存のプロジェクトでBabelを使用している場合、pluginsにBabelPluginを指定する必要があります。
.babelrcに記載していたようなpresetsの設定等はこの中に書きます。
QuantumPlugin
バンドルしたソースコードをminifyしたりtreeshakeしたりuglifyしたりできます。
このプラグインを追加すると、バンドルファイルと別にapi.jsというファイルが吐かれるようになります。
それも含めて一つのバンドルファイルにしたい場合、bakeApiIntoBundleオプションにバンドル名を指定します。
plugin chaining
[SassPlugin(), CSSPlugin()]
こんな感じでプラグインを配列で指定すると、まずSassPluginを通してからCSSPluginに渡す、といったことができます。
plugin chaining という機能名らしいです。
homeDirフォルダ配下のソースしか読んでくれない
例えば以下のようなフォルダ構成でindex.jsがエントリポイントであるとき、

Sparky.task('config', () => {
fuse = new FuseBox({
homeDir: 'src/javascripts/',
...
app = fuse.bundle('bundle').instructions('> index.js')
})
と設定すると、homeDirに指定したsrc/javascripts配下のhoge.jsやhage.jsは読み込めますが、その他のsrc/stylesheetsやsrc/assets配下にあるファイルは読み込めません。(バンドルは正常に終了するが、ブラウザで実行時エラーが発生する)
この場合、
Sparky.task('config', () => {
fuse = new FuseBox({
homeDir: 'src/',
...
app = fuse.bundle('bundle').instructions('> javascripts/index.js')
})
このように、homeDirに指定するのはsrcまでとすることで、ブラウザで正常に読み込めるようになります。
私はこの問題で三時間くらい悩みました……。

