溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

webpack文件打包機(jī)制的示例分析

發(fā)布時(shí)間:2021-08-17 10:25:29 來源:億速云 閱讀:195 作者:小新 欄目:web開發(fā)

這篇文章主要為大家展示了“webpack文件打包機(jī)制的示例分析”,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“webpack文件打包機(jī)制的示例分析”這篇文章吧。

前言

最近在重拾 webpack 一些知識(shí)點(diǎn),希望對(duì)前端模塊化有更多的理解,以前對(duì) webpack 打包機(jī)制有所好奇,沒有理解深入,淺嘗則止,最近通過對(duì) webpack 打包后的文件進(jìn)行查閱,對(duì)其如何打包 JS 文件有了更深的理解,希望通過這篇文章,能夠幫助讀者你理解:

  1. webpack 單文件如何進(jìn)行打包?

  2. webpack 多文件如何進(jìn)行代碼切割?

  3. webpack1 和 webpack2 在文件打包上有什么區(qū)別?

  4. webpack2 如何做到 tree shaking?

  5. webpack3 如何做到 scope hoisting?

本文所有示例代碼全部放在我的 Github 上,看興趣的可以看看:

git clone https://github.com/happylindz/blog.git
cd blog/code/webpackBundleAnalysis
npm install

webpack 單文件如何打包?

首先現(xiàn)在 webpack 作為當(dāng)前主流的前端模塊化工具,在 webpack 剛開始流行的時(shí)候,我們經(jīng)常通過 webpack 將所有處理文件全部打包成一個(gè) bundle 文件, 先通過一個(gè)簡單的例子來看:

// src/single/index.js
var index2 = require('./index2');
var util = require('./util');
console.log(index2);
console.log(util);

// src/single/index2.js
var util = require('./util');
console.log(util);
module.exports = "index 2";

// src/single/util.js
module.exports = "Hello World";

// 通過 config/webpack.config.single.js 打包
const webpack = require('webpack');
const path = require('path')

module.exports = {
 entry: {
 index: [path.resolve(__dirname, '../src/single/index.js')],
 },
 output: {
 path: path.resolve(__dirname, '../dist'),
 filename: '[name].[chunkhash:8].js'
 },
}

通過 npm run build:single 可看到打包效果,打包內(nèi)容大致如下(經(jīng)過精簡):

// dist/index.xxxx.js
(function(modules) {
 // 已經(jīng)加載過的模塊
 var installedModules = {};

 // 模塊加載函數(shù)
 function __webpack_require__(moduleId) {
 if(installedModules[moduleId]) {
  return installedModules[moduleId].exports;
 }
 var module = installedModules[moduleId] = {
  i: moduleId,
  l: false,
  exports: {}
 };
 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 module.l = true;
 return module.exports;
 }
 return __webpack_require__(__webpack_require__.s = 3);
})([
/* 0 */
(function(module, exports, __webpack_require__) {
 var util = __webpack_require__(1);
 console.log(util);
 module.exports = "index 2";
}),
/* 1 */
(function(module, exports) {
 module.exports = "Hello World";
}),
/* 2 */
(function(module, exports, __webpack_require__) {
 var index2 = __webpack_require__(0);
 index2 = __webpack_require__(0);
 var util = __webpack_require__(1);
 console.log(index2);
 console.log(util);
}),
/* 3 */
(function(module, exports, __webpack_require__) {
 module.exports = __webpack_require__(2);
})]);

將相對(duì)無關(guān)的代碼剔除掉后,剩下主要的代碼:

  1. 首先 webpack 將所有模塊(可以簡單理解成文件)包裹于一個(gè)函數(shù)中,并傳入默認(rèn)參數(shù),這里有三個(gè)文件再加上一個(gè)入口模塊一共四個(gè)模塊,將它們放入一個(gè)數(shù)組中,取名為 modules,并通過數(shù)組的下標(biāo)來作為 moduleId。

  2. 將 modules 傳入一個(gè)自執(zhí)行函數(shù)中,自執(zhí)行函數(shù)中包含一個(gè) installedModules 已經(jīng)加載過的模塊和一個(gè)模塊加載函數(shù),最后加載入口模塊并返回。

  3. __webpack_require__ 模塊加載,先判斷 installedModules 是否已加載,加載過了就直接返回 exports 數(shù)據(jù),沒有加載過該模塊就通過 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 執(zhí)行模塊并且將 module.exports 給返回。

很簡單是不是,有些點(diǎn)需要注意的是:

  1. 每個(gè)模塊 webpack 只會(huì)加載一次,所以重復(fù)加載的模塊只會(huì)執(zhí)行一次,加載過的模塊會(huì)放到 installedModules,下次需要需要該模塊的值就直接從里面拿了。

  2. 模塊的 id 直接通過數(shù)組下標(biāo)去一一對(duì)應(yīng)的,這樣能保證簡單且唯一,通過其它方式比如文件名或文件路徑的方式就比較麻煩,因?yàn)槲募赡艹霈F(xiàn)重名,不唯一,文件路徑則會(huì)增大文件體積,并且將路徑暴露給前端,不夠安全。

  3. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 保證了模塊加載時(shí) this 的指向 module.exports 并且傳入默認(rèn)參數(shù),很簡單,不過多解釋。

webpack 多文件如何進(jìn)行代碼切割?

webpack 單文件打包的方式應(yīng)付一些簡單場景就足夠了,但是我們在開發(fā)一些復(fù)雜的應(yīng)用,如果沒有對(duì)代碼進(jìn)行切割,將第三方庫(jQuery)或框架(React)和業(yè)務(wù)代碼全部打包在一起,就會(huì)導(dǎo)致用戶訪問頁面速度很慢,不能有效利用緩存,你的老板可能就要找你談話了。

那么 webpack 多文件入口如何進(jìn)行代碼切割,讓我先寫一個(gè)簡單的例子:

// src/multiple/pageA.js
const utilA = require('./js/utilA');
const utilB = require('./js/utilB');
console.log(utilA);
console.log(utilB);

// src/multiple/pageB.js
const utilB = require('./js/utilB');
console.log(utilB);
// 異步加載文件,類似于 import()
const utilC = () => require.ensure(['./js/utilC'], function(require) {
 console.log(require('./js/utilC'))
});
utilC();

// src/multiple/js/utilA.js 可類比于公共庫,如 jQuery
module.exports = "util A";

// src/multiple/js/utilB.js
module.exports = 'util B';

// src/multiple/js/utilC.js
module.exports = "util C";

這里我們定義了兩個(gè)入口 pageA 和 pageB 和三個(gè)庫 util,我們希望代碼切割做到:

  1. 因?yàn)閮扇肟诙际怯玫搅?utilB,我們希望把它抽離成單獨(dú)文件,并且當(dāng)用戶訪問 pageA 和 pageB 的時(shí)候都能去加載 utilB 這個(gè)公共模塊,而不是存在于各自的入口文件中。

  2. pageB 中 utilC 不是頁面一開始加載時(shí)候就需要的內(nèi)容,假如 utilC 很大,我們不希望頁面加載時(shí)就直接加載 utilC,而是當(dāng)用戶達(dá)到某種條件(如:點(diǎn)擊按鈕)才去異步加載 utilC,這時(shí)候我們需要將 utilC 抽離成單獨(dú)文件,當(dāng)用戶需要的時(shí)候再去加載該文件。

那么 webpack 需要怎么配置呢?

// 通過 config/webpack.config.multiple.js 打包
const webpack = require('webpack');
const path = require('path')

module.exports = {
 entry: {
 pageA: [path.resolve(__dirname, '../src/multiple/pageA.js')],
 pageB: path.resolve(__dirname, '../src/multiple/pageB.js'),
 },
 output: {
 path: path.resolve(__dirname, '../dist'),
 filename: '[name].[chunkhash:8].js',
 },
 plugins: [
 new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: 2,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  chunks: ['vendor']
 })
 ]
}

單單配置多 entry 是不夠的,這樣只會(huì)生成兩個(gè) bundle 文件,將 pageA 和 pageB 所需要的內(nèi)容全部放入,跟單入口文件并沒有區(qū)別,要做到代碼切割,我們需要借助 webpack 內(nèi)置的插件 CommonsChunkPlugin。

首先 webpack 執(zhí)行存在一部分運(yùn)行時(shí)代碼,即一部分初始化的工作,就像之前單文件中的 __webpack_require__ ,這部分代碼需要加載于所有文件之前,相當(dāng)于初始化工作,少了這部分初始化代碼,后面加載過來的代碼就無法識(shí)別并工作了。

new webpack.optimize.CommonsChunkPlugin({
 name: 'vendor',
 minChunks: 2,
})

這段代碼的含義是,在這些入口文件中,找到那些引用兩次的模塊(如:utilB),幫我抽離成一個(gè)叫 vendor 文件,此時(shí)那部分初始化工作的代碼會(huì)被抽離到 vendor 文件中。

new webpack.optimize.CommonsChunkPlugin({
 name: 'manifest',
 chunks: ['vendor'],
 // minChunks: Infinity // 可寫可不寫
})

這段代碼的含義是在 vendor 文件中幫我把初始化代碼抽離到 mainifest 文件中,此時(shí) vendor 文件中就只剩下 utilB 這個(gè)模塊了。你可能會(huì)好奇為什么要這么做?

因?yàn)檫@樣可以給 vendor 生成穩(wěn)定的 hash 值,每次修改業(yè)務(wù)代碼(pageA),這段初始化時(shí)代碼就會(huì)發(fā)生變化,那么如果將這段初始化代碼放在 vendor 文件中的話,每次都會(huì)生成新的 vendor.xxxx.js,這樣不利于持久化緩存,如果不理解也沒關(guān)系,下次我會(huì)另外寫一篇文章來講述這部分內(nèi)容。

另外 webpack 默認(rèn)會(huì)抽離異步加載的代碼,這個(gè)不需要你做額外的配置,pageB 中異步加載的 utilC 文件會(huì)直接抽離為 chunk.xxxx.js 文件。

所以這時(shí)候我們頁面加載文件的順序就會(huì)變成:

mainifest.xxxx.js // 初始化代碼
vendor.xxxx.js // pageA 和 pageB 共同用到的模塊,抽離
pageX.xxxx.js  // 業(yè)務(wù)代碼 
當(dāng) pageB 需要 utilC 時(shí)候則異步加載 utilC

執(zhí)行 npm run build:multiple 即可查看打包內(nèi)容,首先來看下 manifest 如何做初始化工作(精簡版)?

// dist/mainifest.xxxx.js
(function(modules) { 
 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
 var moduleId, chunkId, i = 0, callbacks = [];
 for(;i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  if(installedChunks[chunkId])
  callbacks.push.apply(callbacks, installedChunks[chunkId]);
  installedChunks[chunkId] = 0;
 }
 for(moduleId in moreModules) {
  if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  modules[moduleId] = moreModules[moduleId];
  }
 }
 while(callbacks.length)
  callbacks.shift().call(null, __webpack_require__);
 if(moreModules[0]) {
  installedModules[0] = 0;
  return __webpack_require__(0);
 }
 };
 var installedModules = {};
 var installedChunks = {
 4:0
 };
 function __webpack_require__(moduleId) {
 // 和單文件一致
 }
 __webpack_require__.e = function requireEnsure(chunkId, callback) {
 if(installedChunks[chunkId] === 0)
  return callback.call(null, __webpack_require__);
 if(installedChunks[chunkId] !== undefined) {
  installedChunks[chunkId].push(callback);
 } else {
  installedChunks[chunkId] = [callback];
  var head = document.getElementsByTagName('head')[0];
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.charset = 'utf-8';
  script.async = true;
  script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";
  head.appendChild(script);
 }
 };
})([]);

與單文件內(nèi)容一致,定義了一個(gè)自執(zhí)行函數(shù),因?yàn)樗话魏文K,所以傳入一個(gè)空數(shù)組。除了定義了 __webpack_require__ ,還另外定義了兩個(gè)函數(shù)用來進(jìn)行加載模塊。

首先講解代碼前需要理解兩個(gè)概念,分別是 module 和 chunk

  1. chunk 代表生成后 js 文件,一個(gè) chunkId 對(duì)應(yīng)一個(gè)打包好的 js 文件(一共五個(gè)),從這段代碼可以看出,manifest 的 chunkId 為 4,并且從代碼中還可以看到:0-3 分別對(duì)應(yīng) pageA, pageB, 異步 utilC, vendor 公共模塊文件,這也就是我們?yōu)槭裁床荒軐⑦@段代碼放在 vendor 的原因,因?yàn)槲募?hash 值會(huì)變。內(nèi)容變了,vendor 生成的 hash 值也就變了。

  2. module 對(duì)應(yīng)著模塊,可以簡單理解為打包前每個(gè) js 文件對(duì)應(yīng)一個(gè)模塊,也就是之前 __webpack_require__ 加載的模塊,同樣的使用數(shù)組下標(biāo)作為 moduleId 且是唯一不重復(fù)的。

那么為什么要區(qū)分 chunk 和 module 呢?

首先使用 installedChunks 來保存每個(gè) chunkId 是否被加載過,如果被加載過,則說明該 chunk 中所包含的模塊已經(jīng)被放到了 modules 中,注意是 modules 而不是 installedModules。我們先來簡單看一下 vendor chunk 打包出來的內(nèi)容。

// vendor.xxxx.js
webpackJsonp([3,4],{
 3: (function(module, exports) {
 module.exports = 'util B';
 })
});

在執(zhí)行完 manifest 后就會(huì)先執(zhí)行 vendor 文件,結(jié)合上面 webpackJsonp 的定義,我們可以知道 [3, 4] 代表 chunkId,當(dāng)加載到 vendor 文件后,installedChunks[3] 和 installedChunks[4] 將會(huì)被置為 0,這表明 chunk3,chunk4 已經(jīng)被加載過了。

webpackJsonpCallback 一共有兩個(gè)參數(shù),chuckIds 一般包含該 chunk 文件依賴的 chunkId 以及自身 chunkId,moreModules 代表該 chunk 文件帶來新的模塊。

var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
 chunkId = chunkIds[i];
 if(installedChunks[chunkId])
 callbacks.push.apply(callbacks, installedChunks[chunkId]);
 installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
while(callbacks.length)
 callbacks.shift().call(null, __webpack_require__);
if(moreModules[0]) {
 installedModules[0] = 0;
 return __webpack_require__(0);
}

簡單說說 webpackJsonpCallback 做了哪些事,首先判斷 chunkIds 在 installedChunks 里有沒有回調(diào)函數(shù)函數(shù)未執(zhí)行完,有的話則放到 callbacks 里,并且等下統(tǒng)一執(zhí)行,并將 chunkIds 在 installedChunks 中全部置為 0, 然后將 moreModules 合并到 modules。

這里面只有 modules[0] 是不固定的,其它 modules 下標(biāo)都是唯一的,在打包的時(shí)候 webpack 已經(jīng)為它們統(tǒng)一編號(hào),而 0 則為入口文件即 pageA,pageB 各有一個(gè) module[0]。

然后將 callbacks 執(zhí)行并清空,保證了該模塊加載開始前所以前置依賴內(nèi)容已經(jīng)加載完畢,最后判斷 moreModules[0], 有值說明該文件為入口文件,則開始執(zhí)行入口模塊 0。

上面解釋了一大堆,但是像 pageA 這種同步加載 manifest, vendor 以及 pageA 文件來說,每次加載的時(shí)候 callbacks 都是為空的,因?yàn)樗鼈冊?installedChunks 中的值要嘛為 undefined(未加載), 要嘛為 0(已被加載)。installedChunks[chunkId] 的值永遠(yuǎn)為 false,所以在這種情況下 callbacks 里根本不會(huì)出現(xiàn)函數(shù),如果僅僅是考慮這樣的場景,上面的 webpackJsonpCallback 完全可以寫成下面這樣:

var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
 chunkId = chunkIds[i];
 installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
if(moreModules[0]) {
 installedModules[0] = 0;
 return __webpack_require__(0);
}

但是考慮到異步加載 js 文件的時(shí)候(比如 pageB 異步加載 utilC 文件),就沒那么簡單,我們先來看下 webpack 是如何加載異步腳本的:

// 異步加載函數(shù)掛載在 __webpack_require__.e 上
__webpack_require__.e = function requireEnsure(chunkId, callback) {
 if(installedChunks[chunkId] === 0)
 return callback.call(null, __webpack_require__);
  
 if(installedChunks[chunkId] !== undefined) {
 installedChunks[chunkId].push(callback);
 } else {
 installedChunks[chunkId] = [callback];
 var head = document.getElementsByTagName('head')[0];
 var script = document.createElement('script');
 script.type = 'text/javascript';
 script.charset = 'utf-8';
 script.async = true;

 script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";
 head.appendChild(script);
 }
};

大致分為三種情況,(已經(jīng)加載過,正在加載中以及從未加載過)

  1. 已經(jīng)加載過該 chunk 文件,那就不用再重新加載該 chunk 了,直接執(zhí)行回調(diào)函數(shù)即可,可以理解為假如頁面有兩種操作需要加載加載異步腳本,但是兩個(gè)腳本都依賴于公共模塊,那么第二次加載的時(shí)候發(fā)現(xiàn)之前第一次操作已經(jīng)加載過了該 chunk,則不用再去獲取異步腳本了,因?yàn)樵摴材K已經(jīng)被執(zhí)行過了。

  2. 從未加載過,則動(dòng)態(tài)地去插入 script 腳本去請求 js 文件,這也就為什么取名 webpackJsonpCallback ,因?yàn)楦?jsonp 的思想很類似,所以這種異步加載腳本在做腳本錯(cuò)誤監(jiān)控時(shí)經(jīng)常出現(xiàn) Script error,具體原因可以查看我之前寫的文章:前端代碼異常監(jiān)控實(shí)戰(zhàn)

  3. 正在加載中代表該 chunk 文件已經(jīng)在加載中了,比如說點(diǎn)擊按鈕觸發(fā)異步腳本,用戶點(diǎn)太快了,連點(diǎn)兩次就可能出現(xiàn)這種情況,此時(shí)將回調(diào)函數(shù)放入 installedChunks。

我們通過 utilC 生成的 chunk 來進(jìn)行講解:

webpackJsonp([2,4],{
 4: (function(module, exports) {
 module.exports = "util C";
 })
});

pageB 需要異步加載這個(gè) chunk:

webpackJsonp([1,4],[
/* 0 */
 (function(module, exports, __webpack_require__) {
 const utilB = __webpack_require__(3);
 console.log(utilB);
 const utilC = () => __webpack_require__.e/* nsure */(2, function(require) {
  console.log(__webpack_require__(4))
 });
 utilC();
 })
]);

當(dāng) pageB 進(jìn)行某種操作需要加載 utilC 時(shí)就會(huì)執(zhí)行 __webpack_require__.e(2, callback) 2,代表需要加載的模塊 chunkId(utilC),異步加載 utilC 并將 callback 添加到 installedChunks[2] 中,然后當(dāng) utilC 的 chunk 文件加載完畢后,chunkIds 包含 2,發(fā)現(xiàn) installedChunks[2] 是個(gè)數(shù)組,里面還有之前還未執(zhí)行的 callback 函數(shù)。

既然這樣,那我就將我自己帶來的模塊先放到 modules 中,然后再統(tǒng)一執(zhí)行之前未執(zhí)行完的 callbacks 函數(shù),這里指的是存放于 installedChunks[2] 中的回調(diào)函數(shù) (可能存在多個(gè)),這也就是說明這里的先后順序:

// 先將 moreModules 合并到 modules, 再去執(zhí)行 callbacks, 不然之前未執(zhí)行的 callback 依賴于新來的模塊,你不放進(jìn) module 我豈不是得不到想要的模塊
for(moduleId in moreModules) {
 if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 modules[moduleId] = moreModules[moduleId];
 }
}
while(callbacks.length)
 callbacks.shift().call(null, __webpack_require__);

webpack1 和 webpack2 在文件打包上有什么區(qū)別?

經(jīng)過我對(duì)打包文件的觀察,從 webpack1 到 webpack2 在打包文件上有下面這些主要的改變:

首先,moduleId[0] 不再為入口執(zhí)行函數(shù)做保留,所以說不用傻傻看到 moduleId[0] 就認(rèn)為是打包文件的入口模塊,取而代之的是 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {} 傳入了第三個(gè)參數(shù) executeModules,是個(gè)數(shù)組,如果參數(shù)存在則說明它是入口模塊,然后就去執(zhí)行該模塊。

if(executeModules) {
 for(i=0; i < executeModules.length; i++) {
 result = __webpack_require__(__webpack_require__.s = executeModules[i]);
 }
}

其次,webpack2 中會(huì)默認(rèn)加載 OccurrenceOrderPlugin 這個(gè)插件,即你不用 plugins 中添加這個(gè)配置它也會(huì)默認(rèn)執(zhí)行,那它有什么用途呢?主要是在 webpack1 中 moduleId 的不確定性導(dǎo)致的,在 webpack1 中 moduleId 取決于引入文件的順序,這就會(huì)導(dǎo)致這個(gè) moduleId 可能會(huì)時(shí)常發(fā)生變化, 而 OccurrenceOrderPlugin 插件會(huì)按引入次數(shù)最多的模塊進(jìn)行排序,引入次數(shù)的模塊的 moduleId 越小,比如說上面引用的 utilB 模塊引用次數(shù)為 2(最多),所以它的 moduleId 為 0。

webpackJsonp([3],[
/* 0 */
 (function(module, exports) {
 module.exports = 'util B';
 })
]);

最后說下在異步加載模塊時(shí), webpack2 是基于 Promise 的,所以說如果你要兼容低版本瀏覽器,需要引入 Promise-polyfill ,另外為引入請求添加了錯(cuò)誤處理。

__webpack_require__.e = function requireEnsure(chunkId) {
 var promise = new Promise(function(resolve, reject) {
 installedChunkData = installedChunks[chunkId] = [resolve, reject];
 });
 installedChunkData[2] = promise;
 // start chunk loading
 var head = document.getElementsByTagName('head')[0];
 var script = document.createElement('script');
 script.type = 'text/javascript';
 script.charset = 'utf-8';
 script.async = true;
 script.timeout = 120000;
 script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f","1":"0ac69acb","2":"20651a9c","3":"0cdc6c84"}[chunkId] + ".js";
 var timeout = setTimeout(onScriptComplete, 120000);
 script.onerror = script.onload = onScriptComplete;
 function onScriptComplete() {
 // 防止內(nèi)存泄漏
 script.onerror = script.onload = null;
 clearTimeout(timeout);
 var chunk = installedChunks[chunkId];
 if(chunk !== 0) {
  if(chunk) {
  chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
  }
  installedChunks[chunkId] = undefined;
 }
 };
 head.appendChild(script);
 return promise;
};

可以看出,原本基于回調(diào)函數(shù)的方式已經(jīng)變成基于 Promise 做異步處理,另外添加了 onScriptComplete 用于做腳本加載失敗處理。

在 webpack1 的時(shí)候,如果由于網(wǎng)絡(luò)原因當(dāng)你加載腳本失敗后,即使網(wǎng)絡(luò)恢復(fù)了,你再次進(jìn)行某種操作需要同個(gè) chunk 時(shí)候都會(huì)無效,主要原因是失敗之后沒把 installedChunks[chunkId] = undefined; 導(dǎo)致之后不會(huì)再對(duì)該 chunk 文件發(fā)起異步請求。

而在 webpack2 中,當(dāng)腳本請求超時(shí)了(2min)或者加載失敗,會(huì)將 installedChunks[chunkId] 清空,當(dāng)下次重新請求該 chunk 文件會(huì)重新加載,提高了頁面的容錯(cuò)性。

這些是我在打包文件中看到主要的區(qū)別,難免有所遺漏,如果你有更多的見解,歡迎在評(píng)論區(qū)留言。

webpack2 如何做到 tree shaking?

什么是 tree shaking,即 webpack 在打包的過程中會(huì)將沒用的代碼進(jìn)行清除(dead code)。一般 dead code 具有一下的特征:

  1. 代碼不會(huì)被執(zhí)行,不可到達(dá)

  2. 代碼執(zhí)行的結(jié)果不會(huì)被用到

  3. 代碼只會(huì)影響死變量(只寫不讀)

是不是很神奇,那么需要怎么做才能使 tree shaking 生效呢?

首先,模塊引入要基于 ES6 模塊機(jī)制,不再使用 commonjs 規(guī)范,因?yàn)?es6 模塊的依賴關(guān)系是確定的,和運(yùn)行時(shí)的狀態(tài)無關(guān),可以進(jìn)行可靠的靜態(tài)分析,然后清除沒用的代碼。而 commonjs 的依賴關(guān)系是要到運(yùn)行時(shí)候才能確定下來的。

其次,需要開啟 UglifyJsPlugin 這個(gè)插件對(duì)代碼進(jìn)行壓縮。

我們先寫一個(gè)例子來說明:

// src/es6/pageA.js
import {
 utilA,
 funcA, // 引入 funcA 但未使用, 故 funcA 會(huì)被清除
} from './js/utilA';
import utilB from './js/utilB'; // 引入 utilB(函數(shù)) 未使用,會(huì)被清除
import classC from './js/utilC'; // 引入 classC(類) 未使用,不會(huì)被清除
console.log(utilA);

// src/es6/js/utilA.js
export const utilA = 'util A';
export function funcA() {
 console.log('func A');
}

// src/es6/js/utilB.js
export default function() {
 console.log('func B');
}
if(false) { // 被清除
 console.log('never use');
}
while(true) {}
console.log('never use');

// src/es6/js/utilC.js
const classC = function() {} // 類方法不會(huì)被清除
classC.prototype.saySomething = function() {
 console.log('class C');
}
export default classC;

打包的配置也很簡單:

const webpack = require('webpack');
const path = require('path')
module.exports = {
 entry: {
 pageA: path.resolve(__dirname, '../src/es6/pageA.js'),
 },
 output: {
 path: path.resolve(__dirname, '../dist'),
 filename: '[name].[chunkhash:8].js'
 },
 plugins: [
 new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  minChunks: Infinity,
 }),
 new webpack.optimize.UglifyJsPlugin({
  compress: {
  warnings: false
  }
 })
 ]
}

通過 npm run build:es6 對(duì)壓縮的文件進(jìn)行分析:

// dist/pageA.xxxx.js
webpackJsonp([0],[
 function(o, t, e) {
 'use strict';
 Object.defineProperty(t, '__esModule', { value: !0 });
 var n = e(1);
 e(2), e(3);
 console.log(n.a);
 },function(o, t, e) {
 'use strict';
 t.a = 'util A';
 },function(o, t, e) {
 'use strict';
 for (;;);
 console.log('never use');
 },
 function(o, t, e) {
 'use strict';
 const n = function() {};
 n.prototype.saySomething = function() {
  console.log('class C');
 };
 }
],[0]);

引入但是沒用的變量,函數(shù)都會(huì)清除,未執(zhí)行的代碼也會(huì)被清除。但是類方法是不會(huì)被清除的。因?yàn)?webpack 不會(huì)區(qū)分不了是定義在 classC 的 prototype 還是其它 Array 的 prototype 的,比如 classC 寫成下面這樣:

const classC = function() {}
var a = 'class' + 'C';
var b;
if(a === 'Array') {
 b = a;
}else {
 b = 'classC';
}
b.prototype.saySomething = function() {
 console.log('class C');
}
export default classC;

webpack 無法保證 prototype 掛載的對(duì)象是 classC,這種代碼,靜態(tài)分析是分析不了的,就算能靜態(tài)分析代碼,想要正確完全的分析也比較困難。所以 webpack 干脆不處理類方法,不對(duì)類方法進(jìn)行 tree shaking。

更多的 tree shaking 的副作用可以查閱: Tree shaking class methods

webpack3 如何做到 scope hoisting?

scope hoisting,顧名思義就是將模塊的作用域提升,在 webpack 中不能將所有所有的模塊直接放在同一個(gè)作用域下,有以下幾個(gè)原因:

  1. 按需加載的模塊

  2. 使用 commonjs 規(guī)范的模塊

  3. 被多 entry 共享的模塊

在 webpack3 中,這些情況生成的模塊不會(huì)進(jìn)行作用域提升,下面我就舉個(gè)例子來說明:

// src/hoist/utilA.js
export const utilA = 'util A';
export function funcA() {
 console.log('func A');
}

// src/hoist/utilB.js
export const utilB = 'util B';
export function funcB() {
 console.log('func B');
}

// src/hoist/utilC.js
export const utilC = 'util C';

// src/hoist/pageA.js
import { utilA, funcA } from './utilA';
console.log(utilA);
funcA();

// src/hoist/pageB.js
import { utilA } from './utilA';
import { utilB, funcB } from './utilB';

funcB();
import('./utilC').then(function(utilC) {
 console.log(utilC);
})

這個(gè)例子比較典型,utilA 被 pageA 和 pageB 所共享,utilB 被 pageB 單獨(dú)加載,utilC 被 pageB 異步加載。

想要 webpack3 生效,則需要在 plugins 中添加 ModuleConcatenationPlugin。

webpack 配置如下:

const webpack = require('webpack');
const path = require('path')
module.exports = {
 entry: {
 pageA: path.resolve(__dirname, '../src/hoist/pageA.js'),
 pageB: path.resolve(__dirname, '../src/hoist/pageB.js'),
 },
 output: {
 path: path.resolve(__dirname, '../dist'),
 filename: '[name].[chunkhash:8].js'
 },
 plugins: [
 new webpack.optimize.ModuleConcatenationPlugin(),
 new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: 2,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  minChunks: Infinity,
 })
 ]
}

運(yùn)行 npm run build:hoist 進(jìn)行編譯,簡單看下生成的 pageB 代碼:

webpackJsonp([2],{
 2: (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
 var utilA = __webpack_require__(0);
 // CONCATENATED MODULE: ./src/hoist/utilB.js
 const utilB = 'util B';
 function funcB() {
  console.log('func B');
 }
 // CONCATENATED MODULE: ./src/hoist/pageB.js
 funcB();
 __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 3)).then(function(utilC) {
  console.log(utilC);
 })
 })
},[2]);

通過代碼分析,可以得出下面的結(jié)論:

  1. 因?yàn)槲覀兣渲昧斯蚕砟K抽離,所以 utilA 被抽出為單獨(dú)模塊,故這部分內(nèi)容不會(huì)進(jìn)行作用域提升。

  2. utilB 無牽無掛,被 pageB 單獨(dú)加載,所以這部分不會(huì)生成新的模塊,而是直接作用域提升到 pageB 中。

  3. utilC 被異步加載,需要抽離成單獨(dú)模塊,很明顯沒辦法作用域提升。

以上是“webpack文件打包機(jī)制的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI