LoginSignup
47

More than 5 years have passed since last update.

webpack -> FuseBox 移行メモ

Posted at

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までとすることで、ブラウザで正常に読み込めるようになります。
私はこの問題で三時間くらい悩みました……。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47