垫片(Shimming)

webpack 编译器能够理解用 ES2015 模块、CommonJS 或 AMD 编写的模块。然而,一些第三方库可能需要全局依赖(例如 jQuery$)。这些库也可能创建需要导出的全局变量。这些“损坏的模块”是*垫片(shimming)*发挥作用的一个场景。

另一个*垫片(shimming)*有用的场景是当你想填充(polyfill)浏览器功能以支持更多用户时。在这种情况下,你可能只想将这些 polyfills 交付给需要修补的浏览器(即按需加载它们)。

以下文章将介绍这两种使用场景。

垫片全局变量

让我们从垫片全局变量的第一个用例开始。在做任何事情之前,让我们再次审视我们的项目

项目

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

我们还可以通过配置“数组路径”(例如 [module, child, ...children?])来使用 ProvidePlugin 暴露模块的单个导出。所以让我们想象一下,我们只想在任何调用 join 方法的地方提供 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 库的其余部分应该会被移除。

细粒度垫片

一些遗留模块依赖于 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());

当模块在 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');,一切都应该顺利运行。

加载 Polyfills

到目前为止我们讨论的几乎所有内容都与处理遗留包有关。让我们转向第二个主题:**polyfills**。

有很多方法可以加载 polyfills。例如,要包含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());

请注意,这种方法优先考虑正确性而非打包文件大小。为了安全和健壮,polyfills/shims 必须在**所有其他代码之前**运行,因此要么需要同步加载,要么所有应用程序代码都需要在所有 polyfills/shims 加载之后加载。社区中也有许多误解,认为现代浏览器“不需要” polyfills,或者 polyfills/shims 只是为了添加缺失的功能——事实上,它们即使在最现代的浏览器中也经常*修复损坏的实现*。因此,最佳实践仍然是无条件地同步加载所有 polyfills/shims,尽管这会带来打包文件大小的成本。

如果你认为你已经缓解了这些担忧并希望承担中断的风险,这里有一种方法可以做到:让我们将 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 文件。你如何做出这个决定取决于你需要支持的技术和浏览器。我们将进行一些测试来确定是否需要我们的 polyfills

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 文件,并且一切都应该在浏览器中顺利运行。请注意,这种设置可能还有改进的空间,但它应该能让你很好地了解如何仅向实际需要 polyfills 的用户提供它们。

进一步优化

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,可以直接从你的配置文件中进行 polyfill,无需使用任何特殊的加载器或插件。有关更多信息和示例,请参阅Node 配置页面

其他工具

在处理遗留模块时,还有一些其他工具可以提供帮助。

当模块没有 AMD/CommonJS 版本并且你想包含 dist 时,你可以在noParse中标记此模块。这将导致 webpack 包含该模块而不对其进行解析或解析 require()import 语句。这种做法也用于提高构建性能。

最后,有些模块支持多种模块风格;例如,AMD、CommonJS 和遗留模块的组合。在大多数情况下,它们首先检查 define,然后使用一些奇特的代码来导出属性。在这种情况下,通过imports-loader设置 additionalCode=var%20define%20=%20false; 来强制使用 CommonJS 路径可能会有所帮助。

14 贡献者

pksjcejhnnssimon04jeremenichellisvyandunbyzykEugeneHlushkoAnayaDesigndhurlburtusaplr108NicolasLetellierwizardofhogwartssnitin315chenxsan