前言
最近在重拾 webpack 一些知識點,希望對前端模組化有更多的理解,以前對 webpack 打包機制有所好奇,沒有理解深入,淺嘗則止,最近通過對 webpack 打包後的檔案進行查閱,對其如何打包 JS 檔案有了更深的理解,希望通過這篇文章,能夠幫助讀者你理解:
- webpack 單檔案如何進行打包?
- webpack 多檔案如何進行程式碼切割?
- webpack1 和 webpack2 在檔案打包上有什麼區別?
- webpack2 如何做到 tree shaking?
- webpack3 如何做到 scope hoisting?
本文所有示例程式碼全部放在我的 Github 上,看興趣的可以看看:
1 2 3 |
git clone https://github.com/happylindz/blog.git cd blog/code/webpackBundleAnalysis npm install |
webpack 單檔案如何打包?
首先現在 webpack 作為當前主流的前端模組化工具,在 webpack 剛開始流行的時候,我們經常通過 webpack 將所有處理檔案全部打包成一個 bundle 檔案, 先通過一個簡單的例子來看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 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
可看到打包效果,打包內容大致如下(經過精簡):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// dist/index.xxxx.js (function(modules) { // 已經載入過的模組 var installedModules = {}; // 模組載入函式 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); })]); |
將相對無關的程式碼剔除掉後,剩下主要的程式碼:
- 首先 webpack 將所有模組(可以簡單理解成檔案)包裹於一個函式中,並傳入預設引數,這裡有三個檔案再加上一個入口模組一共四個模組,將它們放入一個陣列中,取名為 modules,並通過陣列的下標來作為 moduleId。
- 將 modules 傳入一個自執行函式中,自執行函式中包含一個 installedModules 已經載入過的模組和一個模組載入函式,最後載入入口模組並返回。
__webpack_require__
模組載入,先判斷 installedModules 是否已載入,載入過了就直接返回 exports 資料,沒有載入過該模組就通過modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
執行模組並且將 module.exports 給返回。
很簡單是不是,有些點需要注意的是:
- 每個模組 webpack 只會載入一次,所以重複載入的模組只會執行一次,載入過的模組會放到 installedModules,下次需要需要該模組的值就直接從裡面拿了。
- 模組的 id 直接通過陣列下標去一一對應的,這樣能保證簡單且唯一,通過其它方式比如檔名或檔案路徑的方式就比較麻煩,因為檔名可能出現重名,不唯一,檔案路徑則會增大檔案體積,並且將路徑暴露給前端,不夠安全。
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
保證了模組載入時 this 的指向 module.exports 並且傳入預設引數,很簡單,不過多解釋。
webpack 多檔案如何進行程式碼切割?
webpack 單檔案打包的方式應付一些簡單場景就足夠了,但是我們在開發一些複雜的應用,如果沒有對程式碼進行切割,將第三方庫(jQuery)或框架(React) 和業務程式碼全部打包在一起,就會導致使用者訪問頁面速度很慢,不能有效利用快取,你的老闆可能就要找你談話了。
那麼 webpack 多檔案入口如何進行程式碼切割,讓我先寫一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 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"; |
這裡我們定義了兩個入口 pageA 和 pageB 和三個庫 util,我們希望程式碼切割做到:
- 因為兩入口都是用到了 utilB,我們希望把它抽離成單獨檔案,並且當使用者訪問 pageA 和 pageB 的時候都能去載入 utilB 這個公共模組,而不是存在於各自的入口檔案中。
- pageB 中 utilC 不是頁面一開始載入時候就需要的內容,假如 utilC 很大,我們不希望頁面載入時就直接載入 utilC,而是當使用者達到某種條件(如:點選按鈕)才去非同步載入 utilC,這時候我們需要將 utilC 抽離成單獨檔案,當使用者需要的時候再去載入該檔案。
那麼 webpack 需要怎麼配置呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 通過 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 是不夠的,這樣只會生成兩個 bundle 檔案,將 pageA 和 pageB 所需要的內容全部放入,跟單入口檔案並沒有區別,要做到程式碼切割,我們需要藉助 webpack 內建的外掛 CommonsChunkPlugin。
首先 webpack 執行存在一部分執行時程式碼,即一部分初始化的工作,就像之前單檔案中的 __webpack_require__
,這部分程式碼需要載入於所有檔案之前,相當於初始化工作,少了這部分初始化程式碼,後面載入過來的程式碼就無法識別並工作了。
1 2 3 4 |
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: 2, }) |
這段程式碼的含義是,在這些入口檔案中,找到那些引用兩次的模組(如:utilB),幫我抽離成一個叫 vendor 檔案,此時那部分初始化工作的程式碼會被抽離到 vendor 檔案中。
1 2 3 4 5 |
new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'], // minChunks: Infinity // 可寫可不寫 }) |
這段程式碼的含義是在 vendor 檔案中幫我把初始化程式碼抽離到 mainifest 檔案中,此時 vendor 檔案中就只剩下 utilB 這個模組了。你可能會好奇為什麼要這麼做?
因為這樣可以給 vendor 生成穩定的 hash 值,每次修改業務程式碼(pageA),這段初始化時程式碼就會發生變化,那麼如果將這段初始化程式碼放在 vendor 檔案中的話,每次都會生成新的 vendor.xxxx.js,這樣不利於持久化快取,如果不理解也沒關係,下次我會另外寫一篇文章來講述這部分內容。
另外 webpack 預設會抽離非同步載入的程式碼,這個不需要你做額外的配置,pageB 中非同步載入的 utilC 檔案會直接抽離為 chunk.xxxx.js 檔案。
所以這時候我們頁面載入檔案的順序就會變成:
1 2 3 4 5 |
mainifest.xxxx.js // 初始化程式碼 vendor.xxxx.js // pageA 和 pageB 共同用到的模組,抽離 pageX.xxxx.js // 業務程式碼 當 pageB 需要 utilC 時候則非同步載入 utilC |
執行 npm run build:multiple
即可檢視打包內容,首先來看下 manifest 如何做初始化工作(精簡版)?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// 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); } }; })([]); |
與單檔案內容一致,定義了一個自執行函式,因為它不包含任何模組,所以傳入一個空陣列。除了定義了 __webpack_require__
,還另外定義了兩個函式用來進行載入模組。
首先講解程式碼前需要理解兩個概念,分別是 module 和 chunk
- chunk 代表生成後 js 檔案,一個 chunkId 對應一個打包好的 js 檔案(一共五個),從這段程式碼可以看出,manifest 的 chunkId 為 4,並且從程式碼中還可以看到:0-3 分別對應 pageA, pageB, 非同步 utilC, vendor 公共模組檔案,這也就是我們為什麼不能將這段程式碼放在 vendor 的原因,因為檔案的 hash 值會變。內容變了,vendor 生成的 hash 值也就變了。
- module 對應著模組,可以簡單理解為打包前每個 js 檔案對應一個模組,也就是之前
__webpack_require__
載入的模組,同樣的使用陣列下標作為 moduleId 且是唯一不重複的。
那麼為什麼要區分 chunk 和 module 呢?
首先使用 installedChunks 來儲存每個 chunkId 是否被載入過,如果被載入過,則說明該 chunk 中所包含的模組已經被放到了 modules 中,注意是 modules 而不是 installedModules。我們先來簡單看一下 vendor chunk 打包出來的內容。
1 2 3 4 5 6 |
// vendor.xxxx.js webpackJsonp([3,4],{ 3: (function(module, exports) { module.exports = 'util B'; }) }); |
在執行完 manifest 後就會先執行 vendor 檔案,結合上面 webpackJsonp 的定義,我們可以知道 [3, 4] 代表 chunkId,當載入到 vendor 檔案後,installedChunks[3] 和 installedChunks[4] 將會被置為 0,這表明 chunk3,chunk4 已經被載入過了。
webpackJsonpCallback
一共有兩個引數,chuckIds 一般包含該 chunk 檔案依賴的 chunkId 以及自身 chunkId,moreModules 代表該 chunk 檔案帶來新的模組。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 裡有沒有回撥函式函式未執行完,有的話則放到 callbacks 裡,並且等下統一執行,並將 chunkIds 在 installedChunks 中全部置為 0, 然後將 moreModules 合併到 modules。
這裡面只有 modules[0] 是不固定的,其它 modules 下標都是唯一的,在打包的時候 webpack 已經為它們統一編號,而 0 則為入口檔案即 pageA,pageB 各有一個 module[0]。
然後將 callbacks 執行並清空,保證了該模組載入開始前所以前置依賴內容已經載入完畢,最後判斷 moreModules[0], 有值說明該檔案為入口檔案,則開始執行入口模組 0。
上面解釋了一大堆,但是像 pageA 這種同步載入 manifest, vendor 以及 pageA 檔案來說,每次載入的時候 callbacks 都是為空的,因為它們在 installedChunks 中的值要嘛為 undefined(未載入), 要嘛為 0(已被載入)。installedChunks[chunkId] 的值永遠為 false,所以在這種情況下 callbacks 里根本不會出現函式,如果僅僅是考慮這樣的場景,上面的 webpackJsonpCallback
完全可以寫成下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 檔案的時候(比如 pageB 非同步載入 utilC 檔案),就沒那麼簡單,我們先來看下 webpack 是如何載入非同步指令碼的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 非同步載入函式掛載在 __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); } }; |
大致分為三種情況,(已經載入過,正在載入中以及從未載入過)
- 已經載入過該 chunk 檔案,那就不用再重新載入該 chunk 了,直接執行回撥函式即可,可以理解為假如頁面有兩種操作需要載入載入非同步指令碼,但是兩個指令碼都依賴於公共模組,那麼第二次載入的時候發現之前第一次操作已經載入過了該 chunk,則不用再去獲取非同步指令碼了,因為該公共模組已經被執行過了。
- 從未載入過,則動態地去插入 script 指令碼去請求 js 檔案,這也就為什麼取名 webpackJsonpCallback,因為跟 jsonp 的思想很類似,所以這種非同步載入指令碼在做指令碼錯誤監控時經常出現 Script error,具體原因可以檢視我之前寫的文章:前端程式碼異常監控實戰
- 正在載入中代表該 chunk 檔案已經在載入中了,比如說點選按鈕觸發非同步指令碼,使用者點太快了,連點兩次就可能出現這種情況,此時將回撥函式放入 installedChunks。
我們通過 utilC 生成的 chunk 來進行講解:
1 2 3 4 5 |
webpackJsonp([2,4],{ 4: (function(module, exports) { module.exports = "util C"; }) }); |
pageB 需要非同步載入這個 chunk:
1 2 3 4 5 6 7 8 9 10 11 |
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(); }) ]); |
當 pageB 進行某種操作需要載入 utilC 時就會執行 __webpack_require__.e(2, callback)
2,代表需要載入的模組 chunkId(utilC),非同步載入 utilC 並將 callback 新增到 installedChunks[2] 中,然後當 utilC 的 chunk 檔案載入完畢後,chunkIds 包含 2,發現 installedChunks[2] 是個陣列,裡面還有之前還未執行的 callback 函式。
既然這樣,那我就將我自己帶來的模組先放到 modules 中,然後再統一執行之前未執行完的 callbacks 函式,這裡指的是存放於 installedChunks[2] 中的回撥函式 (可能存在多個),這也就是說明這裡的先後順序:
1 2 3 4 5 6 7 8 |
// 先將 moreModules 合併到 modules, 再去執行 callbacks, 不然之前未執行的 callback 依賴於新來的模組,你不放進 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 在檔案打包上有什麼區別?
經過我對打包檔案的觀察,從 webpack1 到 webpack2 在打包檔案上有下面這些主要的改變:
首先,moduleId[0] 不再為入口執行函式做保留,所以說不用傻傻看到 moduleId[0] 就認為是打包檔案的入口模組,取而代之的是 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {}
傳入了第三個引數 executeModules,是個陣列,如果引數存在則說明它是入口模組,然後就去執行該模組。
1 2 3 4 5 |
if(executeModules) { for(i=0; i executeModules.length; i++) { result = __webpack_require__(__webpack_require__.s = executeModules[i]); } } |
其次,webpack2 中會預設載入 OccurrenceOrderPlugin 這個外掛,即你不用 plugins 中新增這個配置它也會預設執行,那它有什麼用途呢?主要是在 webpack1 中 moduleId 的不確定性導致的,在 webpack1 中 moduleId 取決於引入檔案的順序,這就會導致這個 moduleId 可能會時常發生變化, 而 OccurrenceOrderPlugin 外掛會按引入次數最多的模組進行排序,引入次數的模組的 moduleId 越小,比如說上面引用的 utilB 模組引用次數為 2(最多),所以它的 moduleId 為 0。
1 2 3 4 5 6 |
webpackJsonp([3],[ /* 0 */ (function(module, exports) { module.exports = 'util B'; }) ]); |
最後說下在非同步載入模組時, webpack2 是基於 Promise 的,所以說如果你要相容低版本瀏覽器,需要引入 Promise-polyfill
,另外為引入請求新增了錯誤處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
__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() { // 防止記憶體洩漏 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; }; |
可以看出,原本基於回撥函式的方式已經變成基於 Promise 做非同步處理,另外新增了 onScriptComplete
用於做指令碼載入失敗處理。
在 webpack1 的時候,如果由於網路原因當你載入指令碼失敗後,即使網路恢復了,你再次進行某種操作需要同個 chunk 時候都會無效,主要原因是失敗之後沒把 installedChunks[chunkId] = undefined;
導致之後不會再對該 chunk 檔案發起非同步請求。
而在 webpack2 中,當指令碼請求超時了(2min)或者載入失敗,會將 installedChunks[chunkId] 清空,當下次重新請求該 chunk 檔案會重新載入,提高了頁面的容錯性。
這些是我在打包檔案中看到主要的區別,難免有所遺漏,如果你有更多的見解,歡迎在評論區留言。
webpack2 如何做到 tree shaking?
什麼是 tree shaking,即 webpack 在打包的過程中會將沒用的程式碼進行清除(dead code)。一般 dead code 具有一下的特徵:
- 程式碼不會被執行,不可到達
- 程式碼執行的結果不會被用到
- 程式碼只會影響死變數(只寫不讀)
是不是很神奇,那麼需要怎麼做才能使 tree shaking 生效呢?
首先,模組引入要基於 ES6 模組機制,不再使用 commonjs 規範,因為 es6 模組的依賴關係是確定的,和執行時的狀態無關,可以進行可靠的靜態分析,然後清除沒用的程式碼。而 commonjs 的依賴關係是要到執行時候才能確定下來的。
其次,需要開啟 UglifyJsPlugin 這個外掛對程式碼進行壓縮。
我們先寫一個例子來說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// src/es6/pageA.js import { utilA, funcA, // 引入 funcA 但未使用, 故 funcA 會被清除 } from './js/utilA'; import utilB from './js/utilB'; // 引入 utilB(函式) 未使用,會被清除 import classC from './js/utilC'; // 引入 classC(類) 未使用,不會被清除 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() {} // 類方法不會被清除 classC.prototype.saySomething = function() { console.log('class C'); } export default classC; |
打包的配置也很簡單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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
對壓縮的檔案進行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 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]); |
引入但是沒用的變數,函式都會清除,未執行的程式碼也會被清除。但是類方法是不會被清除的。因為 webpack 不會區分不了是定義在 classC 的 prototype 還是其它 Array 的 prototype 的,比如 classC 寫成下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 掛載的物件是 classC,這種程式碼,靜態分析是分析不了的,就算能靜態分析程式碼,想要正確完全的分析也比較困難。所以 webpack 乾脆不處理類方法,不對類方法進行 tree shaking。
更多的 tree shaking 的副作用可以查閱:Tree shaking class methods
webpack3 如何做到 scope hoisting?
scope hoisting,顧名思義就是將模組的作用域提升,在 webpack 中不能將所有所有的模組直接放在同一個作用域下,有以下幾個原因:
- 按需載入的模組
- 使用 commonjs 規範的模組
- 被多 entry 共享的模組
在 webpack3 中,這些情況生成的模組不會進行作用域提升,下面我就舉個例子來說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 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); }) |
這個例子比較典型,utilA 被 pageA 和 pageB 所共享,utilB 被 pageB 單獨載入,utilC 被 pageB 非同步載入。
想要 webpack3 生效,則需要在 plugins 中新增 ModuleConcatenationPlugin。
webpack 配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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, }) ] } |
執行 npm run build:hoist
進行編譯,簡單看下生成的 pageB 程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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]); |
通過程式碼分析,可以得出下面的結論:
- 因為我們配置了共享模組抽離,所以 utilA 被抽出為單獨模組,故這部分內容不會進行作用域提升。
- utilB 無牽無掛,被 pageB 單獨載入,所以這部分不會生成新的模組,而是直接作用域提升到 pageB 中。
- utilC 被非同步載入,需要抽離成單獨模組,很明顯沒辦法作用域提升。
結尾
好了,講到這差不多就完了,理解上面的內容對前端模組化會有更多的認知,如果有什麼寫的不對或者不完整的地方,還望補充說明,希望這篇文章能幫助到你。