webpack
コンパイラは、ES2015 モジュール、CommonJS、または AMD として記述されたモジュールを理解できます。ただし、一部のサードパーティライブラリはグローバル依存関係(例:jQuery
の $
)を想定している場合があります。ライブラリは、エクスポートする必要があるグローバルを作成することもあります。これらの「壊れたモジュール」は、*シミング*が役立つ1つの例です。
*シミング*が役立つもう1つの例は、より多くのユーザーをサポートするためにブラウザの機能をポリフィルする場合です。この場合、パッチを必要とするブラウザにのみこれらのポリフィルを提供したい場合があります(つまり、オンデマンドでロードします)。
以下の記事では、これらのユースケースの両方について説明します。
グローバル変数のシミングの最初のユースケースから始めましょう。何をする前に、プロジェクトをもう一度見てみましょう
プロジェクト
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
|- /node_modules
使用していた `lodash` パッケージを覚えていますか?デモンストレーションの目的で、これをアプリケーション全体でグローバルに提供したいとしましょう。これを行うには、`ProvidePlugin` を使用できます。
`ProvidePlugin` は、webpack を介してコンパイルされたすべてのモジュールでパッケージを変数として使用できるようにします。 webpack がその変数が使用されていることを確認した場合、最終的なバンドルに指定されたパッケージを含めます。`lodash` の `import` ステートメントを削除し、代わりにプラグインを介して提供することで、先に進みましょう
src/index.js
-import _ from 'lodash';
-
function component() {
const element = document.createElement('div');
- // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
+const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
+ }),
+ ],
};
ここで本質的に行ったことは、webpack に指示することです...
変数 `_` のインスタンスが少なくとも1つ見つかったら、`lodash` パッケージを含めて、それを必要とするモジュールに提供します。
ビルドを実行すると、同じ出力が表示されます
$ npm run build
..
[webpack-cli] Compilation finished
asset main.js 69.1 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 344 bytes 2 modules
cacheable modules 530 KiB
./src/index.js 191 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 2910 ms
また、「配列パス」(例:`[module, child, ...children?]`)で設定することにより、`ProvidePlugin` を使用してモジュールの単一エクスポートを公開することもできます。そのため、`lodash` から `join` メソッドのみを呼び出された場所に提供したいと想像してみましょう
src/index.js
function component() {
const element = document.createElement('div');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.ProvidePlugin({
- _: 'lodash',
+ join: ['lodash', 'join'],
}),
],
};
これはツリーシェイキングと相性が良いでしょう。`lodash` ライブラリの残りは削除されるはずです。
一部のレガシーモジュールは、`this` が `window` オブジェクトであることに依存しています。これが当てはまるように `index.js` を更新しましょう
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
+ // Assume we are in the context of `window`
+ this.alert("Hmmm, this probably isn't a great idea...");
+
return element;
}
document.body.appendChild(component());
これは、`this` が `module.exports` と等しい CommonJS コンテキストでモジュールが実行されるときに問題になります。この場合、`imports-loader`を使用して `this` をオーバーライドできます
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: require.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
ライブラリが、コンシューマーが使用することを期待するグローバル変数を作成するとしましょう。これを示すために、小さなモジュールをセットアップに追加できます
プロジェクト
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- globals.js
|- /node_modules
src/globals.js
const file = 'blah.txt';
const helpers = {
test: function () {
console.log('test something');
},
parse: function () {
console.log('parse something');
},
};
さて、自分のソースコードではおそらくこれを行うことはありませんが、上記に示されているのと同様のコードを含む、使用したい古いライブラリに遭遇する可能性があります。この場合、`exports-loader`を使用して、そのグローバル変数を通常のモジュールエクスポートとしてエクスポートできます。たとえば、`file` を `file` として、`helpers.parse` を `parse` としてエクスポートするには
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: require.resolve('./src/globals.js'),
+ use:
+ 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+ },
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
これで、エントリスクリプト(つまり `src/index.js`)内から、`const { file, parse } = require('./globals.js');` を使用でき、すべてがスムーズに機能するはずです。
これまでに説明してきたほとんどすべては、レガシーパッケージの処理に関連しています。2番目のトピックである**ポリフィル**に進みましょう。
ポリフィルを読み込む方法はたくさんあります。例えば、babel-polyfill
を含めるには、次のようにします。
npm install --save babel-polyfill
そして、メインバンドルに含めるために import
します。
src/index.js
+import 'babel-polyfill';
+
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
このアプローチは、バンドルサイズよりも正確性を優先することに注意してください。安全で堅牢にするために、ポリフィル/シムは**他のすべてのコードの前に**実行する必要があり、そのため、同期的にロードするか、すべてのアプリコードをすべてのポリフィル/シムのロード後にロードする必要があります。また、コミュニティには、最新のブラウザはポリフィルを「必要としない」とか、ポリフィル/シムは単に不足している機能を追加するためだけのものであるという誤解が多くあります。実際には、最新のブラウザであっても、ポリフィル/シムはしばしば*壊れた実装を修復*します。したがって、バンドルサイズのコストが発生するにもかかわらず、すべてのポリフィル/シムを無条件に同期的にロードすることが依然としてベストプラクティスです。
これらの懸念事項を軽減し、破損のリスクを負うことをいとわない場合は、次のような方法があります。import
を新しいファイルに移動し、whatwg-fetch
ポリフィルを追加します。
npm install --save whatwg-fetch
src/index.js
-import 'babel-polyfill';
-
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
プロジェクト
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- globals.js
+ |- polyfills.js
|- /node_modules
src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ polyfills: './src/polyfills',
+ index: './src/index.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
{
test: require.resolve('./src/globals.js'),
use:
'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
},
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
これができたら、新しい polyfills.bundle.js
ファイルを条件付きでロードするロジックを追加できます。この決定方法は、サポートする必要があるテクノロジーとブラウザによって異なります。ポリフィルが必要かどうかを判断するために、いくつかのテストを行います。
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
+ <script>
+ const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+ if (!modernBrowser) {
+ const scriptElement = document.createElement('script');
+
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
- <script src="main.js"></script>
+ <script src="index.bundle.js"></script>
</body>
</html>
これで、エントリスクリプト内で fetch
でデータを取得できます。
src/index.js
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+ .then((response) => response.json())
+ .then((json) => {
+ console.log(
+ "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+ );
+ console.log(json);
+ })
+ .catch((error) =>
+ console.error('Something went wrong when fetching this data: ', error)
+ );
ビルドを実行すると、別の polyfills.bundle.js
ファイルが出力され、ブラウザですべてがスムーズに実行されるはずです。この設定はさらに改善できる可能性がありますが、実際にポリフィルを必要とするユーザーにのみポリフィルを提供する方法の良いアイデアが得られるはずです。
babel-preset-env
パッケージは、browserslist を使用して、ブラウザマトリックスでサポートされていないものだけをトランスパイルします。このプリセットには、デフォルトで false
の useBuiltIns
オプションが付属しており、グローバルな babel-polyfill
インポートを、より粒度の細かい機能ごとの import
パターンに変換します。
import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';
詳細については、babel-preset-env のドキュメントを参照してください。
process
のような Node 組み込み機能は、特別なローダーやプラグインを使用せずに、設定ファイルから直接ポリフィルできます。詳細と例については、node 設定ページを参照してください。
レガシーモジュールを扱う際に役立つツールが他にもいくつかあります。
モジュールの AMD/CommonJS バージョンがなく、dist
を含めたい場合は、noParse
でこのモジュールにフラグを立てることができます。これにより、webpack はモジュールを解析したり、require()
や import
ステートメントを解決したりせずにモジュールを含めます。この方法は、ビルドのパフォーマンスを向上させるためにも使用されます。
最後に、複数の モジュールスタイル をサポートするモジュールがあります。例えば、AMD、CommonJS、およびレガシーの組み合わせです。これらのほとんどの場合、最初に define
をチェックし、次にいくつかの風変わりなコードを使用してプロパティをエクスポートします。このような場合は、imports-loader
を介して additionalCode=var%20define%20=%20false;
を設定することで、CommonJS パスを強制することが役立つ場合があります。