webpack打包是如何運行的
- 也可以稱為,webpack是如何實現(xiàn)模塊化的
- CommonJS是同步加載模塊,一般用于node。因為node應(yīng)用程序運行在服務(wù)器上,程序通過文件系統(tǒng)可以直接讀取到各個模塊的文件,特點是響應(yīng)快速,不會因為同步而阻塞了程序的運行;
- AMD是異步加載模塊,所以普遍用于前端。而前端項目運行在瀏覽器中,每個模塊都要通過http請求加載js模塊文件,受到網(wǎng)絡(luò)等因素的影響如果同步的話就會使瀏覽器出現(xiàn)“假死”(卡死)的情況,影響到了用戶體驗。
- ESModule 旨在實現(xiàn)前后端模塊化的統(tǒng)一。而webpack就是把ES6的模塊化代碼轉(zhuǎn)碼成CommonJS的形式,從而兼容瀏覽器的。
- 為什么webpack打包后的文件,可以用在瀏覽器:此時webpack會將所有的js模塊打包到bundle.js中(異步加載的模塊除外,異步模塊后面會講),讀取到了內(nèi)存里,就不會再分模塊加載了。
webpack對CommonJS的模塊化處理
- 舉例: const foo = require('./foo');console.log(foo);
console.log('我是高級前端工程師~');
module.exports = {name: 'quanquan',job: 'fe',
};
- 當(dāng)我們執(zhí)行webpack之后,打包完成,可以看到bundle.js內(nèi)的代碼
// modules 即為存放所有模塊的數(shù)組,數(shù)組中的每一個元素都是一個函數(shù)
(function(modules) {// 安裝過的模塊都存放在這里面// 作用是把已經(jīng)加載過的模塊緩存在內(nèi)存中,提升性能var installedModules = {};// 去數(shù)組中加載一個模塊,moduleId 為要加載模塊在數(shù)組中的 index// __webpack_require__作用和 Node.js 中 require 語句相似function __webpack_require__(moduleId) {// require 模塊時先判斷是否已經(jīng)緩存, 已經(jīng)緩存的模塊直接返回if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// 如果緩存中不存在需要加載的模塊,就新建一個模塊,并把它存在緩存中var module = installedModules[moduleId] = {// 模塊在數(shù)組中的indexi: moduleId,// 該模塊是否已加載完畢l: false,// 該模塊的導(dǎo)出值,也叫模塊主體內(nèi)容, 會被重寫exports: {}};// 從 modules 中獲取 index 為 moduleId 的模塊對應(yīng)的函數(shù)// 再調(diào)用這個函數(shù),同時把函數(shù)需要的參數(shù)傳入,this指向模塊的主體內(nèi)容modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// 將模塊標(biāo)記為已加載module.l = true;// 返回模塊的導(dǎo)出值,即模塊主體內(nèi)容return module.exports;}// 向外暴露所有的模塊__webpack_require__.m = modules;// 向外暴露已緩存的模塊__webpack_require__.c = installedModules;......// Webpack 配置中的 publicPath,用于加載被分割出去的異步代碼,這個暫時還沒有用到__webpack_require__.p = "";// Load entry module and return exports// 準(zhǔn)備工作做完了, require 一下入口模塊, 讓項目跑起來// 使用 __webpack_require__ 去加載 index 為 0 的模塊,并且返回該模塊導(dǎo)出的內(nèi)容// index 為 0 的模塊就是 index.js文件,也就是執(zhí)行入口模塊// __webpack_require__.s 的含義是啟動模塊對應(yīng)的 indexreturn __webpack_require__(__webpack_require__.s = 0);
})
/***** 華麗的分割線 上邊時 webpack 初始化代碼, 下邊是我們寫的模塊代碼 *******/
// 所有的模塊都存放在了一個數(shù)組里,根據(jù)每個模塊在數(shù)組的 index 來區(qū)分和定位模塊
([/* 模塊 0 對應(yīng) index.js */(function(module, exports, __webpack_require__) {// 通過 __webpack_require__ 規(guī)范導(dǎo)入 foo 函數(shù),foo.js 對應(yīng)的模塊 index 為 1const foo = __webpack_require__(1);console.log(foo);console.log('我是高級前端工程師~');}),/* 模塊 1 對應(yīng) foo.js */(function(module, exports) {// 通過 CommonJS 規(guī)范導(dǎo)出對象module.exports = {name: 'quanquan',job: 'fe',};})
]);
- 上面是一個立即執(zhí)行函數(shù),簡單點寫:
(function(modules) {// 模擬 require 語句function __webpack_require__(index) {return [/*存放所有模塊的數(shù)組中,第index個模塊暴露的東西*/]}// 執(zhí)行存放所有模塊數(shù)組中的第0個模塊,并且返回該模塊導(dǎo)出的內(nèi)容return __webpack_require__(0);})([/*存放所有模塊的數(shù)組*/])
- bundle.js 能直接運行在瀏覽器中的原因在于:
- webpack通過 _webpack_require_ 函數(shù)(該函數(shù)定義了一個可以在瀏覽器中執(zhí)行的加載函數(shù))模擬了模塊的加載(類似于Node.js 中的 require 語句),把定義的模塊內(nèi)容掛載到module.exports上;
- 同時__webpack_require__函數(shù)中也對模塊緩存做了優(yōu)化,執(zhí)行加載過的模塊不會再執(zhí)行第二次,執(zhí)行結(jié)果會緩存在內(nèi)存中,當(dāng)某個模塊第二次被訪問時會直接去內(nèi)存中讀取被緩存的返回值。
- 原來一個個獨立的模塊文件被合并到了一個單獨的 bundle.js 的原因在于,瀏覽器不能像 Node.js 那樣快速地去本地加載一個個模塊文件,而必須通過網(wǎng)絡(luò)請求去加載還未得到的文件。 如果模塊數(shù)量很多,加載時間會很長,因此把所有模塊都存放在了數(shù)組中,執(zhí)行一次網(wǎng)絡(luò)加載。
webpack對es6 Module模塊化的處理
- 舉例 const foo = require('./foo');?
import foo from './foo';?console.log(foo);
console.log('我是高級前端工程師~');
module.exports = {?
export default {?name: 'quanquan',job: 'fe',
};
- 打包完后bundle.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;}__webpack_require__.m = modules;__webpack_require__.c = installedModules;__webpack_require__.d = function(exports, name, getter) {if(!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, {configurable: false,enumerable: true,get: getter});}};__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;};__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };__webpack_require__.p = "";return __webpack_require__(__webpack_require__.s = 0);
})([相關(guān)模塊]);
- 打包好的內(nèi)容和commonjs模塊化方法差不多
function(module, __webpack_exports__, __webpack_require__) {"use strict";// 在__webpack_exports__上定義__esModule為true,表明是一個模塊對象Object.defineProperty(__webpack_exports__, "__esModule", { value: true });var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);console.log(__WEBPACK_IMPORTED_MODULE_0__foo__["a"]);console.log('我是高級前端工程師~');
},
function(module, __webpack_exports__, __webpack_require__) {"use strict";__webpack_exports__["a"] = ({name: 'quanquan',job: 'fe',});
}
- 和 commonjs 不同的地方
- 首先, 包裝函數(shù)的參數(shù)之前的 module.exports 變成了_webpack_exports_
- 其次, 在使用了 es6 模塊導(dǎo)入語法(import)的地方, 給__webpack_exports__添加了屬性__esModule
- 其余的部分和 commonjs 類似
webpack文件的按需加載
- 以上webpack把所有模塊打包到主文件中,所以模塊加載方式都是同步方式。但在開發(fā)應(yīng)用過程中,按需加載(也叫懶加載)也是經(jīng)常使用的優(yōu)化技巧之一。
- 按需加載,通俗講就是代碼執(zhí)行到異步模塊(模塊內(nèi)容在另外一個js文件中),通過網(wǎng)絡(luò)請求即時加載對應(yīng)的異步模塊代碼,再繼續(xù)接下去的流程。
- 在給單頁應(yīng)用做按需加載優(yōu)化時,一般采用以下原則:
- 把整個網(wǎng)站劃分成一個個小功能,再按照每個功能的相關(guān)程度把它們分成幾類。
- 把每一類合并為一個 Chunk,按需加載對應(yīng)的 Chunk。
- 對于用戶首次打開你的網(wǎng)站時需要看到的畫面所對應(yīng)的功能,不要對它們做按需加載,而是放到執(zhí)行入口所在的 Chunk 中,以降低用戶能感知的網(wǎng)頁加載時間。
- 對于個別依賴大量代碼的功能點,例如依賴 Chart.js 去畫圖表、依賴 flv.js 去播放視頻的功能點,可再對其進(jìn)行按需加載。
- 被分割出去的代碼的加載需要一定的時機(jī)去觸發(fā),也就是當(dāng)用戶操作到了或者即將操作到對應(yīng)的功能時再去加載對應(yīng)的代碼。 被分割出去的代碼的加載時機(jī)需要開發(fā)者自己去根據(jù)網(wǎng)頁的需求去衡量和確定。
- 由于被分割出去進(jìn)行按需加載的代碼在加載的過程中也需要耗時,你可以預(yù)言用戶接下來可能會進(jìn)行的操作,并提前加載好對應(yīng)的代碼,從而讓用戶感知不到網(wǎng)絡(luò)加載時間。
- 舉個例子
- 網(wǎng)頁首次加載時只加載 main.js 文件,網(wǎng)頁會展示一個按鈕,main.js 文件中只包含監(jiān)聽按鈕事件和加載按需加載的代碼。當(dāng)按鈕被點擊時才去加載被分割出去的 show.js 文件,加載成功后再執(zhí)行 show.js 里的函數(shù)。
- main.js 文件
window.document.getElementById('btn').addEventListener('click', function () {// 當(dāng)按鈕被點擊后才去加載 show.js 文件,文件加載成功后執(zhí)行文件導(dǎo)出的函數(shù)import(/* webpackChunkName: "show" */ './show').then((show) => {show('Webpack');})
});
module.exports = function (content) {window.alert('Hello ' + content);
};
- 代碼中最關(guān)鍵的一句是 import(/* webpackChunkName: “show” / ‘./show’),Webpack 內(nèi)置了對 import() 語句的支持,當(dāng) Webpack 遇到了類似的語句時會這樣處理:
- 以 ./show.js 為入口新生成一個 Chunk;
- 當(dāng)代碼執(zhí)行到 import 所在語句時才會去加載由 Chunk 對應(yīng)生成的文件。
- import 返回一個 Promise,當(dāng)文件加載成功時可以在 Promise 的 then 方法中獲取到 show.js 導(dǎo)出的內(nèi)容。
-
webpack有個require.ensure api語法來標(biāo)記為異步加載模塊,最新的webpack4推薦使用新的import() api(需要配合@babel/plugin-syntax-dynamic-import插件)。
-
因為require.ensure是通過回調(diào)函數(shù)執(zhí)行接下來的流程,而import()返回promise,這意味著可以使用最新的ES8 async/await語法,使得可以像書寫同步代碼一樣,執(zhí)行異步流程。
按需加載輸出代碼分析
- 舉例 // main.js
import Add from './add'
console.log(Add, Add(1, 2), 123)// 按需加載
// 方式1: require.ensure
// require.ensure([], function(require){
// var asyncModule = require('./async')
// console.log(asyncModule.default, 234)
// })// 方式2: webpack4新的import語法
// 需要加@babel/plugin-syntax-dynamic-import插件
let asyncModuleWarp = async () => await import('./async')
console.log(asyncModuleWarp().default, 234)
// async.js
export default function() {return 'hello, aysnc module'
}
- 打包后會生成兩個chunk文件,分別是主文件執(zhí)行入口文件 bundle.js 和 異步加載文件 0.bundle.js。
// 0.bundle.js
// 異步模塊
// window["webpackJsonp"]是連接多個chunk文件的橋梁
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], // 異步模塊標(biāo)識chunkId,可判斷異步代碼是否加載成功// 跟同步模塊一樣,存放了{(lán)模塊路徑:模塊內(nèi)容}{"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);__webpack_exports__["default"] = (function () {return 'hello, aysnc module';});})}
]);
- 異步模塊打包后的文件中保存著異步模塊源代碼,同時為了區(qū)分不同的異步模塊,還保存著該異步模塊對應(yīng)的標(biāo)識:chunkId。以上代碼主動調(diào)用window[“webpackJsonp”].push函數(shù),該函數(shù)是連接異步模塊與主模塊的關(guān)鍵函數(shù),該函數(shù)定義在主文件中,實際上window[“webpackJsonp”].push = webpackJsonpCallback,詳細(xì)源碼咱們看看主文件打包后的代碼bundle.js:
(function(modules) {// 獲取到異步chunk代碼后的回調(diào)函數(shù)// 連接兩個模塊文件的關(guān)鍵函數(shù)function webpackJsonpCallback(data) {var chunkIds = data[0]; //data[0]存放了異步模塊對應(yīng)的chunkIdvar moreModules = data[1]; // data[1]存放了異步模塊代碼// 標(biāo)記異步模塊已加載成功var moduleId, chunkId, i = 0, resolves = [];for(;i < chunkIds.length; i++) {chunkId = chunkIds[i];if(installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);}installedChunks[chunkId] = 0;}// 把異步模塊代碼都存放到modules中// 此時萬事俱備,異步代碼都已經(jīng)同步加載到主模塊中for(moduleId in moreModules) {modules[moduleId] = moreModules[moduleId];}// 重點:執(zhí)行resolve() = installedChunks[chunkId][0]()返回promisewhile(resolves.length) {resolves.shift()();}};// 記錄哪些chunk已加載完成var installedChunks = {"main": 0};// __webpack_require__依然是同步讀取模塊代碼作用function __webpack_require__(moduleId) {...}// 加載異步模塊__webpack_require__.e = function requireEnsure(chunkId) {// 創(chuàng)建promise// 把resolve保存到installedChunks[chunkId]中,等待代碼加載好再執(zhí)行resolve()以返回promisevar promise = new Promise(function(resolve, reject) {installedChunks[chunkId] = [resolve, reject];});// 通過往head頭部插入script標(biāo)簽異步加載到chunk代碼var script = document.createElement('script');script.charset = 'utf-8';script.timeout = 120;script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"var onScriptComplete = function (event) {var chunk = installedChunks[chunkId];};script.onerror = script.onload = onScriptComplete;document.head.appendChild(script);return promise;};var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];// 關(guān)鍵代碼: window["webpackJsonp"].push = webpackJsonpCallbackjsonpArray.push = webpackJsonpCallback;// 入口執(zhí)行return __webpack_require__(__webpack_require__.s = "./src/main.js");})({"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {...}),"./src/main.js": (function(module, exports, __webpack_require__) {// 同步方式var Add = __webpack_require__("./src/add.js").default;console.log(Add, Add(1, 2), 123);// 異步方式var asyncModuleWarp =function () {var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {return regeneratorRuntime.wrap(function _callee$(_context) {// 執(zhí)行到異步代碼時,會去執(zhí)行__webpack_require__.e方法// __webpack_require__.e其返回promise,表示異步代碼都已經(jīng)加載到主模塊了// 接下來像同步一樣,直接加載模塊return __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/async.js"))}, _callee);}));return function asyncModuleWarp() {return _ref.apply(this, arguments);};}();console.log(asyncModuleWarp().default, 234)})
});
總結(jié)
- webpack對于ES模塊/CommonJS模塊的實現(xiàn),是基于自己實現(xiàn)的webpack_require,所以代碼能跑在瀏覽器中。
- 從 webpack2 開始,已經(jīng)內(nèi)置了對 ES6、CommonJS、AMD 模塊化語句的支持。但不包括新的ES6語法轉(zhuǎn)為ES5代碼,這部分工作還是留給了babel及其插件。
- 在webpack中可以同時使用ES6模塊和CommonJS模塊。因為 module.exports很像export default,所以ES6模塊可以很方便兼容 CommonJS:import XXX from ‘commonjs-module’。反過來CommonJS兼容ES6模塊,需要額外加上default:require(‘es-module’).default。
- webpack異步加載模塊實現(xiàn)流程跟jsonp基本一致。
?
本面試題為前端常考面試題,后續(xù)有機(jī)會繼續(xù)完善。我是歌謠,一個沉迷于故事的講述者。
歡迎一起私信交流。
“睡服“面試官系列之各系列目錄匯總(建議學(xué)習(xí)收藏)?
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎
總結(jié)
以上是生活随笔為你收集整理的“约见”面试官系列之常见面试题第四十四篇之webpack打包原理解析?(建议收藏)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。