在使用webpack的過程當中,你是否好奇webpack打包的代碼爲何能夠直接在瀏覽器中跑?爲何webpack能夠支持各類ES6最新語法?爲何在webpack中能夠書寫import ES6模塊,也支持require CommonJS模塊?html
關於模塊,咱們先來認識下目前主流的模塊規範(自從有了ES6 Module及Webpack等工具,AMD/CMD規範生存空間已經很小了):前端
ES6前,js沒有屬於本身的模塊規範,因此社區制定了 CommonJS規範。而NodeJS所使用的模塊系統就是基於CommonJS規範實現的。node
// CommonJS 導出
module.exports = { age: 1, a: 'hello', foo:function(){} }
// CommonJS 導入
const foo = require('./foo.js')
複製代碼
根據當前運行環境的判斷,若是是 Node 環境 就是使用 CommonJS 規範, 若是不是就判斷是否爲 AMD 環境, 最後導出全局變量。這樣代碼能夠同時運行在Node和瀏覽器環境中。目前大部分庫都是打包成UMD規範,Webpack也支持UMD打包,配置API是output.libraryTarget。詳細案例能夠看筆者封裝的npm工具包:cache-manage-jswebpack
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.libName = factory());
}(this, (function () { 'use strict';})));
複製代碼
ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。具體思想和語法能夠看筆者的另一篇文章:ES6-模塊詳解git
// es6模塊 導出
export default { age: 1, a: 'hello', foo:function(){} }
// es6模塊 導入
import foo from './foo'
複製代碼
既然模塊規範有這麼多,那webpack是如何去解析不一樣的模塊呢?es6
webpack根據webpack.config.js中的入口文件,在入口文件裏識別模塊依賴,無論這裏的模塊依賴是用CommonJS寫的,仍是ES6 Module規範寫的,webpack會自動進行分析,並經過轉換、編譯代碼,打包成最終的文件。最終文件中的模塊實現是基於webpack本身實現的webpack_require(es5代碼)
,因此打包後的文件能夠跑在瀏覽器上。github
同時以上意味着在webapck環境下,你能夠只使用ES6 模塊語法書寫代碼(一般咱們都是這麼作的),也可使用CommonJS模塊語法,甚至能夠二者混合使用。由於從webpack2開始,內置了對ES六、CommonJS、AMD 模塊化語句的支持,webpack會對各類模塊進行語法分析,並作轉換編譯
。web
咱們舉個例子來分析下打包後的源碼文件,例子源代碼在 webpack-module-examplenpm
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
// JavaScript 執行入口文件
entry: './src/main.js',
output: {
// 把全部依賴的模塊合併輸出到一個 bundle.js 文件
filename: 'bundle.js',
// 輸出文件都放到 dist 目錄下
path: path.resolve(__dirname, './dist'),
}
};
複製代碼
// src/add
export default function(a, b) {
let { name } = { name: 'hello world,'} // 這裏特地使用了ES6語法
return name + a + b
}
// src/main.js
import Add from './add'
console.log(Add, Add(1, 2))
複製代碼
打包後精簡的bundle.js文件以下:json
// modules是存放全部模塊的數組,數組中每一個元素存儲{ 模塊路徑: 模塊導出代碼函數 }
(function(modules) {
// 模塊緩存做用,已加載的模塊能夠不用再從新讀取,提高性能
var installedModules = {};
// 關鍵函數,加載模塊代碼
// 形式有點像Node的CommonJS模塊,但這裏是可跑在瀏覽器上的es5代碼
function __webpack_require__(moduleId) {
// 緩存檢查,有則直接從緩存中取得
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 先建立一個空模塊,塞入緩存中
var module = installedModules[moduleId] = {
i: moduleId,
l: false, // 標記是否已經加載
exports: {} // 初始模塊爲空
};
// 把要加載的模塊內容,掛載到module.exports上
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true; // 標記爲已加載
// 返回加載的模塊,調用方直接調用便可
return module.exports;
}
// __webpack_require__對象下的r函數
// 在module.exports上定義__esModule爲true,代表是一個模塊對象
__webpack_require__.r = function(exports) {
Object.defineProperty(exports, '__esModule', { value: true });
};
// 啓動入口模塊main.js
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
// add模塊
"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {
// 在module.exports上定義__esModule爲true
__webpack_require__.r(__webpack_exports__);
// 直接把add模塊內容,賦給module.exports.default對象上
__webpack_exports__["default"] = (function(a, b) {
let { name } = { name: 'hello world,'}
return name + a + b
});
}),
// 入口模塊
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__)
// 拿到add模塊的定義
// _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports,有點相似require
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");
// add模塊內容: _add__WEBPACK_IMPORTED_MODULE_0__["default"]
console.log(_add__WEBPACK_IMPORTED_MODULE_0__["default"], Object(_add__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
})
});
複製代碼
以上核心代碼中,能讓打包後的代碼直接跑在瀏覽器中,是由於webpack經過__webpack_require__ 函數模擬了模塊的加載(相似於node中的require語法),把定義的模塊內容掛載到module.exports上。同時__webpack_require__函數中也對模塊緩存作了優化,防止模塊二次從新加載,優化性能。
再讓咱們看下webpack的源碼:
// webpack/lib/MainTemplate.js
// 主文件模板
// webpack生成的最終文件叫chunk,chunk包含若干的邏輯模塊,即爲module
this.hooks.render.tap( "MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
// 入口內容,__webpack_require__就在bootstrapSource中
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })\n");
source.add(
"/************************************************************************/\n"
);
source.add("/******/ (");
source.add(
// 依賴的module都會寫入對應數組
this.hooks.modules.call(
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
複製代碼
可能細心的讀者看到,以上打包後的add模塊代碼中依然仍是ES6語法,在低端的瀏覽器中不支持。這是由於沒有對應的loader去解析js代碼,webpack把全部的資源都視做模塊,不一樣的資源使用不一樣的loader進行轉換。
這裏須要使用babel-loader及其插件@babel/preset-env進行處理,把ES6代碼轉換成可在瀏覽器中跑的es5代碼。
// webpack.config.js
module.exports = {
...,
module: {
rules: [
{
// 對以js後綴的文件資源,用babel進行處理
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
複製代碼
// 通過babel處理es6語法後的代碼
__webpack_exports__["default"] = (function (a, b) {
var _name = { name: 'hello world,' }, name = _name.name;
return name + a + b;
});
複製代碼
以上webpack把全部模塊打包到主文件中,因此模塊加載方式都是同步方式。但在開發應用過程當中,按需加載(也叫懶加載)也是常用的優化技巧之一。按需加載,通俗講就是代碼執行到異步模塊(模塊內容在另一個js文件中),經過網絡請求即時加載對應的異步模塊代碼,再繼續接下去的流程。那webpack是如何執行代碼時,判斷哪些代碼是異步模塊呢?webpack又是如何加載異步模塊呢?
webpack有個require.ensure api語法來標記爲異步加載模塊,最新的webpack4推薦使用新的import() api(須要配合@babel/plugin-syntax-dynamic-import插件)。由於require.ensure是經過回調函數執行接下來的流程,而import()返回promise,這意味着可使用最新的ES8 async/await語法,使得能夠像書寫同步代碼同樣,執行異步流程。
如今咱們從webpack打包後的源碼來看下,webpack是如何實現異步模塊加載的。修改入口文件main.js,引入異步模塊async.js:
// 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文件,分別是主文件main.bundle.js
以及異步模塊文件0.bundle.js
。一樣,爲方便讀者快速理解,精簡保留主流程代碼。
// 0.bundle.js
// 異步模塊
// window["webpackJsonp"]是鏈接多個chunk文件的橋樑
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[0], // 異步模塊標識chunkId,可判斷異步代碼是否加載成功
// 跟同步模塊同樣,存放了{模塊路徑:模塊內容}
{
"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (function () {
return 'hello, aysnc module';
});
})
}
]);
複製代碼
以上知道,異步模塊打包後的文件中保存着異步模塊源代碼,同時爲了區分不一樣的異步模塊,還保存着該異步模塊對應的標識:chunkId。以上代碼主動調用window["webpackJsonp"].push函數,該函數是鏈接異步模塊與主模塊的關鍵函數,該函數定義在主文件中,實際上window["webpackJsonp"].push = webpackJsonpCallback
,詳細源碼我們看看主文件打包後的代碼:
// main.bundle.js
(function(modules) {
// 獲取到異步chunk代碼後的回調函數
// 鏈接兩個模塊文件的關鍵函數
function webpackJsonpCallback(data) {
var chunkIds = data[0]; //data[0]存放了異步模塊對應的chunkId
var moreModules = data[1]; // data[1]存放了異步模塊代碼
// 標記異步模塊已加載成功
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中
// 此時萬事俱備,異步代碼都已經同步加載到主模塊中
for(moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
// 重點:執行resolve() = installedChunks[chunkId][0]()返回promise
while(resolves.length) {
resolves.shift()();
}
};
// 記錄哪些chunk已加載完成
var installedChunks = {
"main": 0
};
// __webpack_require__依然是同步讀取模塊代碼做用
function __webpack_require__(moduleId) {
...
}
// 加載異步模塊
__webpack_require__.e = function requireEnsure(chunkId) {
// 建立promise
// 把resolve保存到installedChunks[chunkId]中,等待代碼加載好再執行resolve()以返回promise
var promise = new Promise(function(resolve, reject) {
installedChunks[chunkId] = [resolve, reject];
});
// 經過往head頭部插入script標籤異步加載到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"] || [];
// 關鍵代碼: window["webpackJsonp"].push = webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
// 入口執行
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) {
// 執行到異步代碼時,會去執行__webpack_require__.e方法
// __webpack_require__.e其返回promise,表示異步代碼都已經加載到主模塊了
// 接下來像同步同樣,直接加載模塊
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)
})
});
複製代碼
從上面源碼能夠知道,webpack實現模塊的異步加載有點像jsonp的流程。在主js文件中經過在head中構建script標籤方式,異步加載模塊信息;再使用回調函數webpackJsonpCallback,把異步的模塊源碼同步到主文件中,因此後續操做異步模塊能夠像同步模塊同樣。 源碼具體實現流程:
__webpack_require__.e
函數去把異步代碼加載進來。該函數會在html的head中動態增長script標籤,src指向指定的異步模塊存放的文件。webpackJsonpCallback
函數,把異步模塊加載到主文件中。__webpack_require__("./src/async.js")
加載異步模塊。注意源碼中的primose使用很是精妙,主模塊加載完成異步模塊才resolve()