webpack
编译器可以理解以 ES2015 模块、CommonJS 或 AMD 形式编写的模块。但是,一些第三方库可能期望全局依赖项(例如,$
用于 jQuery
)。这些库也可能创建需要导出的全局变量。这些“损坏的模块”是垫片发挥作用的一个实例。
垫片可以发挥作用的另一个实例是,当您想要填充浏览器功能以支持更多用户时。在这种情况下,您可能只想将这些填充程序提供给需要修补的浏览器(即按需加载它们)。
以下文章将逐步介绍这两个用例。
让我们从垫片全局变量的第一个用例开始。在我们做任何事情之前,让我们再看一下我们的项目
项目
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...
如果您遇到至少一个变量
_
的实例,请包含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?]
)对其进行配置来公开模块的单个导出。因此,假设我们只想在调用 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
库应该会被丢弃。
一些旧版模块依赖于 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());
当模块在 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');
,一切应该顺利运行。
到目前为止,我们讨论的几乎所有内容都与处理旧版包有关。让我们继续我们的第二个主题: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/垫片必须在所有其他代码之前运行,因此需要同步加载,或者,所有应用程序代码都需要在所有 polyfills/垫片加载后加载。社区中也有很多误解,认为现代浏览器“不需要”polyfills,或者 polyfills/垫片只是为了添加缺失的功能——实际上,它们通常会修复损坏的实现,即使是在最现代的浏览器中也是如此。因此,最佳实践仍然是无条件地同步加载所有 polyfills/垫片,尽管这会导致包大小成本。
如果您认为您已经缓解了这些问题,并且希望承担代码崩溃的风险,以下是一种方法:将我们的 `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 内置函数,如 `process`,可以直接从您的配置文件中进行 polyfill,而无需使用任何特殊的加载器或插件。有关更多信息和示例,请参阅 node 配置页面。
在处理遗留模块时,还有一些其他工具可以提供帮助。
当模块没有 AMD/CommonJS 版本,并且您想包含 `dist` 时,您可以在 noParse
中标记此模块。这将导致 webpack 包含模块,而不会解析它或解析 `require()` 和 `import` 语句。此做法也用于提高构建性能。
最后,有一些模块支持多种 模块样式;例如,AMD、CommonJS 和遗留的组合。在大多数情况下,它们首先检查 `define`,然后使用一些奇怪的代码来导出属性。在这些情况下,通过 imports-loader
设置 `additionalCode=var%20define%20=%20false;` 来强制使用 CommonJS 路径可能会有所帮助。