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

webpackでjQueryの多重ロードを回避する方法

More than 3 years have passed since last update.

最適解だと確信が持てないので、あえてQiitaに晒してパブリックコメントを募りたい。
今回はMaterialize-CSSjQuery Validation Pluginで発生したけど、似たシチュエーションは多そう。

Qiita内だとこの辺の記事とか。

stack overflowだとこの記事がまさに。

TL; DR

jQuery2系を内包するMaterialize-CSSを使いつつ、
jQuery1系を推奨するjQuery Validation Pluginが効いたフォームを作りたい。

あちらを立てればこちらが立たず。気づけばjQuery3系も混じっていた。

アプローチはいろいろあれど、alias作戦が一番良さそう。

Google Formのクローンを作る、的な案件にて。
修正前のコード(= Materialize-CSSは動くが、jQuery.validationは動かない状態)。

さぁ、間違い探しの時間だよ!

package.json

デプロイ先はFlask on GAE/Python。
Yarnが node_modules/ 以下をフラットにしてくれるおかげで、パッケージは追いやすい。

package.json
{
  "version": "1.0.0",
  "dependencies": {
    "jquery": "^3.1.1",
    "materialize-css": "^0.98.0"
  },
  "devDependencies": {
    "babel": "^6.5.2",
    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-polyfill": "^6.22.0",
    "babel-preset-es2015": "^6.22.0",
    "babel-register": "^6.22.0",
    "chai": "^3.5.0",
    "css-loader": "^0.26.1",
    "eslint": "^3.14.1",
    "eslint-loader": "^1.6.1",
    "file-loader": "^0.10.0",
    "html-loader": "^0.4.4",
    "html-webpack-plugin": "^2.28.0",
    "mocha": "^3.2.0",
    "mocha-generators": "^1.2.0",
    "nightmare": "^2.9.1",
    "node-sass": "^4.5.0",
    "sass-loader": "^5.0.1",
    "style-loader": "^0.13.1",
    "url-loader": "^0.5.7",
    "webpack": "^2.2.1",
    "webpack-dashboard": "^0.3.0",
    "webpack-shell-plugin": "^0.5.0"
  },
  "scripts": {
    "clean": "rm -f static/* *.pyc *.pyo",
    "stage": "dev_appserver.py .",
    "build:debug": "webpack-dashboard -- webpack -d --progress",
    "build:watch": "webpack-dashboard -- webpack -d --progress --watch",
    "build:production": "webpack-dashboard -- webpack -p --progress",
    "start": "yarn run build:watch & yarn run stage",
    "test": "mocha",
    "test:production": "env production=true mocha",
    "test:watch": "mocha --watch",
    "log": "gcloud app logs read -s default"
  }
}

webpack.config.js

webpackは2系。
html-webpack-pluginで、 <head> 内に読み込みたいhead.jsファイルとejsテンプレート、そのテンプレートを使うa、b、cを生成する構成。

webpack.config.js
const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const webpackShellPlugin = require('webpack-shell-plugin');
const webpackDashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    head: './head.js',
    a: './a.js',
    b: './b.js',
    c: './c.js'
  },
  output: {
    path: path.resolve(__dirname, './static'),
    filename: '[name].js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      enforce: 'pre',
      exclude: /node_modules/,
      use: [{
        loader: 'eslint-loader',
        options: {
          configFile: './.eslintrc.json'
        }
      }, {
        loader: 'babel-loader',
        query: {
          presets: ['es2015'],
          comments: false,
          compact: true
        }
      }]
    }, {
      test: /\.html$/,
      loader: 'html-loader'
    }, {
      test: /\.css$/,
      loader: ['style-loader', 'css-loader']
    }, {
      test: /\.(sass|scss)$/,
      loader: ['style-loader', 'css-loader', 'sass-loader']
    }, {
      test: /\.(jpg|png)$/,
      loader: 'url-loader?limit=10240&name=../static/[name].[ext]'
    }, {
      test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
      loader: 'url-loader'
    }]
  },
  plugins: [
    new webpack.EnvironmentPlugin({
      'NODE_ENV': 'development',
      'DEBUG': false
    }),
    new htmlWebpackPlugin({
      filename: '../templates/head.html',
      chunks: ['head'],
      template: './head.html',
      inject: 'head',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/a.html',
      chunks: ['a'],
      template: './a.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/b.html',
      chunks: ['b'],
      template: './b.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/c.html',
      chunks: ['c'],
      template: './c.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      comments: false,
      compress: {
        dead_code: true,
        unused: true,
        drop_console: true
      }
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.AggressiveMergingPlugin(),
    new webpack.ProvidePlugin({
      // $: 'jquery',
      // jQuery: 'jquery',
      'window.jQuery': 'jquery'
    }),
    new webpackShellPlugin({
      onBuildStart: ['yarn run clean'],
      onBuildEnd: ['say webpack completed']
    }),
    new webpackDashboardPlugin()
  ]
};

head.js

読み込みを確認するためだけのconsole.log。
--production すれば、UglifyJSPluginがdrop_consoleしてくれる。

head.js
import 'jquery';
import 'materialize-css';
import 'materialize-css/sass/materialize.scss';
import './head.scss'

$(document).ready(() => {
  console.log('HEAD');  // XXX: for Debug!
});

a.js

共有部分はhead.jsに寄せたので、差分だけ。
.materialbox().material_select() がMaterialize-CSS(= 動く)。
.validate() がjQuery Validation(= 上記設定だと動かない)。

a.js
import './a.scss';

$(document).ready(() => {
  console.log('A');  // XXX: for Debug!

  $('.materialboxed').materialbox();
  $('select').material_select();

  let form = $('#form');
  form.validate();
});

思い違い

「Materialize-CSSはjQueryに依存してるのか」 → yarn add jquery

HEADA も表示される、jQueryは読み込めてるな」

「Materialize-CSSってjQuery3でも動くのかー」

この辺でモヤモヤしはじめ、 .validate() is not a function. が出て「やっぱり依存解決できてないじゃん」と。

現象の確認

依存解決が失敗している理由を探す。

webpack -d --display-modules true

--display-modules true で組み込んだファイルの一覧が得られる。

webpack
$ webpack -d --display-modules true
[0] ../~/materialize-css/~/jquery/dist/jquery.js 258 kB {0} {1} {2} [built]
[1] ../~/css-loader/lib/css-body.js 1.51 kB {0} {1} {2} {3} [built]
[2] ../~/style-loader/addStyles.js 7.15 kB {0} {1} {2} {3} [built]
[3] ../~/materialize-css/bin/materialize.js 146 kB {0} {1} {2} [built]
[4] ../~/hammerjs/hammer.js 73.8 kB {0} {1} {2} [built]
[5] ../~/webpack/buildin/amd-options.js 82 bytes {0} {1} {2} [built]
[6] ../~/jquery/dist/jquery.js 267 kB {0} {1} [built]

Materialize-CSSのjQueryとyarnで入れたjQuery、どっちも組み込んでいた。

jQuery ValidationとMaterialize-CSSで追加する先のjQueryが食い違って動かないと推測。

解決方法は6つある

stack overflowで紹介されていたのは下記の6つ。

1. 読み込みたいファイルへaliasを張る

node_modules/ なりへインストールされている当該ファイルへのパスをベタ書き。

webpack.config.js
module.exports = {
    ...
    resolve: {
        alias: {
            jquery: "jquery/src/jquery"
        }
    }
};

2. ProvidePluginを使う

グローバル変数として参照できる名前を追加する。

webpack.config.js
var webpack = require("webpack");
    ...
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ]

3. import-loaderでthisを置き換える

thiswindow に置き換える。

webpack.config.js
module: {
    loaders: [
        {
            test: /[\/\\]node_modules[\/\\]some-module[\/\\]index\.js$/,
            loader: "imports?this=>window"
        }
    ]
}

4. import-loaderでAMDを殺す

Asynchronous Module Definitionで依存解決しないようにする。

webpack.config.js
module: {
    loaders: [
        {
            test: /[\/\\]node_modules[\/\\]some-module[\/\\]index\.js$/,
            loader: "imports?define=>false"
        }
    ]
}

5. <script> でグローバルに読み込む

script-loaderを使って旧態然にする。

6. noParseを指定してパースさせない

webpack的なことをすべて諦める。

webpack.config.js
module: {
    noParse: [
        /[\/\\]node_modules[\/\\]angular[\/\\]angular\.js$/
    ]
}

aliasを張って解決

6つの方法、それぞれにPros/Consがあって、状況によって選び分けなければならない。
とはいえ、回答者はオススメ順に挙げているらしい。
ProvidePluginではライブラリごとの読み込み先を制御できないし、不用意にグローバル変数を増やしたくない。
一番最初の「aliasを張っての解決」であればDedupePluginなどの最適化も効く。

今回は、jQuery Validationが2系でも動くことが確認できていたので、Materialize-CSS内のjQueryだけ参照するように書き換えた。

webpack.config.js
const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const webpackShellPlugin = require('webpack-shell-plugin');

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    head: './head.js',
    a: './a.js',
    b: './b.js',
    c: './c.js'
  },
  output: {
    path: path.resolve(__dirname, './static'),
    filename: '[name].js'
  },
  resolve: {
    alias: {
      jquery: 'materialize-css/bin/jquery-2.1.1.min.js'
    }
  },
  module: {
    rules: [{
      test: /\.js$/,
      enforce: 'pre',
      exclude: /node_modules/,
      use: [{
        loader: 'eslint-loader',
        options: {
          configFile: './.eslintrc.json'
        }
      }, {
        loader: 'babel-loader',
        query: {
          presets: ['es2015'],
          comments: false,
          compact: true
        }
      }]
    }, {
      test: /\.html$/,
      loader: 'html-loader'
    }, {
      test: /\.css$/,
      loader: ['style-loader', 'css-loader']
    }, {
      test: /\.(sass|scss)$/,
      loader: ['style-loader', 'css-loader', 'sass-loader']
    }, {
      test: /\.(jpg|png)$/,
      loader: 'url-loader?limit=10240&name=../static/[name].[ext]'
    }, {
      test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
      loader: 'url-loader'
    }]
  },
  plugins: [
    new webpack.EnvironmentPlugin({
      'NODE_ENV': 'development',
      'DEBUG': false
    }),
    new htmlWebpackPlugin({
      filename: '../templates/head.html',
      chunks: ['head'],
      template: './head.html',
      inject: 'head',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/a.html',
      chunks: ['a'],
      template: './a.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/b.html',
      chunks: ['b'],
      template: './b.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new htmlWebpackPlugin({
      filename: '../templates/c.html',
      chunks: ['c'],
      template: './c.html',
      inject: 'body',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      comments: false,
      compress: {
        dead_code: true,
        unused: true,
        drop_console: true
      }
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpackShellPlugin({
      onBuildStart: ['yarn run clean'],
      onBuildEnd: ['say webpack completed']
    })
  ]
};

resolve>aliasにパスを追加、ProvidePluginは使わなくなったので削除。

webpack
$ webpack -d --display-modules true
[0] ../~/materialize-css/bin/jquery-2.1.1.min.js 84.3 kB {1} {2} [built]
[1] ../~/css-loader/lib/css-base.js 1.51 kB {0} {1} {2} {3} [built]
[2] ../~/style-loader/addStyles.js 7.15 kB {0} {1} {2} {3} [built]
[3] ../~/webpack/buildin/amd-options.js 82 bytes {1} {2} [built]
[4] ../~/materialize-css/bin/materialize.js 146 kB {1} {2} [built]
[5] ../~/hammerjs/hammer.js 73.8 kB {1} {2} [built]

1つしか組み込んでいないことを確認、問題解決。

まとめ

問題は解決したけれど、なんだかモヤモヤする。
Forumやsoを読む限り、Materialize-CSSは積極的にmodule化する気配はないし。
脱jQueryの動きがあっても、依然としてjQuery Validationが定番だし。
なにより、webpackの仕組みが完成されているとも思えず、またすぐに別の技術で取って代わられそうなのが…

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