hello~親愛的看官老爺們大家好~ 最近一直在學習 webpack 的相關知識,當清晰地領悟到 webpack
就是不同 loader
和 plugin
組合起來打包之後,只作為工具使用而言,算是入門了。當然,在過程中碰到數之不盡的坑,也產生了想要深入一點了解 webpack
的原理(主要是掉進坑能靠自己爬出來)。因而就從簡單的入手,先看看使用 webpack
打包後的 JS
檔案是如何載入吧。
友情提示,本文簡單易懂,就算沒用過 webpack
問題都不大。如果已經瞭解過相關知識的朋友,不妨快速閱讀一下,算是溫故知新 ,其實是想請你告訴我哪裡寫得不對。
簡單配置
既然需要用到 webpack
,還是需要簡單配置一下的,這裡就簡單貼一下程式碼,首先是 webpack.config.js
:
const path = require('path');
const webpack = require('webpack');
//用於插入html模板
const HtmlWebpackPlugin = require('html-webpack-plugin');
//清除輸出目錄,免得每次手動刪除
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
index: path.join(__dirname, 'index.js'),
},
output: {
path: path.join(__dirname, '/dist'),
filename: 'js/[name].[chunkhash:4].js'
},
module: {},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
}),
//持久化moduleId,主要是為了之後研究載入程式碼好看一點。
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
})
]
};
複製程式碼
這是我能想到近乎最簡單的配置,用到的兩個額外下載的外掛都是十分常用的,也已經在註釋中簡單說明了。
之後是兩個簡單的 js
檔案:
// test.js
const str = 'test is loaded';
module.exports = str;
// index.js
const test = require('./src/js/test');
console.log(test);
複製程式碼
這個就不解釋了,貼一下打包後,專案的目錄結構應該是這樣的:
至此,我們的配置就完成了。
從 index.js
開始看程式碼
先從打包後的 index.html
檔案看看兩個 JS
檔案的載入順序:
<body>
<script type="text/javascript" src="js/manifest.2730.js"></script>
<script type="text/javascript" src="js/index.5f4f.js"></script>
</body>
複製程式碼
可以看到,打包後 js
檔案的載入順序是先 manifest.js
,之後才是 index.js
,按理說應該先看 manifest.js
的內容的。然而這裡先賣個關子,我們先看看 index.js
的內容是什麼,這樣可以帶著問題去了解 manifest.js
,也就是主流程的邏輯到底是怎樣的,為何能做到模組化。
// index.js
webpackJsonp([0], {
"JkW7": (function(module, exports, __webpack_require__) {
const test = __webpack_require__("zFrx");
console.log(test);
}),
"zFrx": (function(module, exports) {
const str = 'test is loaded';
module.exports = str;
})
}, ["JkW7"]);
複製程式碼
刪去各種奇怪的註釋後剩下這麼點內容,首先應該關注到的是 webpackJsonp
這個函式,可以看見是不在任何名稱空間下的,也就是 manifest.js
應該定義了一個掛在 window
下的全域性函式,index.js
往這個函式傳入三個引數並呼叫。
第一個引數是陣列,現在暫時還不清楚這個陣列有什麼作用。
第二個引數是一個物件,物件內都是方法,這些方法看起來至少接受兩個引數(名為 zFrx
的方法只有兩個形參)。看一眼這兩個方法的內部,其實看見了十分熟悉的東西, module.exports
,儘管看不見 require
, 但有一個樣子類似的 __webpack_require__
,這兩個應該是模組化的關鍵,先記下這兩個函式。
第三個引數也是一個陣列,也不清楚是有何作用的,但我們觀察到它的值是 JkW7
,與引數2中的某個方法的鍵是一致的,這可能存在某種邏輯關聯。
至此,index.js
的內容算是過了一遍,接下來應當帶著問題在 manifest.js
中尋找答案。
manifest.js
程式碼閱讀
由於沒有配置任何壓縮 js
的選項,因此 manifest.js
的原始碼大約在 150 行左右,簡化後為 28 行(已經跑過程式碼,實測沒問題)。鑑於精簡後的程式碼真的不多,因而先貼程式碼,大家帶著剛才提出的問題,先看看能找到幾個答案:
(function(modules) {
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, result;
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (executeModules) {
for (i = 0; i < executeModules.length; i++) {
result = __webpack_require__(executeModules[i]);
}
}
return result;
};
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
})([]);
複製程式碼
首先應該看到的是,manifest.js
內部是一個 IIFE
,就是自執行函式咯,這個函式會接受一個空陣列作為引數,該陣列被命名為 modules
。之後看到我們在 index.js
中的猜想,果然在 window
上掛了一個名為 webpackJsonp
的函式。它接受的三個引數,分別名為chunkIds
, moreModules
, executeModules
。對應了 index.js
中呼叫 webpackJsonp
時傳入的三個引數。而 webpackJsonp
內究竟是有怎樣的邏輯呢?
先不管定義的引數,webpackJsonp
先是 for in
遍歷了一次 moreModules
,將 moreModules
內的所有方法都存在 modules
, 也就是自執行函式執行時傳入的陣列。
之後是一個條件判斷:
if (executeModules) {
for (i = 0; i < executeModules.length; i++) {
result = __webpack_require__(executeModules[i]);
}
}
複製程式碼
判斷 executeModules
, 也就是第三個引數是否存在,如存在即執行 __webpack_require__
方法。在 index.js
呼叫 webpackJsonp
方法時,這個引數當然是存在的,因而要看看 __webpack_require__
方法是什麼了。
__webpack_require__
接受一個名為 moduleId
的引數。方法內部首先是一個條件判斷,先不管。接下來看到賦值邏輯
var module = installedModules[moduleId] = {
exports: {}
};
複製程式碼
結合剛才的條件判斷,可以推測出 installedModules
是一個快取的容器,那麼前面的程式碼意思就是如果快取中有對應的 moduleId
,那麼直接返回它的 exports
,不然就定義並賦值一個吧。接著先偷看一下 __webpack_require__
的最後的返回值,可以看到函式返回的是 module.exports
,那麼 module.exports
又是如何被賦值的呢? 看看之後的程式碼:
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
複製程式碼
剛才我們知道 modules[moduleId]
就是 moreModules
中的方法,此處就是將 this
指定為 module.exports
,再把module
, module.exports
, __webpack_require__
傳入去作為引數呼叫。這三個引數是不是很熟悉?之前我們看 index.js
裡面程式碼時,有一個疑問就是模組化是如何實現的。這裡我們已經看出了眉目。
其實 webpack
就是將每一個 js
檔案封裝成一個函式,每個檔案中的 require
方法對應的就是 __webpack_require__
, __webpack_require__
會根據傳入的 moduleId
再去載入對應的程式碼。而當我們想匯出 js
檔案的值時,要麼用 module.exports
,要麼用 exports
,這就對應了module
, module.exports
兩個引數。少接觸這塊的童鞋,應該就能理解為何匯出值時,直接使用 exports = xxx
會匯出失敗了。簡單舉個例子:
const module = {
exports: {}
};
function demo1(module) {
module.exports = 1;
}
demo1(module);
console.log(module.exports); // 1
function demo2(exports) {
exports = 2;
}
demo2(module.exports);
console.log(module.exports); // 1
複製程式碼
貼上這段程式碼去瀏覽器跑一下,可以發現兩次列印出來都是1。這和 wenpack
打包邏輯是一模一樣的。
梳理一下打包後程式碼執行的流程,首先 minifest.js
會定義一個 webpackJsonp
方法,待其他打包後的檔案(也可稱為 chunk
)呼叫。當呼叫 chunk
時,會先將該 chunk
中所有的 moreModules
, 也就是每一個依賴的檔案也可稱為 module
(如 test.js
)存起來。之後通過 executeModules
判斷這個檔案是不是入口檔案,決定是否執行第一次 __webpack_require__
。而 __webpack_require__
的作用,就是根據這個 module
所 require
的東西,不斷遞迴呼叫 __webpack_require__
,__webpack_require__
函式返回值後供 require
使用。當然,模組是不會重複載入的,因為 installedModules
記錄著 module
呼叫後的 exports
的值,只要命中快取,就返回對應的值而不會再次呼叫 module
。webpack
打包後的檔案,就是通過一個個函式隔離 module
的作用域,以達到不互相汙染的目的。
小結
以上就是 webpack
打包後 js
檔案是載入過程的簡單描述,其實還是有很多細節沒有說完的,比如如何非同步載入對應模組, chunkId
有什麼作用等,但由於篇幅所限 (還沒研究透),不再詳述。相關程式碼會放置 github 中,歡迎隨時查閱順便點star
。
感謝各位看官大人看到這裡,知易行難,希望本文對你有所幫助~謝謝!