1、單入口文件如何打包javascript
/src/single/index.js前端
var index2 = require('./index2'); var util = require('./util'); console.log(index2); console.log(util);
/src/single/index2.jsjava
var util = require('./util'); console.log(util); module.exports = "index 2";
/src/single/util.jswebpack
module.exports = "Hello World";
/config/webpack.config.single.jsgit
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' } };
運行webpack命令es6
webpack --config ./config/webpack.config.single.js
獲得的單個打包文件/dist/index.35dff1f1.jsgithub
(function(modules) { // webpackBootstrap // The module cache已經加載過的模塊 var installedModules = {}; // The require function模塊加載函數 function __webpack_require__(moduleId) { // Check if module is in cache判斷模塊是否已經加載過,若加載過直接返回加載的模塊 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function執行加載函數 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded標記該模塊已經加載 module.l = true; // Return the exports of the module return module.exports; } // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = 1); })([ /* 0 */ (function(module, exports) { module.exports = "Hello World"; }), /* 1 */ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(2); }), /* 2 */ (function(module, exports, __webpack_require__) { var index2 = __webpack_require__(3); var util = __webpack_require__(0); console.log(index2); console.log(util); }), /* 3 */ (function(module, exports, __webpack_require__) { var util = __webpack_require__(0); console.log(util); module.exports = "index 2"; }) ]);
__webpack_require__
模塊加載,先判斷 installedModules 是否已加載,加載過了就直接返回 exports 數據,沒有加載過該模塊就經過 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
執行模塊而且將 module.exports 給返回。上述過程仍是比較簡單的,但有些點須要注意:web
一、每一個模塊只會加載一次,因此重複加載的模塊只會執行一次,加載過的模塊會放到 installedModules,下次須要須要該模塊的值就直接從裏面拿了。json
二、模塊的 id 直接經過數組下標去一一對應的,這樣能保證簡單且惟一,經過其它方式好比文件名或文件路徑的方式就比較麻煩,由於文件名可能出現重名,不惟一,文件路徑則會增大文件體積,而且將路徑暴露給前端,不夠安全。數組
三、modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
保證了模塊加載時 this 的指向 module.exports 而且傳入默認參數
2、多入口文件如何進行代碼切割
咱們在開發一些較複雜的應用時,若是沒有對代碼進行切割,將第三方庫(jQuery)或框架(React) 和業務代碼所有打包在一塊兒,就會致使用戶訪問頁面速度很慢,不能有效利用緩存。
// /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,咱們但願代碼切割作到:
那麼 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 是不夠的,這樣只會生成兩個 bundle 文件,將 pageA 和 pageB 所須要的內容所有放入,跟單入口文件並無區別,要作到代碼切割,咱們須要藉助 webpack 內置的插件 CommonsChunkPlugin。
首先 webpack 執行存在一部分運行時代碼,即一部分初始化的工做,就像以前單文件中的 __webpack_require__
,這部分代碼須要加載於全部文件以前,至關於初始化工做,少了這部分初始化代碼,後面加載過來的代碼就沒法識別並工做了。
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: 2, })
這段代碼的含義是,在這些入口文件中,找到那些引用兩次的模塊(如:utilB),幫我抽離成一個叫 vendor 文件,此時那部分初始化工做的代碼會被抽離到 vendor 文件中。
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 文件。
因此這時候咱們頁面加載文件的順序就會變成:
mainifest.xxxx.js // 初始化代碼 vendor.xxxx.js // pageA 和 pageB 共同用到的模塊,抽離 pageX.xxxx.js // 業務代碼 當 pageB 須要 utilC 時候則異步加載 utilC
執行命令
結果生成了5個文件:異步加載文件utilC.js單獨打包成了一個文件0.×××.js,入口pageA,pageB分別打包成文件,pageA和pageB共用模塊單獨打包成vendor, 初始化代碼manifest
那麼manifest如何作初始化工做呢?
(function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading var parentJsonpFunction = window["webpackJsonp"]; window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = [], result; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while(resolves.length) { resolves.shift()(); } if(executeModules) { for(i=0; i < executeModules.length; i++) { result = __webpack_require__(__webpack_require__.s = executeModules[i]); } } return result; }; // The module cache var installedModules = {}; // objects to store loaded and loading chunks var installedChunks = { 4: 0 }; // The require function模塊加載函數 function __webpack_require__(moduleId) { // Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports; } // This file contains only the entry chunk. // The chunk loading function for additional chunks __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } // a Promise means "currently loading". if(installedChunkData) { return installedChunkData[2]; } // setup Promise in chunk cache 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; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f"}[chunkId] + ".js"; var timeout = setTimeout(onScriptComplete, 120000); script.onerror = script.onload = onScriptComplete; function onScriptComplete() { // avoid mem leaks in IE. 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; }; // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // on error function for async loading __webpack_require__.oe = function(err) { console.error(err); throw err; }; })([]);
與單文件內容一致,定義了一個自執行函數,由於它不包含任何模塊,因此傳入一個空數組。除了定義了 __webpack_require__
,還另外定義了兩個函數用來進行加載模塊。
首先講解代碼前須要理解兩個概念,分別是 module 和 chunk
__webpack_require__
加載的模塊,一樣的使用數組下標做爲 moduleId 且是惟一不重複的。那麼爲何要區分 chunk 和 module 呢?
首先使用 installedChunks 來保存每一個 chunkId 是否被加載過,若是被加載過,則說明該 chunk 中所包含的模塊已經被放到了 modules 中,注意是 modules 而不是 installedModules。咱們先來簡單看一下 vendor chunk 打包出來的內容。
webpackJsonp([3],[ /* 0 */ (function(module, exports) { module.exports = 'util B'; }) ]);
在執行完 manifest 後就會先執行 vendor 文件,結合上面 webpackJsonp 的定義,咱們能夠知道 [3] 表明 chunkId,當加載到 vendor 文件後,installedChunks[3] 將會被置爲 0,這代表 chunk3 已經被加載過了。
webpack如何加載異步腳本:
// 異步加載函數掛載在__webpack_require__.e // This file contains only the entry chunk. // The chunk loading function for additional chunks __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } // a Promise means "currently loading". if(installedChunkData) { return installedChunkData[2]; } // setup Promise in chunk cache 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; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = __webpack_require__.p + "" + chunkId + "." + {"0":"ae9c5f5f"}[chunkId] + ".js"; var timeout = setTimeout(onScriptComplete, 120000); script.onerror = script.onload = onScriptComplete; function onScriptComplete() { // avoid mem leaks in IE. 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; };
大體分爲三種狀況,(已經加載過,正在加載中以及從未加載過)
經過 utilC 生成的 chunk 來進行講解:
webpackJsonp([0],[ /* 0 */, /* 1 */ (function(module, exports) { module.exports = "util C"; }) ]);
pageB須要異步加載這個chunk:
webpackJsonp([2],{ 4: (function(module, exports, __webpack_require__) { const utilB = __webpack_require__(0); console.log(utilB); const utilC = () => __webpack_require__.e/* require.ensure */(0).then((function(require) { console.log(__webpack_require__(1)) }).bind(null, __webpack_require__)).catch(__webpack_require__.oe); utilC(); }) },[4]);
當 pageB 進行某種操做須要加載 utilC 時就會執行 __webpack_require__.e(0, callback),
0,表明須要加載的模塊 chunkId(utilC),異步加載 utilC 並將 callback 添加到 installedChunks[0] 中,而後當 utilC 的 chunk 文件加載完畢後,chunkIds 包含 1,發現 installedChunks[0] 是個數組,裏面還有以前還未執行的 callback 函數
既然這樣,那我就將我本身帶來的模塊先放到 modules 中,而後再統一執行以前未執行完的 callbacks 函數,這裏指的是存放於 installedChunks[1] 中的回調函數 (可能存在多個),這也就是說明這裏的前後順序:
// 先將 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__);
webpack2 中會默認加載 OccurrenceOrderPlugin 這個插件,即你不用 plugins 中添加這個配置它也會默認執行,那它有什麼用途呢?主要是在 webpack1 中 moduleId 的不肯定性致使的,在 webpack1 中 moduleId 取決於引入文件的順序,這就會致使這個 moduleId 可能會時常發生變化, 而 OccurrenceOrderPlugin 插件會按引入次數最多的模塊進行排序,引入次數的模塊的 moduleId 越小,好比說上面引用的 utilB 模塊引用次數爲 2(最多),因此它的 moduleId 爲 0。
最後說下在異步加載模塊時, webpack2 是基於 Promise 的,因此說若是你要兼容低版本瀏覽器,須要引入 Promise-polyfill
,另外爲引入請求添加了錯誤處理。
在 webpack1 的時候,若是因爲網絡緣由當你加載腳本失敗後,即便網絡恢復了,你再次進行某種操做須要同個 chunk 時候都會無效,主要緣由是失敗以後沒把 installedChunks[chunkId] = undefined;
致使以後不會再對該 chunk 文件發起異步請求。
而在 webpack2 中,當腳本請求超時了(2min)或者加載失敗,會將 installedChunks[chunkId] 清空,當下次從新請求該 chunk 文件會從新加載,提升了頁面的容錯性
3、webpack2如何作到tree shaking?
什麼是 tree shaking,即 webpack 在打包的過程當中會將沒用的代碼進行清除(dead code)。通常 dead code 具備一下的特徵:
是否是很神奇,那麼須要怎麼作才能使 tree shaking 生效呢?
首先,模塊引入要基於 ES6 模塊機制,再也不使用 commonjs 規範,由於 es6 模塊的依賴關係是肯定的,和運行時的狀態無關,能夠進行可靠的靜態分析,而後清除沒用的代碼。而 commonjs 的依賴關係是要到運行時候才能肯定下來的。
其次,須要開啓 UglifyJsPlugin 這個插件對代碼進行壓縮。
咱們先寫一個例子來講明:
// 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;
打包的配置也很簡單:
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 } }) ] }
對壓縮的文件進行分析:
// 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 寫成下面這樣:
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
4、webpack3如何作到scope hoisting?
scope hoisting,顧名思義就是將模塊的做用域提高,在 webpack 中不能將全部全部的模塊直接放在同一個做用域下,有如下幾個緣由:
在 webpack3 中,這些狀況生成的模塊不會進行做用域提高,下面我就舉個例子來講明:
// 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 配置以下:
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, }) ] }
運行webpack --config ./config/webpack.config.hoist.js進行編譯,簡單看下生成的 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]);
經過代碼分析,能夠得出下面的結論:
轉自:https://github.com/happylindz/blog/issues/6