手把手教你撸一个简易的 webpack
背景
隨著前端復雜度的不斷提升,誕生出很多打包工具,比如最先的grunt,gulp。到后來的webpack和Parcel。但是目前很多腳手架工具,比如vue-cli已經幫我們集成了一些構建工具的使用。有的時候我們可能并不知道其內部的實現原理。其實了解這些工具的工作方式可以幫助我們更好理解和使用這些工具,也方便我們在項目開發中應用。
一些知識點
在我們開始造輪子前,我們需要對一些知識點做一些儲備工作。
模塊化知識
首先是模塊的相關知識,主要的是 es6 modules 和 commonJS模塊化的規范。更詳細的介紹可以參考這里 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理淺析。現在我們只需要了解:
AST 基礎知識
什么是抽象語法樹?
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。樹上的每個節點都表示源代碼中的一種結構。之所以說語法是「抽象」的,是因為這里的語法并不會表示出真實語法中出現的每個細節。
大家可以通過Esprima 這個網站來將代碼轉化成 ast。首先一段代碼轉化成的抽象語法樹是一個對象,該對象會有一個頂級的type屬性Program,第二個屬性是body是一個數組。body數組中存放的每一項都是一個對象,里面包含了所有的對于該語句的描述信息:
type:描述該語句的類型 --變量聲明語句 kind:變量聲明的關鍵字 -- var declaration: 聲明的內容數組,里面的每一項也是一個對象type: 描述該語句的類型 id: 描述變量名稱的對象type:定義name: 是變量的名字init: 初始化變量值得對象type: 類型value: 值 "is tree" 不帶引號row: "\"is tree"\" 帶引號 復制代碼進入正題
webpack 簡易打包
有了上面這些基礎的知識,我們先來看一下一個簡單的webpack打包的過程,首先我們定義3個文件:
// index.js import a from './test'console.log(a)// test.js import b from './message'const a = 'hello' + bexport default a// message.js const b = 'world'export default b 復制代碼方式很簡單,定義了一個index.js引用test.js;test.js內部引用message.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__);// Flag the module as loadedmodule.l = true;// Return the exports of the modulereturn 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, {enumerable: true, get: getter});}};// define __esModule on exports__webpack_require__.r = function (exports) {if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});}Object.defineProperty(exports, '__esModule', {value: true});};// create a fake namespace object// mode & 1: value is a module id, require it// mode & 2: merge all properties of value into the ns// mode & 4: return value when already ns object// mode & 8|1: behave like require__webpack_require__.t = function (value, mode) {/******/if (mode & 1) value = __webpack_require__(value);if (mode & 8) return value;if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;var ns = Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns, 'default', {enumerable: true, value: value});if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) {return value[key];}.bind(null, key));return ns;};// 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 exportsreturn __webpack_require__(__webpack_require__.s = "./src/index.js"); })({"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { ;eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");}),"./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}) }); 復制代碼看起來很亂?沒關系,我們來屢一下。一眼看過去我們看到的是這樣的形式:
(function(modules) {// ... })({// ... }) 復制代碼這樣好理解了吧,就是一個自執行函數,傳入了一個modules對象,modules 對象是什么樣的格式呢?上面的代碼已經給了我們答案:
{"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}) } 復制代碼是這樣的一個 路徑 --> 函數 這樣的 key,value 鍵值對。而函數內部是我們定義的文件轉移成 ES5 之后的代碼:
; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?"); 復制代碼到這里基本上結構是分析完了,接著我們看看他的執行,自執行函數一開始執行的代碼是:
__webpack_require__(__webpack_require__.s = "./src/index.js"); 復制代碼調用了__webpack_require_函數,并傳入了一個moduleId參數是"./src/index.js"。再看看函數內部的主要實現:
// 定義 module 格式 var module = installedModules[moduleId] = {i: moduleId, // moduleIdl: false, // 是否已經緩存exports: {} // 導出對象,提供掛載};modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 復制代碼這里調用了我們modules中的函數,并傳入了__webpack_require__函數作為函數內部的調用。module.exports參數作為函數內部的導出。因為index.js里面引用了test.js,所以又會通過__webpack_require__來執行對test.js的加載:
var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js"); 復制代碼test.js內又使用了message.js所以,test.js內部又會執行對message.js的加載。message.js執行完成之后,因為沒有依賴項,所以直接返回了結果:
var b = 'world' __webpack_exports__["default"] = (b) 復制代碼執行完成之后,再一級一級返回到根文件index.js。最終完成整個文件依賴的處理。 整個過程中,我們像是通過一個依賴關系樹的形式,不斷地向數的內部進入,等返回結果,又開始回溯到根。
開發一個簡單的 tinypack
通過上面的這些調研,我們先考慮一下一個基礎的打包編譯工具可以做什么?
第一個問題,轉換語法,其實我們可以通過babel來做。核心步驟也就是:
- 通過babylon生成AST
- 通過babel-core將AST重新生成源碼
接著我們需要處理模塊依賴的關系,那就需要得到一個依賴關系視圖。好在babel-traverse提供了一個可以遍歷AST視圖并做處理的功能,通過 ImportDeclaration 可以得到依賴屬性:
function getDependence (ast) {let dependencies = []traverse(ast, {ImportDeclaration: ({node}) => {dependencies.push(node.source.value);},})return dependencies }/*** 生成完整的文件依賴關系映射* @param fileName* @param entry* @returns {{fileName: *, dependence, code: *}}*/ function parse(fileName, entry) {let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileNamelet dirName = entry ? '' : path.dirname(config.entry)let absolutePath = path.join(dirName, filePath)const ast = getAst(absolutePath)return {fileName,dependence: getDependence(ast),code: getTranslateCode(ast),}; } 復制代碼到目前為止,我們也只是得到根文件的依賴關系和編譯后的代碼,比如我們的index.js依賴了test.js但是我們并不知道test.js還需要依賴message.js,他們的源碼也是沒有編譯過。所以此時我們還需要做深度遍歷,得到完成的深度依賴關系:
/*** 獲取深度隊列依賴關系* @param main* @returns {*[]}*/ function getQueue(main) {let queue = [main]for (let asset of queue) {asset.dependence.forEach(function (dep) {let child = parse(dep)queue.push(child)})}return queue } 復制代碼那么進行到這一步我們已經完成了所有文件的編譯解析。最后一步,就是需要我們按照webpack的思想對源碼進行一些包裝。第一步,先是要生成一個modules對象:
function bundle(queue) {let modules = ''queue.forEach(function (mod) {modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`})// ... } 復制代碼得到 modules 對象后,接下來便是對整體文件的外部包裝,注冊require,module.exports:
(function(modules) {function require(fileName) {// ...}require('${config.entry}');})({${modules}}) 復制代碼而函數內部,也只是循環執行每個依賴文件的 JS 代碼而已,完成代碼:
function bundle(queue) {let modules = ''queue.forEach(function (mod) {modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`})const result = `(function(modules) {function require(fileName) {const fn = modules[fileName];const module = { exports : {} };fn(require, module, module.exports);return module.exports;}require('${config.entry}');})({${modules}})`;return result; } 復制代碼到這里基本上也就介紹完了,我們來打包試一下:
(function (modules) {function require(fileName) {const fn = modules[fileName];const module = {exports: {}};fn(require, module, module.exports);return module.exports;}require('./src/index.js'); })({'./src/index.js': function (require, module, exports) { ;var _test = require("./test");var _test2 = _interopRequireDefault(_test);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : {default: obj};}console.log(_test2.default);}, './test': function (require, module, exports) { ;Object.defineProperty(exports, "__esModule", {value: true});var _message = require("./message");var _message2 = _interopRequireDefault(_message);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : {default: obj};}var a = 'hello' + _message2.default;exports.default = a;}, './message': function (require, module, exports) { ;Object.defineProperty(exports, "__esModule", {value: true});var b = 'world';exports.default = b;}, }) 復制代碼再測試一下:
恩,基本上已經完成一個簡易的 tinypack。
參考文章
抽象語法樹 Abstract syntax tree
一看就懂的JS抽象語法樹
源碼
tinypack 所有的源碼已經上傳 github
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的手把手教你撸一个简易的 webpack的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux操作系统中查看进程命令有哪些
- 下一篇: CentOS7.5下搭建zabbix3.