【手把手帶你配 webpack】第二步, 面試官-談談你對模組化的理解

圈圈Dei圈發表於2019-01-28

上一步提到, 對於我配出來的 webpack 領導並不是很認可, 那隻好...

2019-01-24-10-36-53
這裡用了程式設計師小灰的圖, 侵刪.(ps: 我也不知道連結的是不是原創)

尷尬的面試題

問: 你知道的 js 模組化方案有哪些?
答: js 的模組化一般來說有 Commonjs AMD(CMD) ESModule 等, Commonjs 使用的同步載入一般用於 node, AMD(CMD) 由於是非同步載入所以普遍用在前端. ESModule 旨在實現前後端模組化的統一. 言簡意駭, 點到為止. 簡直優秀...

2019-01-24-11-10-43

問: 那麼為什麼同步的 Commonjs 要用在 node, 非同步的 AMD 要用在前端呢?
答: node 應用程式執行在伺服器上, 程式通過檔案系統可以直接讀取到各個模組的檔案, 特點是響應快速不會因為同步而阻塞了程式的執行. 前端專案執行在瀏覽器中, 每個模組都要通過 http 請求載入 js 檔案, 受到網路等因素的影響如果同步的話會使得瀏覽器出現"假死"的情況 --- 也就是卡住了. 影響使用者體驗. 整場邂逅已經完全 cover 住了有沒有 ^_^.

2019-01-24-11-41-30

問: 那你聽過 webpack 嗎? 知道 webpack 是怎麼實現模組化的嗎?
答: webpack 看這裡, webpack 的模組化其實就是把 ES6 模組化程式碼轉碼成 Commonjs 的形式(咦, 有點慌, 但是面試題就是這麼背的呀), 從而相容瀏覽器的. 前後矛盾, 已經開始有點雞動啦, 但是強忍著希望矇混過關...

88ad2618a1b1a81e05309c025

問: 好的, 既然你說 Commonjs 的同步方式前端不適用, 為什麼 webpack 轉碼的 Commonjs 形式以後可以前端執行呢?
答: emmmmm

面試官: 那要不今天的面試就先到這兒吧, 你先回去等通知...
我: hmmmmm

2019-01-24-14-15-46

webpack 到底做了什麼

一遍又一遍的執行了 webpack 命令, 一次又一次的研究生成的 bundle.js. 嗯哼? 所有的 js 模組都打包到了一個 bundle.js 裡了, 根本沒有分模組載入, 那還管他丫的同步還是非同步. js 檔案直接讀取到記憶體裡一步到位, 比 nodejs 需要讀取檔案還要方便...

那麼, webpack 到底是怎樣把這麼多檔案組合成一個 bundle.js 檔案的呢? 仔細想一下, 還是把天書和大家一起學習一下吧, 要不感覺這個系列的文章會變成表情包分享教程! 2019-01-25-16-16-47

接下來我會分享 Commonjs 和 es6 Module 兩種模組化方式 webpack 的處理, 如果有小夥伴對這兩種方式有疑問, 請 google 一下. 其實所謂的模組化無非就是給一段程式碼新增了兩把鑰匙實現和其他程式碼的通訊, 相互呼叫, 通過 require 獲取引入其他模組的能力, 通過 export 實現向其他模組輸出方法的功能.

commonjs 模組化的處理

首先新增一個 npm script 修改 package.json 如下圖:

2019-01-25-16-21-54

新增完成後程式碼, 就可以拋棄 ./node_modules/.bin/webpack 這個命令了, 打包時只需要 npm run build 即可.

2019-01-25-16-24-01

搭建專案程式碼, 程式碼地址, 其中, index.js 內容:

const foo = require('./foo');

console.log(foo);
console.log('我是高階前端工程師~');
複製程式碼

foo.js 內容:

module.exports = {
  name: 'quanquan',
  job: 'fe',
};
複製程式碼

純純粹粹的 Commonjs 程式碼, 執行 npm run build 後, 我們再看看打包後的 bundle.js. (ps: 新增了部分註釋)

(function(modules) {
	// 快取模組物件
	var installedModules = {};
	// 模擬 commonjs 實現的 require
	function __webpack_require__(moduleId) {
		// require 模組時先判斷是否已經快取, 已經快取的模組直接返回
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		// (模擬)建立一個模組, 並把新模組的引用儲存到快取中
		var module = installedModules[moduleId] = {
            // 模組 id
            i: moduleId,
            // 模組是否已載入
            l: false,
            // 模組主體內容, 會被重寫
			exports: {}
		};
		// 執行以下模組的包裝函式, 並把模組內部的 this 志向模組主體
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		// 將模組標記為已載入
		module.l = true;
		// 返回模組主體內容
		return module.exports;
	}
    // 向外暴露所有的模組
	__webpack_require__.m = modules;
	// 向外暴露已快取的模組
    __webpack_require__.c = installedModules;

    // 下邊兩個方法暫時還沒有用到
	// define getter function for harmony exports
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, {
				configurable: false,
				enumerable: true,
				get: getter
			});
		}
	};
	// getDefaultExport function for compatibility with non-harmony modules
	__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;
    };

    // Object.prototype.hasOwnProperty.call 這個就沒啥好解釋的啦
    // js 權威指南上有說
    __webpack_require__.o = function(object, property) {
        return Object.prototype.hasOwnProperty.call(object, property);
    };

    // __webpack_public_path__
    // 這個暫時還沒有用到
	__webpack_require__.p = "";
    // Load entry module and return exports
    // 準備工作做完了, require 一下入口模組, 讓專案跑起來
	return __webpack_require__(__webpack_require__.s = 0);
})
/********  華麗的分割線 上邊時 webpack 初始化程式碼, 下邊是我們寫的模組程式碼 ***************/
([
/* 模組 0 對應 index.js */
/***/ (function(module, exports, __webpack_require__) {

const foo = __webpack_require__(1);

console.log(foo);
console.log('我是高階前端工程師~');


/***/ }),
/* 模組 1 對應 foo.js */
/***/ (function(module, exports) {

module.exports = {
  name: 'quanquan',
  job: 'fe',
};


/***/ })
]);
複製程式碼

原來 webpack 就是把我們寫的程式碼用一個一個的包裝函式包裝了起來, 再執行__webpack_require__的時候呼叫一下包裝函式. 通過包裝函式內部的程式碼重寫了引數中引數 module 的 exports 屬性, 獲取到我們編寫的模組的主體程式碼.

所以我們看到了 index.js 包裝後的程式碼為:

function(module, exports, __webpack_require__) {
    const foo = __webpack_require__(1);
    console.log(foo);
    console.log('我是高階前端工程師~');
}
複製程式碼

foo.js 包裝後的程式碼為:

function(module, exports) {
    module.exports = {
        name: 'quanquan',
        job: 'fe',
    };
}
複製程式碼

由於 index.js 中有 require('./foo') 所以 index.js 生成的包裝函式引數中多了__webpack_require__用於匯入 foo 模組, 從頭到尾看下來, 之前的天書似乎不再晦澀難懂了. 當然這一塊內容需要大家稍微動動腦筋思考一下. 也希望大家把自己的想法和問題放到評論區, 我們一起討論.

2019-01-25-19-18-47

es6 Module 模組化的處理

改動我們的專案程式碼為 es6 模組化程式碼, 首先修改 index.js:

import foo from './foo';

console.log(foo);
console.log('我是高階前端工程師~');
複製程式碼

其次修改 foo.js

export default {
  name: 'quanquan',
  job: 'fe',
};
複製程式碼

最後就是命令列執行 npm run build 進行打包啦. 打包完成後 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);
})([
  function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    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 模組化方式大同小異. webpack 初始化程式碼完成相同.

index.js 生成的包裹程式碼為:

function(module, __webpack_exports__, __webpack_require__) {
	"use strict";
	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('我是高階前端工程師~');
}
複製程式碼

foo.js 生成的包裹程式碼為:

function(module, __webpack_exports__, __webpack_require__) {
	"use strict";
	__webpack_exports__["a"] = ({
		name: 'quanquan',
		job: 'fe',
	});
}
複製程式碼

不難發現, 和 commonjs 不同的地方:

  • 首先, 包裝函式的引數之前的 module.exports 變成了__webpack_exports__
  • 其次, 在使用了 es6 模組匯入語法(import)的地方, 給__webpack_exports__新增了屬性__esModule
  • 其餘的部分和 commonjs 類似

我們發現, commonjs 中咩有用到的 __webpack_require__.n 這個方法還是沒有用到, 這難道是廢方法? 那豈不是可以給 webpack 提個 issue, 參與過得開源專案不再是空白. 美滋滋...

2019-01-28-11-42-30

要是這樣都能搭上這種國際開源專案的班車, 豈不是太過容易了呢?

2019-01-28-11-52-20

es6 Module CommonJs 混合使用

修改 foo.js 內容如下:

module.exports = {
  name: 'quanquan',
  job: 'fe',
};
複製程式碼

打包的結果為:

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__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;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
// 這裡用到了 __webpack_require__.n
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__foo__);


console.log(__WEBPACK_IMPORTED_MODULE_0__foo___default.a);
console.log('我是高階前端工程師~');
/* harmony default export */ __webpack_exports__["default"] = ({});


/***/ }),
/* 1 */
/***/ (function(module, exports) {

module.exports = {
  name: 'quanquan',
  job: 'fe',
};


/***/ })
/******/ ]);
複製程式碼

index.js 沒有變化, 也就是說當匯出模組匯出的語法為 commonjs 而匯入模組的匯入語法為 es6 時, 匯入模組就會用到了 __webpack_require__.n 這個方法.

下集預告: 到這裡一個純 js 檔案的打包結果就分析完了, webpack 巧妙的把所有的模組都打包到了一個 bundle.js 檔案中從而實現同步載入模組, 實在高明. 但是, 如果你的專案很大, 動輒成百上千的 js 模組, 此時運用 webpack 又能怎樣實現程式碼拆分呢? 我們下一步一起討論.

2019-01-28-12-14-32

相關文章