Webpack Shimming 預(yù)置依賴

2023-05-18 11:24 更新

webpack compiler 能夠識別遵循 ES2015 模塊語法、CommonJS 或 AMD 規(guī)范編寫的模塊。然而,一些 third party(第三方庫) 可能會引用一些全局依賴(例如 jQuery 中的 $)。因此這些 library 也可能會創(chuàng)建一些需要導(dǎo)出的全局變量。這些 "broken modules(不符合規(guī)范的模塊)" 就是 shimming(預(yù)置依賴) 發(fā)揮作用的地方。

shim 另外一個極其有用的使用場景就是:當(dāng)你希望 polyfill 擴展瀏覽器能力,來支持到更多用戶時。在這種情況下,你可能只是想要將這些 polyfills 提供給需要修補(patch)的瀏覽器(也就是實現(xiàn)按需加載)。

下面的文章將向我們展示這兩種用例。

Shimming 預(yù)置全局變量

讓我們開始第一個 shimming 全局變量的用例。在此之前,先看下我們的項目:

project

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

還記得我們之前用過的 lodash 嗎?出于演示目的,例如把這個應(yīng)用程序中的模塊依賴,改為一個全局變量依賴。要實現(xiàn)這些,我們需要使用 ProvidePlugin 插件。

使用 ProvidePlugin 后,能夠在 webpack 編譯的每個模塊中,通過訪問一個變量來獲取一個 package。如果 webpack 看到模塊中用到這個變量,它將在最終 bundle 中引入給定的 package。讓我們先移除 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',
+    }),
+  ],
 };

我們本質(zhì)上所做的,就是告訴 webpack……

如果你遇到了至少一處用到 _ 變量的模塊實例,那請你將 lodash package 引入進(jìn)來,并將其提供給需要用到它的模塊。

運行我們的構(gòu)建腳本,將會看到同樣的輸出:

$ 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 暴露出某個模塊中單個導(dǎo)出,通過配置一個“數(shù)組路徑”(例如 [module, child, ...children?])實現(xiàn)此功能。所以,我們假想如下,無論 join 方法在何處調(diào)用,我們都只會獲取到 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'],
     }),
   ],
 };

這樣就能很好的與 tree shaking 配合,將 lodash library 中的其余沒有用到的導(dǎo)出去除。

細(xì)粒度 Shimming

一些遺留模塊依賴的 this 指向的是 window 對象。在接下來的用例中,調(diào)整我們的 index.js:

 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

+   // 假設(shè)我們處于 `window` 上下文
+   this.alert('Hmmm, this probably isn\'t a great idea...')
+
   return element;
 }

 document.body.appendChild(component());

當(dāng)模塊運行在 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'],
     }),
   ],
 };

全局 Exports

讓我們假設(shè),某個 library 創(chuàng)建出一個全局變量,它期望 consumer(使用者) 使用這個變量。為此,我們可以在項目配置中,添加一個小模塊來演示說明:

project

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

你可能從來沒有在自己的源碼中做過這些事情,但是你也許遇到過一個老舊的 library,和上面所展示的代碼類似。在這種情況下,我們可以使用 exports-loader,將一個全局變量作為一個普通的模塊來導(dǎo)出。例如,為了將 file 導(dǎo)出為 file 以及將 helpers.parse 導(dǎo)出為 parse,做如下調(diào)整:

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

此時,在我們的 entry 入口文件中(即 src/index.js),可以使用 const { file, parse } = require('./globals.js');,可以保證一切將順利運行。

加載 Polyfills

目前為止我們所討論的所有內(nèi)容都是處理那些遺留的 package,讓我們進(jìn)入到第二個話題:polyfill。

有很多方法來加載 polyfill。例如,想要引入 babel-polyfill 我們只需如下操作:

npm install --save babel-polyfill

然后,使用 import 將其引入到我們的主 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());

注意,這種方式優(yōu)先考慮正確性,而不考慮 bundle 體積大小。為了安全和可靠,polyfill/shim 必須運行于所有其他代碼之前,而且需要同步加載,或者說,需要在所有 polyfill/shim 加載之后,再去加載所有應(yīng)用程序代碼。 社區(qū)中存在許多誤解,即現(xiàn)代瀏覽器“不需要”polyfill,或者 polyfill/shim 僅用于添加缺失功能 - 實際上,它們通常用于修復(fù)損壞實現(xiàn)(repair broken implementation),即使是在最現(xiàn)代的瀏覽器中,也會出現(xiàn)這種情況。 因此,最佳實踐仍然是,不加選擇地和同步地加載所有 polyfill/shim,盡管這會導(dǎo)致額外的 bundle 體積成本。

如果你認(rèn)為自己已經(jīng)打消這些顧慮,并且希望承受損壞的風(fēng)險。那么接下來的這件事情,可能是你應(yīng)該要做的: 我們將會把 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());

project

  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 文件。根據(jù)需要支持的技術(shù)和瀏覽器來決定是否加載。我們將做一些簡單的試驗,來確定是否需要引入這些 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>

現(xiàn)在,在 entry 入口文件中,可以通過 fetch 獲取一些數(shù)據(jù):

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

執(zhí)行構(gòu)建腳本,可以看到,瀏覽器發(fā)送了額外的 polyfills.bundle.js 文件請求,然后所有代碼順利執(zhí)行。注意,以上的這些設(shè)定可能還會有所改進(jìn),這里我們向你提供一個很棒的想法:將 polyfill 提供給需要引入它的用戶。

進(jìn)一步優(yōu)化

babel-preset-env package 通過 browserslist 來轉(zhuǎn)譯那些你瀏覽器中不支持的特性。這個 preset 使用 useBuiltIns 選項,默認(rèn)值是 false,這種方式可以將全局 babel-polyfill 導(dǎo)入,改進(jìn)為更細(xì)粒度的 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';

See the babel-preset-env documentation for more information.

Node 內(nèi)置

像 process 這種 Node 內(nèi)置模塊,能直接根據(jù)配置文件進(jìn)行正確的 polyfill,而不需要任何特定的 loader 或者 plugin。查看 node 配置頁面獲取更多信息。

其他工具

還有一些其他的工具,也能夠幫助我們處理這些遺留模塊。

如果這些遺留模塊沒有 AMD/CommonJS 版本,但你也想將他們加入 dist 文件,則可以使用 noParse 來標(biāo)識出這個模塊。這樣就能使 webpack 將引入這些模塊,但是不進(jìn)行轉(zhuǎn)化(parse),以及不解析(resolve) require() 和 import 語句。這種用法還會提高構(gòu)建性能。

最后,一些模塊支持多種 模塊格式,例如一個混合有 AMD、CommonJS 和 legacy(遺留) 的模塊。在大多數(shù)這樣的模塊中,會首先檢查 define,然后使用一些怪異代碼導(dǎo)出一些屬性。在這些情況下,可以通過 imports-loader 設(shè)置 additionalCode=var%20define%20=%20false; 來強制 CommonJS 路徑。

Further Reading


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號