Help us understand the problem. What is going on with this article?

webpack -> FuseBox 移行メモ

More than 3 years have passed since last update.

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

参考

s4kr4
Webエンジニア
https://s4kr4.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした