webpack -> FuseBox 移行メモ

  • 34
    Like
  • 0
    Comment

webpackを使っていたプロジェクトをFuseBoxに移行してみました。

バンドル時間が超速くなり、バンドルファイルサイズが超小さくなりました。

webpack
image.png

FuseBox
image.png

package.json

package.jsonの比較です。

package.json(webpack)
  "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"
  },
package.json(FuseBox)
  "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からの実行に切り替えました。

webpack.config.dev.js
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
        }
      }
    })
  ],
};
fuse.js
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を使用している場合、pluginsBabelPluginを指定する必要があります。
.babelrcに記載していたようなpresetsの設定等はこの中に書きます。

QuantumPlugin

バンドルしたソースコードをminifyしたりtreeshakeしたりuglifyしたりできます。
このプラグインを追加すると、バンドルファイルと別にapi.jsというファイルが吐かれるようになります。
それも含めて一つのバンドルファイルにしたい場合、bakeApiIntoBundleオプションにバンドル名を指定します。

plugin chaining

[SassPlugin(), CSSPlugin()]

こんな感じでプラグインを配列で指定すると、まずSassPluginを通してからCSSPluginに渡す、といったことができます。
plugin chaining という機能名らしいです。

homeDirフォルダ配下のソースしか読んでくれない

例えば以下のようなフォルダ構成でindex.jsがエントリポイントであるとき、
image.png

fuse.js
Sparky.task('config', () => {
  fuse = new FuseBox({
    homeDir: 'src/javascripts/',

...

  app = fuse.bundle('bundle').instructions('> index.js')
})

と設定すると、homeDirに指定したsrc/javascripts配下のhoge.jshage.jsは読み込めますが、その他のsrc/stylesheetssrc/assets配下にあるファイルは読み込めません。(バンドルは正常に終了するが、ブラウザで実行時エラーが発生する)

この場合、

fuse.js
Sparky.task('config', () => {
  fuse = new FuseBox({
    homeDir: 'src/',

...

  app = fuse.bundle('bundle').instructions('> javascripts/index.js')
})

このように、homeDirに指定するのはsrcまでとすることで、ブラウザで正常に読み込めるようになります。
私はこの問題で三時間くらい悩みました……。

参考