webpack compiler 能夠識(shí)別遵循 ES2015 模塊語(yǔ)法、CommonJS 或 AMD 規(guī)范編寫(xiě)的模塊。然而,一些 third party(第三方庫(kù)) 可能會(huì)引用一些全局依賴(lài)(例如 jQuery 中的 $)。因此這些 library 也可能會(huì)創(chuàng)建一些需要導(dǎo)出的全局變量。這些 "broken modules(不符合規(guī)范的模塊)" 就是 shimming(預(yù)置依賴(lài)) 發(fā)揮作用的地方。
shim 另外一個(gè)極其有用的使用場(chǎng)景就是:當(dāng)你希望 polyfill 擴(kuò)展瀏覽器能力,來(lái)支持到更多用戶(hù)時(shí)。在這種情況下,你可能只是想要將這些 polyfills 提供給需要修補(bǔ)(patch)的瀏覽器(也就是實(shí)現(xiàn)按需加載)。
下面的文章將向我們展示這兩種用例。
讓我們開(kāi)始第一個(gè) shimming 全局變量的用例。在此之前,先看下我們的項(xiàng)目:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
|- /node_modules
還記得我們之前用過(guò)的 lodash 嗎?出于演示目的,例如把這個(gè)應(yīng)用程序中的模塊依賴(lài),改為一個(gè)全局變量依賴(lài)。要實(shí)現(xiàn)這些,我們需要使用 ProvidePlugin 插件。
使用 ProvidePlugin 后,能夠在 webpack 編譯的每個(gè)模塊中,通過(guò)訪(fǎng)問(wèn)一個(gè)變量來(lái)獲取一個(gè) package。如果 webpack 看到模塊中用到這個(gè)變量,它將在最終 bundle 中引入給定的 package。讓我們先移除 lodash 的 import 語(yǔ)句,改為通過(guò)插件提供它:
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……
如果你遇到了至少一處用到 _ 變量的模塊實(shí)例,那請(qǐng)你將 lodash package 引入進(jìn)來(lái),并將其提供給需要用到它的模塊。
運(yùn)行我們的構(gòu)建腳本,將會(huì)看到同樣的輸出:
$ 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 暴露出某個(gè)模塊中單個(gè)導(dǎo)出,通過(guò)配置一個(gè)“數(shù)組路徑”(例如 [module, child, ...children?])實(shí)現(xiàn)此功能。所以,我們假想如下,無(wú)論 join 方法在何處調(diào)用,我們都只會(huì)獲取到 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 中的其余沒(méi)有用到的導(dǎo)出去除。
一些遺留模塊依賴(lài)的 this 指向的是 window 對(duì)象。在接下來(lái)的用例中,調(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)模塊運(yùn)行在 CommonJS 上下文中,這將會(huì)變成一個(gè)問(wèn)題,也就是說(shuō)此時(shí)的 this 指向的是 module.exports。在這種情況下,你可以通過(guò)使用 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'],
}),
],
};
讓我們假設(shè),某個(gè) library 創(chuàng)建出一個(gè)全局變量,它期望 consumer(使用者) 使用這個(gè)變量。為此,我們可以在項(xiàng)目配置中,添加一個(gè)小模塊來(lái)演示說(shuō)明:
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');
},
};
你可能從來(lái)沒(méi)有在自己的源碼中做過(guò)這些事情,但是你也許遇到過(guò)一個(gè)老舊的 library,和上面所展示的代碼類(lèi)似。在這種情況下,我們可以使用 exports-loader,將一個(gè)全局變量作為一個(gè)普通的模塊來(lái)導(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'],
}),
],
};
此時(shí),在我們的 entry 入口文件中(即 src/index.js),可以使用 const { file, parse } = require('./globals.js');,可以保證一切將順利運(yùn)行。
目前為止我們所討論的所有內(nèi)容都是處理那些遺留的 package,讓我們進(jìn)入到第二個(gè)話(huà)題:polyfill。
有很多方法來(lái)加載 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 必須運(yùn)行于所有其他代碼之前,而且需要同步加載,或者說(shuō),需要在所有 polyfill/shim 加載之后,再去加載所有應(yīng)用程序代碼。 社區(qū)中存在許多誤解,即現(xiàn)代瀏覽器“不需要”polyfill,或者 polyfill/shim 僅用于添加缺失功能 - 實(shí)際上,它們通常用于修復(fù)損壞實(shí)現(xiàn)(repair broken implementation),即使是在最現(xiàn)代的瀏覽器中,也會(huì)出現(xiàn)這種情況。 因此,最佳實(shí)踐仍然是,不加選擇地和同步地加載所有 polyfill/shim,盡管這會(huì)導(dǎo)致額外的 bundle 體積成本。
如果你認(rèn)為自己已經(jīng)打消這些顧慮,并且希望承受損壞的風(fēng)險(xiǎn)。那么接下來(lái)的這件事情,可能是你應(yīng)該要做的: 我們將會(huì)把 import 放入一個(gè)新文件,并加入 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ù)和瀏覽器來(lái)決定是否加載。我們將做一些簡(jiǎn)單的試驗(yàn),來(lái)確定是否需要引入這些 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 入口文件中,可以通過(guò) 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 文件請(qǐng)求,然后所有代碼順利執(zhí)行。注意,以上的這些設(shè)定可能還會(huì)有所改進(jìn),這里我們向你提供一個(gè)很棒的想法:將 polyfill 提供給需要引入它的用戶(hù)。
babel-preset-env package 通過(guò) browserslist 來(lái)轉(zhuǎn)譯那些你瀏覽器中不支持的特性。這個(gè) preset 使用 useBuiltIns 選項(xiàng),默認(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.
像 process 這種 Node 內(nèi)置模塊,能直接根據(jù)配置文件進(jìn)行正確的 polyfill,而不需要任何特定的 loader 或者 plugin。查看 node 配置頁(yè)面獲取更多信息。
還有一些其他的工具,也能夠幫助我們處理這些遺留模塊。
如果這些遺留模塊沒(méi)有 AMD/CommonJS 版本,但你也想將他們加入 dist 文件,則可以使用 noParse 來(lái)標(biāo)識(shí)出這個(gè)模塊。這樣就能使 webpack 將引入這些模塊,但是不進(jìn)行轉(zhuǎn)化(parse),以及不解析(resolve) require() 和 import 語(yǔ)句。這種用法還會(huì)提高構(gòu)建性能。
最后,一些模塊支持多種 模塊格式,例如一個(gè)混合有 AMD、CommonJS 和 legacy(遺留) 的模塊。在大多數(shù)這樣的模塊中,會(huì)首先檢查 define,然后使用一些怪異代碼導(dǎo)出一些屬性。在這些情況下,可以通過(guò) imports-loader 設(shè)置 additionalCode=var%20define%20=%20false; 來(lái)強(qiáng)制 CommonJS 路徑。
更多建議: