Shimming 垫片
Shimming 垫片
webpack 编译器可以理解使用 ES2015 模块、CommonJS 或 AMD 编写的模块。然而,一些第三方库可能期望全局依赖(例如 jQuery 的 $)。这些库也可能创建需要导出的全局变量。这些“损坏的模块”就是 垫片(shimming) 发挥作用的一种情况。
W> 我们不建议使用全局变量! webpack 背后的整个概念是允许更模块化的前端开发。这意味着编写隔离的、良好封装的模块,并且不依赖隐藏的依赖(例如全局变量)。请仅在必要时使用这些功能。
垫片 的另一个有用场景是当你想要 polyfill 浏览器功能以支持更多用户时。在这种情况下,你可能只想将那些 polyfill 提供给需要修补的浏览器(即按需加载它们)。
以下文章将逐步介绍这两种用例。
T> 为简单起见,本指南源自 起步 中的示例。在继续之前,请确保你熟悉那里的设置。
垫片全局变量
让我们从垫片全局变量的第一个用例开始。在我们做任何事情之前,先重新看一下我们的项目:
project
diff
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
│ └── index.html
├── /src
│ └── index.js
└── /node_modules
还记得我们使用的 lodash 包吗?为了演示目的,假设我们想将其作为全局变量在整个应用程序中提供。为此,我们可以使用 ProvidePlugin。
ProvidePlugin 使一个包可以作为变量在通过 webpack 编译的每个模块中使用。如果 webpack 看到该变量被使用,它将在最终 bundle 中包含给定的包。让我们继续,删除 lodash 的 import 语句,并通过插件提供它:
src/index.js
diff
-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
diff
import path from "node:path";
import { fileURLToPath } from 'url';
+import webpack from "webpack";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
+ }),
+ ],
};
我们本质上所做的是告诉 webpack……
如果你遇到至少一次变量
_的使用,请包含lodash包并将其提供给需要它的模块。
如果我们运行构建,我们应该仍然看到相同的输出:
bash
$ 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.x.x compiled successfully in 2910 ms
我们还可以使用 ProvidePlugin 通过配置“数组路径”(例如 [module, child, ...children?])来暴露模块的单个导出。那么,让我们假设我们只想在调用 join 方法时从 lodash 提供它:
src/index.js
diff
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
diff
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.ProvidePlugin({
- _: 'lodash',
+ join: ['lodash', 'join'],
}),
],
};
这将与 Tree Shaking 很好地配合,因为 lodash 库的其余部分应该会被丢弃。
细粒度垫片
一些遗留模块依赖 this 为 window 对象。让我们更新 index.js 使其成为这种情况:
diff
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
diff
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
exports default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: import.meta.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
全局导出
假设一个库创建了一个全局变量,并期望其使用者使用该变量。我们可以在我们的设置中添加一个小模块来演示这一点:
project
diff
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
├── /src
│ ├── index.js
+ │ ├── globals.js
└── /node_modules
src/globals.js
js
const file = "blah.txt";
const helpers = {
test() {
console.log("test something");
},
parse() {
console.log("parse something");
},
};
现在,虽然你很可能不会在自己的源代码中这样做,但你可能会遇到一个过时的库,其中包含类似上面所示的代码,而你又想使用它。在这种情况下,我们可以使用 exports-loader 将该全局变量作为普通模块导出。例如,要将 file 导出为 file,并将 helpers.parse 导出为 parse:
webpack.config.js
diff
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: import.meta.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: import.meta.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,我们可以:
bash
npm install --save babel-polyfill
并 import 它以将其包含在我们的主 bundle 中:
src/index.js
diff
+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());
T> 注意,我们没有将 import 绑定到变量。这是因为 polyfills 在代码库的其余部分之前独立运行,从而允许我们假设某些原生功能存在。
请注意,这种方法优先考虑正确性而不是 bundle 大小。为了安全和健壮,polyfills/垫片必须在所有其他代码之前运行,因此要么需要同步加载,要么所有应用程序代码都需要在所有 polyfills/垫片加载之后加载。
社区中也有很多误解,认为现代浏览器“不需要”polyfills,或者 polyfills/垫片仅仅用于添加缺失的功能——事实上,它们通常是_修复损坏的实现_,即使在最现代的浏览器中也是如此。
因此,最佳实践仍然是无条件地同步加载所有 polyfills/垫片,尽管这会增加 bundle 大小的成本。
如果你认为已经缓解了这些问题,并且希望承担损坏的风险,这里有一个可能的方法:
让我们将 import 移到一个新文件中,并添加 whatwg-fetch polyfill:
bash
npm install --save whatwg-fetch
src/index.js
diff
-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
diff
webpack-demo
├── package.json
├── package-lock.json
├── webpack.config.js
├── /dist
├── /src
│ ├── index.js
│ ├── globals.js
+ │ └── polyfills.js
└── /node_modules
src/polyfills.js
js
import "babel-polyfill";
import "whatwg-fetch";
webpack.config.js
diff
import path from "node:path";
import webpack from "webpack";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
- 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: fileURLToPath(import.meta.resolve('./src/index.js')),
use: 'imports-loader?wrapper=window',
},
{
test: fileURLToPath(import.meta.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
diff
<!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
diff
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 仅转译你的浏览器矩阵中不受支持的内容。这个 preset 带有 useBuiltIns 选项,默认值为 false,它会将你的全局 babel-polyfill 导入转换为更细粒度的、逐个功能的 import 模式:
{/* eslint-skip */}
js
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,而无需使用任何特殊的 loader 或插件。更多信息和示例请参阅 Node 配置页面。
其他工具
还有一些其他工具可以帮助处理遗留模块。
当模块没有 AMD/CommonJS 版本,并且你想包含 dist 文件时,可以在 noParse 中标记该模块。这将使 webpack 包含该模块而不解析它或解析 import 和 require() 语句。这种做法也用于提高构建性能。
W> 任何需要 AST 的功能(如 ProvidePlugin)将无法工作。
最后,有些模块支持多种 模块风格;例如,AMD、CommonJS 和遗留风格的组合。在大多数情况下,它们会首先检查 define,然后使用一些古怪的代码来导出属性。在这种情况下,通过 imports-loader 设置 additionalCode=var%20define%20=%20false; 可能有助于强制走 CommonJS 路径。
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
