webpack打包是如何運(yùn)行的
- 也可以稱為,webpack是如何實(shí)現(xiàn)模塊化的
- CommonJS是同步加載模塊,一般用于node。因?yàn)閚ode應(yīng)用程序運(yùn)行在服務(wù)器上,程序通過文件系統(tǒng)可以直接讀取到各個(gè)模塊的文件,特點(diǎn)是響應(yīng)快速,不會(huì)因?yàn)橥蕉枞顺绦虻倪\(yùn)行;
- AMD是異步加載模塊,所以普遍用于前端。而前端項(xiàng)目運(yùn)行在瀏覽器中,每個(gè)模塊都要通過http請求加載js模塊文件,受到網(wǎng)絡(luò)等因素的影響如果同步的話就會(huì)使瀏覽器出現(xiàn)“假死”(卡死)的情況,影響到了用戶體驗(yàn)。
- ESModule 旨在實(shí)現(xiàn)前后端模塊化的統(tǒng)一。而webpack就是把ES6的模塊化代碼轉(zhuǎn)碼成CommonJS的形式,從而兼容瀏覽器的。
- 為什么webpack打包后的文件,可以用在瀏覽器:此時(shí)webpack會(huì)將所有的js模塊打包到bundle.js中(異步加載的模塊除外,異步模塊后面會(huì)講),讀取到了內(nèi)存里,就不會(huì)再分模塊加載了。
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ù)組中的每一個(gè)元素都是一個(gè)函數(shù)
(function(modules) {// 安裝過的模塊都存放在這里面// 作用是把已經(jīng)加載過的模塊緩存在內(nèi)存中,提升性能var installedModules = {};// 去數(shù)組中加載一個(gè)模塊,moduleId 為要加載模塊在數(shù)組中的 index// __webpack_require__作用和 Node.js 中 require 語句相似function __webpack_require__(moduleId) {// require 模塊時(shí)先判斷是否已經(jīng)緩存, 已經(jīng)緩存的模塊直接返回if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// 如果緩存中不存在需要加載的模塊,就新建一個(gè)模塊,并把它存在緩存中var module = installedModules[moduleId] = {// 模塊在數(shù)組中的indexi: moduleId,// 該模塊是否已加載完畢l: false,// 該模塊的導(dǎo)出值,也叫模塊主體內(nèi)容, 會(huì)被重寫exports: {}};// 從 modules 中獲取 index 為 moduleId 的模塊對應(yīng)的函數(shù)// 再調(diào)用這個(gè)函數(shù),同時(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,用于加載被分割出去的異步代碼,這個(gè)暫時(shí)還沒有用到__webpack_require__.p = "";// Load entry module and return exports// 準(zhǔn)備工作做完了, require 一下入口模塊, 讓項(xiàng)目跑起來// 使用 __webpack_require__ 去加載 index 為 0 的模塊,并且返回該模塊導(dǎo)出的內(nèi)容// index 為 0 的模塊就是 index.js文件,也就是執(zhí)行入口模塊// __webpack_require__.s 的含義是啟動(dòng)模塊對應(yīng)的 indexreturn __webpack_require__(__webpack_require__.s = 0);
})
/***** 華麗的分割線 上邊時(shí) webpack 初始化代碼, 下邊是我們寫的模塊代碼 *******/
// 所有的模塊都存放在了一個(gè)數(shù)組里,根據(jù)每個(gè)模塊在數(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',};})
]);
- 上面是一個(gè)立即執(zhí)行函數(shù),簡單點(diǎn)寫:
(function(modules) {// 模擬 require 語句function __webpack_require__(index) {return [/*存放所有模塊的數(shù)組中,第index個(gè)模塊暴露的東西*/]}// 執(zhí)行存放所有模塊數(shù)組中的第0個(gè)模塊,并且返回該模塊導(dǎo)出的內(nèi)容return __webpack_require__(0);})([/*存放所有模塊的數(shù)組*/])
- bundle.js 能直接運(yùn)行在瀏覽器中的原因在于:
- webpack通過 _webpack_require_ 函數(shù)(該函數(shù)定義了一個(gè)可以在瀏覽器中執(zhí)行的加載函數(shù))模擬了模塊的加載(類似于Node.js 中的 require 語句),把定義的模塊內(nèi)容掛載到module.exports上;
- 同時(shí)__webpack_require__函數(shù)中也對模塊緩存做了優(yōu)化,執(zhí)行加載過的模塊不會(huì)再執(zhí)行第二次,執(zhí)行結(jié)果會(huì)緩存在內(nèi)存中,當(dāng)某個(gè)模塊第二次被訪問時(shí)會(huì)直接去內(nèi)存中讀取被緩存的返回值。
- 原來一個(gè)個(gè)獨(dú)立的模塊文件被合并到了一個(gè)單獨(dú)的 bundle.js 的原因在于,瀏覽器不能像 Node.js 那樣快速地去本地加載一個(gè)個(gè)模塊文件,而必須通過網(wǎng)絡(luò)請求去加載還未得到的文件。 如果模塊數(shù)量很多,加載時(shí)間會(huì)很長,因此把所有模塊都存放在了數(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,表明是一個(gè)模塊對象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)容在另外一個(gè)js文件中),通過網(wǎng)絡(luò)請求即時(shí)加載對應(yīng)的異步模塊代碼,再繼續(xù)接下去的流程。
- 在給單頁應(yīng)用做按需加載優(yōu)化時(shí),一般采用以下原則:
- 把整個(gè)網(wǎng)站劃分成一個(gè)個(gè)小功能,再按照每個(gè)功能的相關(guān)程度把它們分成幾類。
- 把每一類合并為一個(gè) Chunk,按需加載對應(yīng)的 Chunk。
- 對于用戶首次打開你的網(wǎng)站時(shí)需要看到的畫面所對應(yīng)的功能,不要對它們做按需加載,而是放到執(zhí)行入口所在的 Chunk 中,以降低用戶能感知的網(wǎng)頁加載時(shí)間。
- 對于個(gè)別依賴大量代碼的功能點(diǎn),例如依賴 Chart.js 去畫圖表、依賴 flv.js 去播放視頻的功能點(diǎn),可再對其進(jìn)行按需加載。
- 被分割出去的代碼的加載需要一定的時(shí)機(jī)去觸發(fā),也就是當(dāng)用戶操作到了或者即將操作到對應(yīng)的功能時(shí)再去加載對應(yīng)的代碼。 被分割出去的代碼的加載時(shí)機(jī)需要開發(fā)者自己去根據(jù)網(wǎng)頁的需求去衡量和確定。
- 由于被分割出去進(jìn)行按需加載的代碼在加載的過程中也需要耗時(shí),你可以預(yù)言用戶接下來可能會(huì)進(jìn)行的操作,并提前加載好對應(yīng)的代碼,從而讓用戶感知不到網(wǎng)絡(luò)加載時(shí)間。
- 舉個(gè)例子
- 網(wǎng)頁首次加載時(shí)只加載 main.js 文件,網(wǎng)頁會(huì)展示一個(gè)按鈕,main.js 文件中只包含監(jiān)聽按鈕事件和加載按需加載的代碼。當(dāng)按鈕被點(diǎn)擊時(shí)才去加載被分割出去的 show.js 文件,加載成功后再執(zhí)行 show.js 里的函數(shù)。
- main.js 文件
window.document.getElementById('btn').addEventListener('click', function () {// 當(dāng)按鈕被點(diǎn)擊后才去加載 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 遇到了類似的語句時(shí)會(huì)這樣處理:
- 以 ./show.js 為入口新生成一個(gè) Chunk;
- 當(dāng)代碼執(zhí)行到 import 所在語句時(shí)才會(huì)去加載由 Chunk 對應(yīng)生成的文件。
- import 返回一個(gè) Promise,當(dāng)文件加載成功時(shí)可以在 Promise 的 then 方法中獲取到 show.js 導(dǎo)出的內(nèi)容。
-
webpack有個(gè)require.ensure api語法來標(biāo)記為異步加載模塊,最新的webpack4推薦使用新的import() api(需要配合@babel/plugin-syntax-dynamic-import插件)。
-
因?yàn)閞equire.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'
}
- 打包后會(huì)生成兩個(gè)chunk文件,分別是主文件執(zhí)行入口文件 bundle.js 和 異步加載文件 0.bundle.js。
// 0.bundle.js
// 異步模塊
// window["webpackJsonp"]是連接多個(gè)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';});})}
]);
- 異步模塊打包后的文件中保存著異步模塊源代碼,同時(shí)為了區(qū)分不同的異步模塊,還保存著該異步模塊對應(yīng)的標(biāo)識:chunkId。以上代碼主動(dòng)調(diào)用window[“webpackJsonp”].push函數(shù),該函數(shù)是連接異步模塊與主模塊的關(guān)鍵函數(shù),該函數(shù)定義在主文件中,實(shí)際上window[“webpackJsonp”].push = webpackJsonpCallback,詳細(xì)源碼咱們看看主文件打包后的代碼bundle.js:
(function(modules) {// 獲取到異步chunk代碼后的回調(diào)函數(shù)// 連接兩個(gè)模塊文件的關(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中// 此時(shí)萬事俱備,異步代碼都已經(jīng)同步加載到主模塊中for(moduleId in moreModules) {modules[moduleId] = moreModules[moduleId];}// 重點(diǎn):執(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í)行到異步代碼時(shí),會(huì)去執(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模塊的實(shí)現(xiàn),是基于自己實(shí)現(xiàn)的webpack_require,所以代碼能跑在瀏覽器中。
- 從 webpack2 開始,已經(jīng)內(nèi)置了對 ES6、CommonJS、AMD 模塊化語句的支持。但不包括新的ES6語法轉(zhuǎn)為ES5代碼,這部分工作還是留給了babel及其插件。
- 在webpack中可以同時(shí)使用ES6模塊和CommonJS模塊。因?yàn)?module.exports很像export default,所以ES6模塊可以很方便兼容 CommonJS:import XXX from ‘commonjs-module’。反過來CommonJS兼容ES6模塊,需要額外加上default:require(‘es-module’).default。
- webpack異步加載模塊實(shí)現(xiàn)流程跟jsonp基本一致。
?
本面試題為前端常考面試題,后續(xù)有機(jī)會(huì)繼續(xù)完善。我是歌謠,一個(gè)沉迷于故事的講述者。
歡迎一起私信交流。
“睡服“面試官系列之各系列目錄匯總(建議學(xué)習(xí)收藏)?
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)
總結(jié)
以上是生活随笔為你收集整理的“约见”面试官系列之常见面试题第四十四篇之webpack打包原理解析?(建议收藏)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。