最適解だと確信が持てないので、あえてQiitaに晒してパブリックコメントを募りたい。
今回はMaterialize-CSSとjQuery 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/
以下をフラットにしてくれるおかげで、パッケージは追いやすい。
{
"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を生成する構成。
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してくれる。
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(= 上記設定だと動かない)。
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
「 HEAD
も A
も表示される、jQueryは読み込めてるな」
「Materialize-CSSってjQuery3でも動くのかー」
この辺でモヤモヤしはじめ、 .validate() is not a function.
が出て「やっぱり依存解決できてないじゃん」と。
現象の確認
依存解決が失敗している理由を探す。
webpack -d --display-modules true
--display-modules true
で組み込んだファイルの一覧が得られる。
$ 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/
なりへインストールされている当該ファイルへのパスをベタ書き。
module.exports = {
...
resolve: {
alias: {
jquery: "jquery/src/jquery"
}
}
};
2. ProvidePluginを使う
グローバル変数として参照できる名前を追加する。
var webpack = require("webpack");
...
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
]
3. import-loaderでthisを置き換える
this
を window
に置き換える。
module: {
loaders: [
{
test: /[\/\\]node_modules[\/\\]some-module[\/\\]index\.js$/,
loader: "imports?this=>window"
}
]
}
4. import-loaderでAMDを殺す
Asynchronous Module Definitionで依存解決しないようにする。
module: {
loaders: [
{
test: /[\/\\]node_modules[\/\\]some-module[\/\\]index\.js$/,
loader: "imports?define=>false"
}
]
}
5. <script>
でグローバルに読み込む
script-loaderを使って旧態然にする。
6. noParseを指定してパースさせない
webpack的なことをすべて諦める。
module: {
noParse: [
/[\/\\]node_modules[\/\\]angular[\/\\]angular\.js$/
]
}
aliasを張って解決
6つの方法、それぞれにPros/Consがあって、状況によって選び分けなければならない。
とはいえ、回答者はオススメ順に挙げているらしい。
ProvidePluginではライブラリごとの読み込み先を制御できないし、不用意にグローバル変数を増やしたくない。
一番最初の「aliasを張っての解決」であればDedupePluginなどの最適化も効く。
今回は、jQuery Validationが2系でも動くことが確認できていたので、Materialize-CSS内のjQueryだけ参照するように書き換えた。
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 -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の仕組みが完成されているとも思えず、またすぐに別の技術で取って代わられそうなのが…