從 bundle.js 源碼學習 Webpack

做者/Youhe(前端時空)html

公衆號「前端時空」每日一題活動 回覆「1」看面試題 | 回覆「2」看答案前端

文章已同步發表於webpack

微信公衆號「前端時空」web

用逆向思惟解決問題

一道典型的場景面試題。一共有140g鹽,如何用一個天平和兩個2g,7g的砝碼分三次成90g、50g。這道題用常規思路想可能會很麻煩,可是若是用逆向思惟就容易的多了。首先若是要湊成50g,最後一步必定是拿兩份25g的鹽,25g又能夠用砝碼和鹽來湊,用2g和7g湊成9g鹽,再稱出7g鹽,把全部砝碼和這兩堆鹽湊在一塊兒,9 + 9 + 7 = 25g。 這樣三次就能夠稱出來50g的鹽。面試

從bundle文件開始

咱們在學習前端、學習webpack的時候,也不妨利用逆向思惟分析問題。按常規來看,學習webpack最好的方式是知曉其背後的原理。事實上,webpack是一個將一切資源都當成模塊的模塊化打包工具。其打包步驟爲:數組

  1. 初始化 webpack.config.js,獲得最後的配置結果。
  2. 初始化compiler對象,註冊全部配置的插件。
  3. 根據入口文件,分析模塊依賴。
  4. 使用對應loader處理對應文件。
  5. 獲得每一個文件結果,包含每一個模塊以及他們之間的依賴關係,生成chunk。webpack將全部的模塊打包成一個函數。
  6. 生成bundle.js文件。

在生成bundle.js文件後,html頁面就能夠利用script標籤的src去引入該文件。
咱們用一個未使用plugin、loader的簡單Demo去就去扒一扒生成bundle.js文件源碼,看看有哪些值得咱們學習的地方,同時從這個角度去思考webpack。微信

自執行函數

首先在主體上看,bundle.js是一個自執行匿名函數,經過傳入一個對象參數(版本v4.0.0+,舊版本是一個數組)。下面是將無關代碼去掉的精簡部分。入口文件是一個index.js文件,在index.js中使用import引入了test.js。markdown

(function (modules) {
// 已安裝模塊
var installedModules = {}
// __webpack_require__函數
function __webpack_require__(moduleId) {
//代碼
}

/*
主體內容

...
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
...

*/

return __webpack_require__(__webpack_require__.s = "./js/index.js");
})
({

"./js/index.js":
(function (module, __webpack_exports__, __webpack_require__) {}),
"./js/test.js":
(function (module, __webpack_exports__, __webpack_require__) {}),
});
複製代碼

這就是bundle的主體,十分簡潔明瞭。是一個自執行的匿名函數,接收一個對象做爲參數,這個對象鍵值分別爲模塊路徑與一個匿名函數。函數體內,有一個installedModules對象,從名稱上能夠推斷出是用來存放已安裝模塊的。以後是十分重要的__webpack_require__函數,這個函數用來安裝模塊和獲取已安裝模塊。咱們詳細看下這個函數的內容。app

__webpack_require__函數

function __webpack_require__(moduleId) {
//已安裝模塊,返回模塊得exports
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
//未安裝,安裝模塊
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// 調用參數modules中的鍵值函數,將this指向module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 表示安裝完成
module.l = true;
// 返回模塊得exports
return module.exports;
}
複製代碼

函數接收一個moduleId做爲參數,首先是一個if語句判斷是否installedModules安裝了相應模塊,若是安裝了則直接返回該模塊的exports屬性。若是不存在,將installedModules[moduleId] 賦值一個對象,其中鍵i爲模塊的ID即moduleId,l爲一個布爾型標識符,表明是否安裝完畢,初值爲false,exports爲一個空對象。接下來去調用modules(傳進來的對象參數),根據moduleId執行相應的函數。將this指向了module.exports,也就是剛纔的那個空對象,並傳入三個參數 module、module.exports、 webpack_require
完成後,將module的i置爲true,表示安裝完成。最後返回module的exports框架

主體內容

在__webpack_require__函數以後的代碼,姑且叫它主體內容。下面是精簡後的部分。請硬着頭皮看完這裏,腦海裏留下印象便可。

// modules
__webpack_require__.m = modules;

// installedModules
__webpack_require__.c = installedModules;

// 判斷__webpack_require__.o是否爲flase
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {enumerable: true, get: getter});
}
};

// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};

// 將exports的toStringTag值變成‘[Module Object]’
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, '__esModule', {value: true});
};
複製代碼

在JavaScript中,函數的本質也是對象。這裏將一些屬性存放在__webpack_require__上。
如m、c、d、o、r。(這裏只講敘這幾種),這種寫法的好處是能夠將單個元素既做爲能夠執行的函數,又能做爲一個具備存儲功能的hash結構。

  • m屬性,用modules爲其賦值,即全部模塊的集合。
  • c屬性,用以前介紹過的installedModules爲其賦值,存放已安裝的模塊。
  • o屬性,做爲一個函數,利用Object.prototype.hasOwnProperty.call方法。用來判斷參數一(object)上是否存在參數二(property)屬性。
  • d屬性,判斷是否符合o屬性的方法,若是不是,也就是說參數二name不在參數一exports上,就將getter賦值給exports.name。爲何這麼作?下面會提到這裏,請繼續。
  • r屬性 將exports屬性Symbol.toStringTag賦值爲true,將exports的__esModule屬性賦值爲true。(這樣對exports使用toString()方法時將顯示‘[Object Module]’)

至此以後自執行函數會執行__webpack_require__函數,並傳入入口文件ID。

return __webpack_require__(__webpack_require__.s = "./js/index.js");
//調用\_\_webpack_require__函數,將__webpack_require__.s賦值爲"./js/index.js"後做爲參數傳入執行。
複製代碼

開始執行

執行__webpack_require__函數後,咱們從新進入到函數內部。到這條語句。

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
複製代碼

這裏將根據moduleId找到對應的函數。貼參數部分代碼。

(function (modules) {})
({

"./js/index.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ "./js/test.js");

const textNode = document.createTextNode('my name is wyh')
document.querySelector('#test').appendChild(textNode)
Object(_test__WEBPACK_IMPORTED_MODULE_0__["printA"])()
}),

"./js/test.js":
(function (module, __webpack_exports__, __webpack_require__) {

"use strict"
;
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "printA", function () {
return printA;
});
__webpack_require__.d(__webpack_exports__, "a", function () {
return a;
});

function printA() {
console.log('A');
}
let a = {}
a.name = 'A'
})
});
複製代碼

咱們對比下兩個函數的相同點,其中:

  • 都接收三個參數,分別是module、`__webpack_exports**、__webpack_require**,
    對應__webpack_require__函數中

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);三個參數。

  • 內部都是嚴格模式
  • 都執行webpack_require.r方法。

對比完畢後,而後開始執行,首先是入口"./js/index.js"。這裏聲明瞭一個_test__WEBPACK_IMPORTED_MODULE_0__變量,事實上,若是含有多個依賴,那麼變量名就會從0開始遞增。

_test__WEBPACK_IMPORTED_MODULE_1__、_test__WEBPACK_IMPORTED_MODULE_2__...

調用__webpack_require__方法並傳入全部依賴文件路徑ID,返回值就是對應的Module。在調用該函數的時候,又會調用modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);,調用"./js/test.js"函數。
該函數內部除了咱們本身寫的代碼,還會調用__webpack_require**.d 函數將導出的內容做爲參數傳入,做爲屬性放在其modules上。其中有三個參數。參數一是\ __webpack_exports**,函數內部須要用到,參數2、三分別是屬性名和一個函數。這時候,若是未指定導出的名字(如 export default),那麼在__webpack_require**.o找不到module的defualt屬性,就會返回false,__webpack_require**.d函數就會將defualt屬性存放該函數。
最後返回該module的exports。

Module
a: (...)
printA: (...)
Symbol(Symbol.toStringTag): "Module"
__esModule: true
get a: ƒ ()
get printA: ƒ ()
__proto__: Object
複製代碼

以後,回到"./js/index.js",將module賦值給_test__WEBPACK_IMPORTED_MODULE_0__變量。在執行導入的方法時,將其替換成變量的屬性調用。

import {printA} from './test1'
import add from './test2'
printA()
add(1,2)
//替換後
Object(_test__WEBPACK_IMPORTED_MODULE_0__["printA"])()
Object(_test1__WEBPACK_IMPORTED_MODULE_1__["default"])(1, 2)
複製代碼

這裏的Object將導入內容進行拷貝,防止如原內容的引用地址發生改變發生的錯誤。

尾聲

至此,一個簡單的bundle.js就分析完畢了。咱們對webpack生成bundle文件有了解以後,會更加有利學習打包過程以及原理。

精彩文章

理想主義團隊的開源做品之Chameleon跨端框架 React 中必會的 10 個概念 一道面試題引起關於 js 隱式轉換的思考 前端首屏耗時測量方法 一分鐘理解 JavaScript 發佈訂閱模式 前端響應式你瞭解多少?

關注咱們

相關文章
相關標籤/搜索