Shimming

webpack 編譯器可以理解撰寫為 ES2015 模組、CommonJS 或 AMD 的模組。然而,某些第三方程式庫可能會預期有全域依賴項(例如 jQuery$)。這些程式庫也可能建立需要匯出的全域變數。這些「損壞的模組」是shim 發揮作用的一個範例。

shim 另一個有用的範例是當您想要polyfill 瀏覽器功能以支援更多使用者時。在這種情況下,您可能只想將這些 polyfill 傳送給需要修補的瀏覽器(即隨選載入它們)。

以下文章將逐步說明這兩個使用案例。

Shimming 全域變數

讓我們從 Shimming 全域變數的第一個使用案例開始。在我們進行任何操作之前,讓我們再看一看我們的專案

專案

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
  |- index.html
|- /src
  |- index.js
|- /node_modules

還記得我們使用的 lodash 套件嗎?為了示範,假設我們想在整個應用程式中提供這項功能作為全域變數。為此,我們可以使用 ProvidePlugin

ProvidePlugin 使套件可用於透過 webpack 編譯的每個模組中的變數。如果 webpack 看見該變數被使用,它會將指定的套件包含在最終的套件中。讓我們繼續刪除 lodashimport 陳述式,並透過外掛程式提供它

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...

如果您遇到變數 _ 的至少一個執行個體,請包含 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

我們也可以使用 ProvidePlugin 透過「陣列路徑」(例如 [module, child, ...children?]) 設定來公開模組的單一匯出。因此,讓我們想像我們只想在任何呼叫它的位置提供 lodashjoin 方法

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'],
     }),
   ],
 };

這會很適合搭配 Tree Shaking,因為其餘的 lodash 函式庫應該會被捨棄。

細粒度 Shimming

有些舊模組依賴 thiswindow 物件。讓我們更新我們的 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());

當模組在 CommonJS 環境中執行時,這會變成一個問題,因為 this 等於 module.exports。在這種情況下,你可以使用 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');,一切都應該順利運作。

載入 Polyfill

到目前為止,我們討論的所有內容幾乎都與處理舊套件有關。讓我們繼續我們的第二個主題:polyfill

有許多載入 polyfill 的方法。例如,要包含 babel-polyfill,我們可以

npm install --save babel-polyfill

import 它,以將其包含在我們的 main bundle 中

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());

請注意,此方法優先考慮正確性,而非 bundle 大小。為了安全和穩健,polyfill/shim 必須在所有其他程式碼之前執行,因此需要同步載入,或者所有應用程式程式碼都必須在所有 polyfill/shim 載入後載入。此外,社群中也有許多誤解,例如現代瀏覽器「不需要」polyfill,或 polyfill/shim 僅用於新增遺失的功能 - 事實上,它們通常會修復損壞的實作,即使在最現代的瀏覽器中也是如此。因此,最佳做法仍然是無條件且同步載入所有 polyfill/shim,儘管這會增加 bundle 大小。

如果您認為已減輕這些疑慮,並希望承擔損壞的風險,以下是可以執行的方法之一:讓我們將我們的 import 移至新的檔案,並新增 whatwg-fetch polyfill

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 檔案。您如何做出此決定取決於您需要支援的技術和瀏覽器。我們將進行一些測試,以確定是否需要我們的 polyfill

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>

現在我們可以在我們的 entry script 中 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 來轉譯瀏覽器矩陣中不支援的內容。此預設值附帶 useBuiltIns 選項,預設為 false,它會將您的全域 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 文件 以取得更多資訊。

Node 內建

Node 內建,例如 process,可以直接從您的設定檔多重填補,而不需要使用任何特殊載入器或外掛程式。請參閱 node 設定頁面 以取得更多資訊和範例。

其他公用程式

有幾個其他工具可以在處理舊式模組時提供協助。

當模組沒有 AMD/CommonJS 版本,且您想要包含 dist 時,您可以在 noParse 中標記此模組。這將導致 webpack 在不解析或解析 require()import 陳述的情況下包含模組。此做法也用於改善建置效能。

最後,有些模組支援多種 模組樣式;例如,AMD、CommonJS 和舊版組合。在這些情況中,它們大多數會先檢查 define,然後使用一些古怪的程式碼來匯出屬性。在這些情況下,透過 imports-loader 設定 additionalCode=var%20define%20=%20false; 來強制 CommonJS 路徑可能會有幫助。

14 貢獻者

pksjcejhnnssimon04jeremenichellisvyandunbyzykEugeneHlushkoAnayaDesigndhurlburtusaplr108NicolasLetellierwizardofhogwartssnitin315chenxsan